import Ember from 'ember';
import { task, all } from 'ember-concurrency';

const COPIES = "__session_copies__";
const COPY_TASK_RUNNER = "__session_copy_task_runner__";
const COPY_TASK = "__session_copy_task__";

const {
  assign,
  inject,
  Logger,
  guidFor,
  isEmpty,
  runInDebug,
  getOwner
} = Ember;

const {
  keys
} = Object;

const PRIMITIVE_TYPES = ['string', 'number', 'boolean'];

export default Ember.Object.extend({
  [COPIES]: Ember.computed(function() { return {}; }),
  _transforms: Ember.computed(function() { return {}; }),
  
  /**
   * Get the transform for a given type.
   *
   * @method getTransform
   * @private
   * @param  {DS.Model} model
   * @param  {String} type
   * @return {DS.Transform}
   */
  getTransform(model, type) {
    let transform = this.get(`_transforms.${type}`);
    if (!transform) {
      transform = getOwner(model).lookup(`transform:${type}`);
      this.set(`_transforms.${type}`, transform);
    }
    return transform;
  },
  
  /**
   * The copy task runner. Allows our copy task to have a drop
   * concurrency policy
   *
   * @type {Task}
   * @private
   */
  [COPY_TASK_RUNNER]: task(function *(model) {
    let isSuccessful = false;

    try {
      let copy = yield this.get(COPY_TASK).perform(model);
      isSuccessful = true;

      return copy;
    } catch (e) {
      runInDebug(() => Logger.error('[ember-data-session]', e));

      // Throw so the task promise will be rejected
      throw new Error(e);
    } finally {
      if (!isSuccessful) {
        let copiesKeys = this.rollback();

        // Display the error
        runInDebug(() => Logger.error(`[ember-data-session] Failed to copy model '${model}'. Cleaning up ${copiesKeys.length} created copies...`));
      }
    }
  }).drop(),

  /**
   * The copy task that gets called from `copy`. Does all the grunt work.
   *
   * NOTE: This task cannot have a concurrency policy since it breaks cyclical
   *       relationships.
   *
   * @type {Task}
   * @private
   */
  [COPY_TASK]: task(function *(model) {
    let { modelName } = model.constructor;
    let copies = this.get(COPIES);
    let store = getOwner(model).lookup('service:store');
    let guid = guidFor(model);
    let attrs = {};

    // Handle cyclic relationships: If the model has already been copied,
    // just return that model
    if (copies[guid]) {
      console.log('copy-guid', copies[guid] + '');
      return copies[guid];
    }

    let copy  = store.createRecord(modelName);	
    copies[guid] = copy;

    // Copy all the attributes
    model.eachAttribute((name, { type, options }) => {
      if (!isEmpty(type) && !PRIMITIVE_TYPES.includes(type)) {
        let value = model.get(name);
        let transform = getTransform(model, type);

        // Run the transform on the value. This should guarantee that we get
        // a new instance.
        value = transform.serialize(value, options);
        value = transform.deserialize(value, options);

        attrs[name] = value;
      } else {
        attrs[name] = model.get(name);
      }
    });

    let relationships = [];

    // Get all the relationship data
    model.eachRelationship((name, meta) => {
      relationships.push({ name, meta });
    });

    // Copy all the relationships
    for (let i = 0; i < relationships.length; i++) {
      let { name, meta } = relationships[i];
      let value = yield model.get(name);

      if (meta.kind === 'belongsTo') {
        if (value) {
          attrs[name] = yield this.get(COPY_TASK).perform(value);
        } else {
          attrs[name] = value;
        }
        
      } else if (meta.kind === 'hasMany') {
        let allRels = [];
        value.forEach((rel) => allRels.push(this.get(COPY_TASK).perform(rel)));
        attrs[name] = yield all(allRels);
      }
    }

    // Set the properties on the copied model
    copy.setProperties(attrs);    

    return copy;
  }),
  
  add(model) {
    // return copy instance
    return this.get(COPY_TASK_RUNNER).perform(model);
  },
  
  rollback() {
    let store = undefined;
    let copies = this.get(COPIES);
    let copiesKeys = keys(copies);

    // Unload all created records
    copiesKeys.forEach((key) => {
      let copy = copies[key];
      if (!store) {
        store = getOwner(copy).lookup('service:store');
      }
      store.unloadRecord(copy);
    });
    
    this.set(COPIES, {});
    return copiesKeys;
  },
  
  submit() {
    let copies = this.get(COPIES);
    let copiesKeys = keys(copies);
    let copiesPromisses = [];

    // save all records
    copiesKeys.forEach((key) => {
      copiesPromisses.push(copies[key].save());
    });
    
    return Ember.RSVP.all(copiesPromisses);
  }
});