import { isObject } from 'lodash';

import { EventEmitter } from '../../emitter';
import { Database, LocalModel } from '../../types';
import { uuid } from '../../utils';

// These are the operators nedb supports
const simpleOperators = {
  $and: true,
  $exists: true,
  $gt: true,
  $gte: true,
  $in: true,
  $lt: true,
  $lte: true,
  $ne: true,
  $nin: true,
  $not: true,
  $or: true,
  $regex: true,
  $size: true,
};

interface Callback {
  (err?: Error, data?: any): void;
}

export function createWebWorkerDbFactory(worker: Worker) {
  let workerEmitter = new EventEmitter();

  worker.onmessage = (event: MessageEvent) => {
    workerEmitter.emit('message', event);
  };

  class WebWorkerDb extends Database {
    private worker: Worker;
    private cbMaps: {[id: string]: Callback};
    private dbid: string;

    constructor() {
      super();

      this.worker = worker;
      this.cbMaps = {};
      this.dbid = uuid();

      workerEmitter.addListener('message', (event: MessageEvent) => {
        let data = event.data.data;
        let error = event.data.error;
        let id = event.data.id;
        let cb = this.cbMaps[id];

        if (cb) {
          cb(error, data);
        }
      });
    }

    public update(res: LocalModel) {
      if (res.$deleted) {
        return this.runWorkerFunction('remove', res.$id);
      } else {
        let doc = res.$toObject();
        // Stick on the internal id
        doc.$id = res.$id;
        return this.runWorkerFunction('update', doc);
      }
    }

    public query(qry: any) {
      return this.runWorkerFunction('query', qry) as Promise<string[]>;
    }

    public qryIsSimple(qry: any): boolean {
      let simple = true;

      if (Array.isArray(qry)) {
        qry.forEach(val => {
          let kosher = this.qryIsSimple(val);
          if (!kosher) {
            simple = false;
            return false;
          }
        });
      } else if (isObject(qry)) {
        for (let key of Object.keys(qry)) {
          let val = qry[key];
          // The key is fine if it doesn't begin with $ or is a simple operator
          let kosherKey = (key[0] !== '$') || simpleOperators[key];

          if (!kosherKey) {
            simple = false;
            break;
          }

          let valKosher = this.qryIsSimple(val);

          if (!valKosher) {
            simple = false;
            break;
          }
        }
      }

      return simple;
    }

    private runWorkerFunction(fnName: string, ...args: any[]) {
      return new Promise((resolve, reject) => {
        let id = uuid();
        this.worker.postMessage({
          fnName,
          id,
          dbid: this.dbid,
          args,
        });

        this.cbMaps[id] = (err, resp) => {
          delete this.cbMaps[id];
          if (err) {
            reject(err);
          } else {
            resolve(resp);
          }
        };
      });
    }
  }

  return () => {
    return new WebWorkerDb() as Database;
  };
}
