// tslint:disable only-arrow-functions

import { EventEmitter } from './emitter';
import { JSONPatch, ModelArray, ServerModel, ServerResource as IServerResource, SocketFactory } from './types';

import { uniq } from 'lodash';
import { isFunction } from 'lodash';
import { isString } from 'lodash';
import { isUndefined } from 'lodash';

import * as utils from './utils';

export function createServerResourceFactory(socket: SocketFactory) {

  function ServerResourceFactory(url: string, rootKey: string, rootKeyPlural: string): IServerResource {
    let sock = socket(url, rootKey, rootKeyPlural);

    // Object holding the resources (keys are the object ids)
    let _resources = {};
    let _internalRes = {};

    function toObject() {
      return utils.toObject(this);
    }

    // Returns the object or a list of objects (depending on ids passed in)
    // The array or object will have the $promise attribute to use.
    function get(ids: (string[] | string), force?: boolean, transform?: Function) {

      if (isFunction(force)) {
        transform = force;
        force = false;
      }

      if (!transform) {
        transform = (val: any) => { return val; };
      }

      let singleId = false;
      let results: ModelArray<ServerModel> = [];

      function transformResults() {
        if (singleId) {
          return results[0];
        } else {
          return results;
        }
      }

      // If we've been passed
      if (isString(ids)) {
        singleId = true;
        ids = [ids];
      }

      // Make sure we have no repeated items in the array
      ids = uniq(ids);

      // Go and load in the results, compiling a list of resources we need to go and fetch
      let unknownIds: string[] = [];
      ids.forEach(function(id) {
        let res: ServerModel = _resources[id];
        if (res) {
          results.push(transform(res, id));

          // If we are forcing then we want to go and refetch assuming we've already resolved
          // If we haven't resolved then we are going to refresh the data anyway so don't do
          // anything here. If we aren't resolved and we aren't fetching then we want to
          // go and refresh the data because we could have been created from somewhere that
          // isn't the server
          if ((force && res.$resolved) || !(res.$resolved || res.$fetching)) {
            unknownIds.push(id);
            res.$fetching = true;

            // If we've already resolved then create a new promise object on the resource
            if (res.$resolved) {
              res.$deferred = new utils.Deferred<ServerModel>();
              res.$promise = this.$deferred.promise;
              res.$resolved = false;
            }
          }
        } else {
          // Haven't seen this id yet. Create a new resource and store it off
          res = _resources[id] = <ServerModel> (new (<any> ServerResource)());
          res.$fetching = true;
          results.push(transform(res, id));
          unknownIds.push(id);
        }
      });

      // Do we have any ids to fetch. If so go and get them
      if (unknownIds.length > 0) {
        let req = sock.get(unknownIds);
        req.then(function(response: any) {

          // We might not get a response (say if the app is offline). In this case
          // we just resolve everything as is.
          if (isUndefined(response)) {
            unknownIds.forEach(function(id) {
              let res = _resources[id];
              res.$fetching = false;

              if (!res.$resolved) {
                res.$resolved = true;
                res.$deferred.resolve(res);
              }
            });
          } else {
            response.forEach(function(resdata: any) {
              if (resdata._id) {
                let id = resdata._id;
                let res = _resources[id];
                res.$fetching = false;
                updateVal(res, resdata);

                // We've got the data for the first time - resolve the deferred
                if (!res.$resolved) {
                  res.$deferred.resolve(res);
                  res.$resolved = true;
                }
              }
            });

            // If any of our unknown ids hasn't been resolved then we assume its deleted...
            unknownIds.forEach(function(id) {
              let res = _resources[id];
              if (!res) {
                return;
              }

              res.$fetching = false;
              if (!res.$resolved) {
                res.$deleted = true;
                res.$emitter.emit('update', null, res.$toObject());
              }
            });
          }
        }, function(reason: any) {
          // Handle an error...
          // Clean up any of our unknown ids - we don't know about them
          unknownIds.forEach(function(id) {
            let res = _resources[id];
            res.$fetching = false;
            res.$deferred.reject(reason);
            delete _resources[id];
          });
        });
      }

      if (!singleId) {
        // Wait for all the promises to be resolved before resolving
        let promises: Array<Promise<any>> = [];
        results.forEach(function(res) {
          promises.push(res.$promise);
        });

        let tmpProm = Promise.all(promises);
        results.$promise = tmpProm.then(function() {
          // Return the results array as we resolve
          return results;
        });
      }

      return transformResults();
    }

    // Perform a save
    function save(this: ServerModel, patch: JSONPatch) {
      utils.applyPatch(this, patch);

      // We update if we have an _id - otherwise we create.
      if (this._id) {
        return updateResource(this, patch);
      } else {
        // This is an initial create
        return createResource(this);
      }
    }

    function updateResource(res: any, patch: JSONPatch) {
      let req = sock.patch(res._id, patch);
      return req.then(function(response: any) {
        updateVal(res, response);
        // Resolve the deferred
        _resources[res._id] = res;
        if (!res.$resolved) {
          res.$deferred.resolve(res);
          res.$resolved = true;
        }

        return res;

      }).catch(function(reason: any) {
        // Something went wrong - go and fetch the object again so we are up to date
        if (res._id) {
          res.$refresh();
        }

        throw reason;
      });
    }

    function createResource(res: any) {
      let data = res.$toObject();

      let req = sock.create(data);
      return req.then(function(response) {
        // There could be a race condition here where we could end up creating the id map
        // earlier (maybe...?)
        updateVal(res, response);
        _resources[res._id] = res;

        if (!res.$resolved) {
          res.$deferred.resolve(res);
          res.$resolved = true;
        }
      });
    }

    function remove() {

      let res = this;
      res.$deleted = true;

      let req = sock.remove(res._id);
      return req.then(function(response) {
        if (response) {
          delete _resources[res._id];
        } else {
          res.$deleted = false;
        }

        return response;
      });
    }

    // Updates the value from the server
    function updateVal(res: any, val: any) {

      // Do we have a null value (indicating the resource has been deleted?)
      if (val) {
        // Convert any JSON dates into dates
        val = utils.convertJsonDates(val);

        let oldData = res.$toObject();
        utils.removeResValues(res);
        utils.setResValues(res, val);

        // Make sure we have store this resource off
        _resources[res._id] = res;
        res.$emitter.emit('update', res.$toObject(), oldData);
      } else {
        // Deleted - clean me up scotty
        delete _resources[res._id];
        res.$emitter.emit('update', null, res.$toObject());
      }
    }

    function _updateVal(this: ServerModel, val: any) {
      return updateVal(this, val);
    }

    function refresh(this: ServerModel) {
      if (this._id) {
        get(this._id, true);
      }
    }

    // The main constructor for theServerResource class
    function ServerResourceConstructor(val?: any, id?: string): void {
      let res = this;
      this.$emitter = new EventEmitter();

      this.$deferred = new utils.Deferred();
      this.$promise = this.$deferred.promise; // An initial promise for our initial
                                              // fetch or create of data
      this.$resolved = false; // Have we had an initial resolution of the promise
      this.$deleted = false; // Has the resource been deleted
      this.$fetching = false; // Are we currently fetching data for this resource

      this.$id = id ? id : utils.uuid();

      _internalRes[this.$id] = this;

      if (val) {
        for (let key of Object.keys(val)) {
          res[key] = val[key];
        }
      }

      // If we have an id then add us to the store
      if (this._id) {
        _resources[this._id] = this;
      }
    }

    const ServerResource: IServerResource = ServerResourceConstructor;
    ServerResource.get = get;

    ServerResource.prototype.$save = save;
    ServerResource.prototype.$remove = remove;
    ServerResource.prototype.$delete = remove;
    ServerResource.prototype.$refresh = refresh;
    ServerResource.prototype.$toObject = toObject;
    ServerResource.prototype.$updateVal = _updateVal;

    return ServerResource;
  }

  return ServerResourceFactory;
}
