/* globals _ */

import Mixin from 'ember-metal/mixin';
import Ember from 'ember';
import ExpiringDataMixin from 'web-client/mixins/expiring-data';
import computed from 'ember-computed';
import OwnerInjectionMixin from './core/mixins/static/owner-injection';
import RSVP from 'rsvp';

const { $, ArrayProxy, Object: EmberObject, assert, defineProperty } = Ember;

export default (function () {
  let RemoteResource = Mixin.create({
    init() {
      this._super();
      if (this.expiration) {
        this.setExpirationInterval(this.expiration);
      }
      this.empty();
    },

    load(returnPromise = false) {
      if (this.shouldLoad()) {
        this.empty();
        return this.loadMore(returnPromise);
      }

      return returnPromise ? RSVP.resolve(this) : this;
    },

    save(hash) {
      if (this.get('isSaving')) {
        return;
      }
      this.set('isSaving', true);
      return this.saveRemote(hash).then((data) => {
        _.each(data, (value, key) => {
          this.set(key, value);
          this.set(`rollbackData.${key}`, value);
        });
        this._setupDirtyTracking();
        return data;
      }).finally(() => {
        this.set('isSaving', false);
      });
    },

    /* Dynamically construct the hasDirtyAttributes computed property */
    _setupDirtyTracking() {
      let initialData = this.get('rollbackData');
      defineProperty(this, 'hasDirtyAttributes', computed('rollbackData', ...Object.keys(initialData), function () {
        let rollbackData = this.get('rollbackData');
        return Object.keys(rollbackData).some((key) => {
          return !_.isEqual(this.get(key), rollbackData[key]);
        });
      }));
    },

    del() {
      if (this.get('isDestroying')) {
        return;
      }
      return this.destroyRemote().then((data) => {
        this.destroy();
        return data;
      }, (error) => {
        throw error;
      });
    },

    // returnPromise forces loadMore to return a promise. This is handy because
    // routes are aware of promises and will pause while the model loads.
    loadMore(returnPromise = false) {
      if (this.get('isLoading')) {
        if (returnPromise) {
          return RSVP.resolve(this);
        }
        return this;
      }

      this.set('isLoading', true);
      this.set('error', null);

      let request = this.request();
      if (request) {
        let promise = request.then((response) => {
          if (this.isDestroyed) { return; }
          this.justGotData();
          if (this.afterSuccess) {
            this.afterSuccess(response);
          }
          this.set('isLoading', false);
          return this;
        }, (response) => {
          if (this.isDestroyed) { throw this; }
          this.set('error', response);
          if (this.afterFail) {
            this.afterFail(response);
          }
          this.set('isLoading', false);
          throw this;
        });

        if (returnPromise) {
          return promise;
        }
        promise.catch(e => {
          if (e.constructor === this.constructor) {
            /* Catch 404s that nobody else is responsible for catching */
            return;
          }
          throw e;
        });
      }

      return this;
    },

    setContent(items) {
      let self = this;

      let limit = this.get('limit');
      let startLimit = this.get('startLimit');
      let currentLength = this.get('content.length');
      let limitForCurrentRequest = !currentLength && startLimit ? startLimit : limit;

      this.set('hideMore', limitForCurrentRequest > items.length);
      this.set('gotNonEmptyResults', items && items.length);

      this.get('content').addObjects(_.map(items, function (item) {
        return self.model.create(item);
      }));
    }
  });

  const ErrorStatuses = Mixin.create({
    forbidden: computed('error.status', function() {
      return _.contains([401, 403], this.get('error.status'));
    }),

    notFound: computed('error.status', function() {
      return this.get('error.status') === 404;
    }),

    internalServerError: computed('error.status', function() {
      return this.get('error.status') === 500;
    })
  });

  const Model = EmberObject.extend(RemoteResource, ErrorStatuses, ExpiringDataMixin, {
    shouldLoad() {
      return !this.get('isLoaded') || this.isExpired();
    },

    empty() {
      this.set('content', undefined);
    },

    rollback() {
      this.setProperties(
        $.extend(true, {}, this.get('rollbackData')) // deep copy
      );
    }
  });

  const Collection = ArrayProxy.extend(RemoteResource, ErrorStatuses, ExpiringDataMixin, {
    shouldLoad() {
      return this.get('content.length') === 0 || this.isExpired();
    },

    empty() {
      this.set('content', []);
    }
  });

  let instanceMixin = function(options) {
    return {
      create(data, superOnly) {
        if (superOnly) {
          return this._super(this._ownerInjection(), data);
        }

        if (options.deserialize) {
          data = options.deserialize(data);
        }

        let instance;
        if (data.id && this.instances[data.id]) {
          // if we have this instance, pull it out of memory and update it
          instance = this.instances[data.id];
          this.instances[data.id].setProperties(data);
        } else {
          // if we don't have this instance, create a new model with data and remember it
          instance = this._super(this._ownerInjection(), data);
          assert(`Deserialized data must have an \`id\` field, data = ${JSON.stringify(data)}`, instance.id);
          this.instances[instance.id] = instance;
        }

        // set related objects
        instance.set('isLoaded', true);
        instance.set(
          'rollbackData',
          $.extend(true, {}, data) // deep copy
        );

        instance._setupDirtyTracking();
        _.each(options.relationships, function (relationFind, relationName) {
          instance.set(relationName, relationFind.call(instance));
        });

        return instance;
      },
      instances: {}
    };
  };

  let collectionMixin = {
    collectionId(type, options) {
      let stringify = function (obj) {
        // Simply calling JSON.stringify on the object is not deterministic due to key ordering.
        return JSON.stringify(_.sortBy(_.pairs(obj), function (o) { return o[0]; }));
      };
      return `__${type}_${stringify(options || {})}`;
    },

    findOne(id) {
      if (!this.instances[id]) {
        this.instances[id] = this.create({id: id}, true);
      }
      return this.instances[id];
    },

    destroy(id) {
      if (this.instances[id]) {
        delete this.instances[id];
      }
      this._super(...arguments);
    },

    reset() {
      this.collections = {};
      this.instances = {};
    },

    find(type, options) {
      let collectionId = this.collectionId(type, options);
      let resource = this.collections[collectionId];
      if (!resource) {
        resource = this.collections[collectionId] = this.__collections[type].create(this._ownerInjection(), options || {});
      }
      return resource;
    },

    defineCollection(type, options) {
      let model = this;
      this.collections = this.collections || {};
      this.__collections = this.__collections || {};
      this.__collections[type] = Collection.extend(OwnerInjectionMixin, _.defaults({
        model,
        type
      }, options));
    }
  };

  return {
    // defineModel matches the signature of Ember.Object.extend: ([mixin1, mixin2, ...,] options)
    defineModel(...args) {
      let options = args[args.length - 1];
      let model = Model.extend(...args);
      model.reopenClass(OwnerInjectionMixin);
      model.reopenClass(instanceMixin(options));
      model.reopenClass(collectionMixin);
      return model;
    }
  };
})();
