Show:
import { Promise, all } from 'rsvp';
import Evented from '@ember/object/evented';
import EmberObject, { computed } from '@ember/object';
import { merge } from '@ember/polyfills';
import { isArray } from '@ember/array';
import { isNone } from '@ember/utils';
import DS from 'ember-data';
import createProj from '../utils/create';
import Copyable from '../mixins/copyable';
import generateUniqueId from '../utils/generate-unique-id';

/**
  Base model that supports projections and copying.

  @module ember-flexberry-data
  @class ModelWithoutValidation
  @namespace Projection
  @extends DS.Model
  @uses Ember.EventedMixin
  @uses CopyableMixin

  @event preSave
  @param {Object} event Event object
  @param {Promise[]} promises Array to which custom 'preSave' promises could be pushed

  @public
 */
let ModelWithoutValidation = DS.Model.extend(Evented, Copyable, {
  /**
    Stored canonical `belongsTo` relationships.

    @property _canonicalBelongsTo
    @type Object
    @private
  */
  _canonicalBelongsTo: computed(() => ({})),

  /**
    Flag that indicates sync up process of model is processing.

    @property isSyncingUp
    @type Boolean
    @default false
  */
  isSyncingUp: false,

  /**
    Flag that indicates model is created during sync up process.

    @property isCreatedDuringSyncUp
    @type Boolean
    @default false
  */
  isCreatedDuringSyncUp: false,

  /**
    Flag that indicates model is updated last time during sync up process.

    @property isCreatedDuringSyncUp
    @type Boolean
    @default false
  */
  isUpdatedDuringSyncUp: false,

  /**
    Flag that indicates model is destroyed during sync up process.

    @property isCreatedDuringSyncUp
    @type Boolean
    @default false
  */
  isDestroyedDuringSyncUp: false,

  /**
    Triggers model's 'preSave' event & allows to execute some additional async logic before model will be saved.

    @method beforeSave

    @param {Object} [options] Method options
    @param {Boolean} [options.softSave = false] Flag: indicates whether following 'save' will be soft
    (without sending a request to server) or not
    @param {Promise[]} [options.promises] Array to which 'preSave' event handlers could add some asynchronous operations promises
    @return {Promise} A promise that will be resolved after all 'preSave' event handlers promises will be resolved
  */
  beforeSave(options) {
    options = merge({ softSave: false, promises: [] }, options || {});

    return new Promise((resolve, reject) => {
      // Trigger 'preSave' event, and  give its handlers possibility to run some 'preSave' asynchronous logic,
      // by adding it's promises to options.promises array.
      this.trigger('preSave', options);

      // Promises array could be totally changed in 'preSave' event handlers, we should prevent possible errors.
      options.promises = isArray(options.promises) ? options.promises : [];
      options.promises = options.promises.filter(function(item) {
        return item instanceof Promise;
      });

      all(options.promises).then(values => {
        resolve(values);
      }).catch(reason => {
        reject(reason);
      });
    });
  },

  /**
    Triggers 'preSave' event, and finally saves model.

    @method save

    @param {Object} [options] Method options
    @param {Boolean} [options.softSave = false] Flag: indicates whether following 'save' will be soft
    (without sending a request to server) or not
    @return {Promise} A promise that will be resolved after model will be successfully saved
  */
  save(options) {
    options = merge({ softSave: false }, options || {});
    this.preSaveSetId();

    return new Promise((resolve, reject) => {
      this.beforeSave(options).then(() => {
        // Call to base class 'save' method with right context.
        // The problem is that call to current save method will be already finished,
        // and traditional _this._super will point to something else, but not to DS.Model 'save' method,
        // so there is no other way, except to call it through the base class prototype.
        if (!options.softSave) {
          return DS.Model.prototype.save.call(this, options);
        }
      }).then(value => {
        // Assuming that record is not updated during sync up;
        this.set('isUpdatedDuringSyncUp', false);

        // All 'preSave' event promises has been successfully resolved,
        // finally model has been successfully saved,
        // so we can resolve 'save' promise.
        resolve(value);
      }).catch(reason => {
        // Any of 'beforeSave' or 'save' promises has been rejected,
        // so we should reject 'save' promise.
        reject(reason);
      });
    });
  },

  /**
    Sets Id on preSave.

    @method preSaveSetId
  */
  preSaveSetId() {
    if (isNone(this.get('id'))) {
      this.set('id', generateUniqueId());
    }
  },

  /**
    Turns model into 'updated.uncommitted' state.

    Transition into the `updated.uncommitted` state
    if the model in the `saved` state (no local changes).
    Alternative: this.get('currentState').becomeDirty();

    @method makeDirty
  */
  makeDirty() {
    this.send('becomeDirty');
  },

  /**
    Return object with changes.

    Object will have structure:
    * key - is name relationships that has changed
      * array - include two array, array with index `0` this old values, array with index `1` this new values.


    @example
      ```javascript
      {
        key: [
          [oldValues],
          [newValues],
        ],
      }
      ```

    @method changedHasMany
    @return {Object} Object with changes, empty object if no change.
  */
  changedHasMany() {
    let changedHasMany = {};
    this.eachRelationship((key, { kind }) => {
      if (kind === 'hasMany') {
        if (this.get(key).filterBy('hasDirtyAttributes', true).length) {
          changedHasMany[key] = [
            this.get(`${key}.canonicalState`).map(internalModel => internalModel ? internalModel.getRecord() : undefined),
            this.get(`${key}.currentState`).map(internalModel => internalModel ? internalModel.getRecord() : undefined),
          ];
        }
      }
    });
    return changedHasMany;
  },

    /**
    Сheck whether there is a changed `hasMany` relationships.

    @method hasChangedHasMany
    @return {Boolean} Returns `true` if `hasMany` relationships are changed.
  */
  hasChangedHasMany() {
    const changedHasMany = this.changedHasMany();

    return Object.keys(changedHasMany).length !== 0;
  },

  /**
    Rollback changes for `hasMany` relationships.

    @method rollbackHasMany
    @param {String} [forOnlyKey] If specified, it is rollback invoked for relationship with this key.
  */
  rollbackHasMany(forOnlyKey) {
    this.eachRelationship((key, { kind }) => {
      if (kind === 'hasMany' && (!forOnlyKey || forOnlyKey === key)) {
        if (this.get(key).filterBy('hasDirtyAttributes', true).length) {
          [this.get(`${key}.canonicalState`), this.get(`${key}.currentState`)].forEach((state, i) => {
            let records = state.map(internalModel => internalModel.getRecord());
            records.forEach((record) => {
              record.rollbackAll();
            });
            if (i === 0) {
              this.set(key, records);
            }
          });
        }
      }
    });
  },

  /**
    Сheck whether there is a changed `belongsTo` relationships.

    @method hasChangedBelongsTo
    @return {Boolean} Returns `true` if `belongsTo` relationships have changed, else `false`.
  */
  hasChangedBelongsTo() {
    let hasChangedBelongsTo = false;
    let changedBelongsTo = this.changedBelongsTo();
    for (let changes in changedBelongsTo) {
      if (changedBelongsTo.hasOwnProperty(changes)) {
        let [oldValue, newValue] = changedBelongsTo[changes];
        let oldValueId = oldValue ? oldValue.get('id') : null;
        let newValueId = newValue ? newValue.get('id') : null;
        if (oldValue !== newValue || oldValueId !== newValueId) {
          hasChangedBelongsTo = true;
          break;
        }
      }
    }

    return hasChangedBelongsTo;
  },

  /**
    Return object with changes.

    Object will have structure:
    * key - is name relationships that has changed
      * array - include two items, old value, with index `0`, and new value, with index `1`.

    @example
      ```javascript
      {
        key: [oldValue, newValue],
      }
      ```

    @method changedBelongsTo
    @return {Object} Object with changes, empty object if no change.
  */
  changedBelongsTo() {
    let changedBelongsTo = {};
    this.eachRelationship((key, { kind }) => {
      if (kind === 'belongsTo') {
        let current = this.get(key);
        let canonical = this.get(`_canonicalBelongsTo.${key}`) || null;
        if (current !== canonical) {
          changedBelongsTo[key] = [canonical, current];
        }
      }
    });
    return changedBelongsTo;
  },

  /**
    Rollback changes for `belongsTo` relationships.

    @method rollbackBelongsTo
    @param {String} [forOnlyKey] If specified, it is rollback invoked for relationship with this key.
  */
  rollbackBelongsTo(forOnlyKey) {
    this.eachRelationship((key, { kind, options }) => {
      if (kind === 'belongsTo' && (!forOnlyKey || forOnlyKey === key)) {
        let current = this.get(key);
        let canonical = this.get(`_canonicalBelongsTo.${key}`) || null;
        if (current !== canonical) {
          if (options.inverse && options.inverse !== key) {
            if (current && current.rollbackBelongsTo) {
              current.rollbackBelongsTo(options.inverse);
            }

            if (canonical && canonical.rollbackBelongsTo) {
              canonical.rollbackBelongsTo(options.inverse);
            }
          }

          this.set(key, canonical);
        }
      }
    });
  },

  /**
    Rollback changes for all relationships.

    @method rollbackRelationships
  */
  rollbackRelationships() {
    this.rollbackBelongsTo();
    this.rollbackHasMany();
  },

  /**
    Rollback all changes.

    @method rollbackAll
  */
  rollbackAll() {
    this.rollbackRelationships();
    this.rollbackAttributes();
  },

  /**
    Fired when the record is loaded from the server.
    [More info](http://emberjs.com/api/data/classes/DS.Model.html#event_didLoad).

    @method didLoad
  */
  didLoad() {
    this._super(...arguments);
    this._saveCanonicalBelongsTo();
  },

  /**
    Fired when the record is updated.
    [More info](http://emberjs.com/api/data/classes/DS.Model.html#event_didUpdate).

    @method didUpdate
  */
  didUpdate() {
    this._super(...arguments);
    this._saveCanonicalBelongsTo();
  },

  /**
    Fired when the record is created.
    [More info](http://emberjs.com/api/data/classes/DS.Model.html#event_didCreate).

    @method didCreate
  */
  didCreate() {
    this._super(...arguments);
    this._saveCanonicalBelongsTo();
  },

  /**
    Set each `belongsTo` relationship, observer, that save canonical state.

    @method _saveCanonicalBelongsTo
    @private
  */
  _saveCanonicalBelongsTo() {
    let _this = this;
    _this.eachRelationship((key, { kind, options }) => {
      if (kind === 'belongsTo') {
        if (options.async === false) {
          let belongsToValue = _this.get(key);
          if (belongsToValue || _this.get('_canonicalBelongsTo')[key]) {
            _this.get('_canonicalBelongsTo')[key] = belongsToValue;
          } else {
            _this.addObserver(key, _this, _this._saveBelongsToObserver);
          }
        } else {
          _this.get(key).then((record) => {
            _this.get('_canonicalBelongsTo')[key] = record;
          });
        }
      }
    });
  },

  /**
    Save canonical state for `belongsTo` relationships.

    @method _saveBelongsToObserver
    @private

    @param {DS.Model} sender
    @param {String} key
  */
  _saveBelongsToObserver(sender, key) {
    sender.get('_canonicalBelongsTo')[key] = sender.get(key);
    sender.removeObserver(key, this, this._saveBelongsToObserver);
  },
});

ModelWithoutValidation.reopenClass({
  /**
   * Defined projections for current model type.
   *
   * @property projections
   * @type Ember.Object
   * @default null
   * @public
   * @static
   */
  projections: null,

  /**
    Flag that indicates model id type.

    @property isType
    @type string
    @default 'guid'
  */
  idType: 'guid',

  /**
    The namespace in which this model is defined.

    @property namespace
    @type string
    @default ''
    @static
  */
  namespace: '',

  /**
   * Defines idType for specified model type.
   *
   * @method defineIdType
   * @param {String} newIdType Model id type.
   * @public
   * @static
   */
  defineIdType: function (newIdType) {
    this.reopenClass({
      idType: newIdType,
    });
  },

  /**
   * Defines projection for specified model type.
   *
   * @method defineProjection
   * @param {String} projectionName Projection name, eg 'EmployeeE'.
   * @param {String} modelName The name of the model type.
   * @param {Object} attributes Projection attributes.
   * @return {Object} Created projection.
   * @public
   * @static
   */
  defineProjection: function (projectionName, modelName, attributes) {
    let proj = createProj(modelName, attributes, projectionName);

    if (!this.projections) {
      this.reopenClass({
        projections: EmberObject.create({ modelName }),
      });
    } else if (this.projections.get('modelName') !== modelName) {
      let baseProjections = merge({}, this.projections);
      this.reopenClass({
        projections: EmberObject.create(merge(baseProjections, { modelName })),
      });
    }

    this.projections.set(projectionName, proj);
    return proj;
  },

  /**
   * Parent model type name.
   *
   * @property _parentModelName
   * @type String
   * @default null
   * @private
   * @static
   */
  _parentModelName: null
});

export default ModelWithoutValidation;