// tslint:disable only-arrow-functions
import { cloneDeep } from 'lodash';
import { isObject } from 'lodash';

import * as Datastore from 'nedb/browser-version/out/nedb';

const me = self as DedicatedWorkerGlobalScope;

me.onmessage = handleMessage;

const _DB_MAP: {[dbid: string]: Database} = {};

declare type Callback = (err?: Error, val?: any) => void;

class Database {
  private db: Datastore;
  private RES_TO_DB_ID_MAP: {[id: string]: string};

  constructor() {
    this.db = new Datastore();

    this.RES_TO_DB_ID_MAP = {};

    // Stick an index on __id - our id field.
    this.db.ensureIndex({ fieldName: '__id' }, function (err) {
      if (err) {
        throw err;
      }
    });
  }

  public remove(id: string, callback: Callback) {
    let dbid = this.RES_TO_DB_ID_MAP[id];
    if (dbid) {
      delete this.RES_TO_DB_ID_MAP[id];
      this.db.remove({_id: dbid}, {multi: true}, function(err) {
        callback(err);
      });
    } else {
      callback(new Error(`No object to remove with id ${id}`));
    }
  }

  public update(doc: any, callback: Callback) {
    // We need to transform the resource by replacing the _id field (nedb uses its own id in that
    // place). Instead call it __id
    doc.__id = doc._id;
    doc.__$id = doc.$id;
    delete doc._id;
    delete doc.$id;
    let dbid = this.RES_TO_DB_ID_MAP[doc.__$id];
    if (dbid) {
      this.db.update({_id: dbid}, doc, {}, function(err) {
        callback(err);
      });
    } else {
      this.db.insert(doc, (err, newDoc) => {
        if (err) {
          return callback(err);
        }

        this.RES_TO_DB_ID_MAP[doc.__$id] = newDoc._id;
        callback();
      });
    }
  }

  public query(qry: any, callback: Callback) {
    let find = createDbFind(qry.find);
    let limit = qry.limit;
    let skip = qry.skip;
    let sort = qry.sort;

    let cur = this.db.find(find);

    if (sort) {
      cur = cur.sort(sort);
    }
    if (skip) {
      cur = cur.skip(skip);
    }
    if (limit) {
      // We go + 1 so we can tell if there are any more results
      cur = cur.limit(limit + 1);
    }

    cur.exec(function(err, docs: any[]) {
      // Now go from our docs to the ids
      let ids = docs.map(function(doc: any) {
        return doc.__$id as string;
      });
      callback(err, ids);
    });
  }
}

// Convert any _id searches to __id (which is where our id moved to)
function _createDbFind(qry: any) {
  if (Array.isArray(qry)) {
    qry.forEach(function(val) {
      _createDbFind(val);
    });
  } else if (isObject(qry)) {
    for (let key of Object.keys(qry)) {
      let val = qry[key];

      // Convert the _id to __id searches
      if (key === '_id') {
        qry.__id = val;
        delete qry._id;
      }

      _createDbFind(val);
    }
  }
}

function createDbFind(qry: any) {
  // Converts the query into the form required for a db search. First clone the object
  qry = cloneDeep(qry);
  _createDbFind(qry);
  return qry;
}

function createCallback(id: string) {
  return function callback(err?: Error, resp?: any) {
    me.postMessage({
      id,
      data: resp,
      error: err && err.message,
    });
  };
}

function getDatabase(dbid: string) {
  let db = _DB_MAP[dbid];

  if (!db) {
    db = new Database();
    _DB_MAP[dbid] = db;
  }

  return db;
}

function handleMessage(event: MessageEvent) {
  let data = event.data;
  let dbid = data.dbid;
  let id = data.id;
  let args = data.args;
  let cb = createCallback(id);
  let db = getDatabase(dbid);

  switch (data.fnName) {
    case 'update':
      db.update(args[0], cb);
      break;
    case 'remove':
      db.remove(args[0], cb);
      break;
    case 'query':
      db.query(args[0], cb);
      break;
    default:
      cb(new Error(`No such method ${data.fnName}`));
      break;
  }
}
