/**
 * @file
 * A Backbone Model for the state of an in-place editable field in the DOM.
 */

(function (_, Backbone, Drupal) {

  "use strict";

  /**
   * State of an in-place editable field in the DOM.
   */
  Drupal.quickedit.FieldModel = Drupal.quickedit.BaseModel.extend({

    defaults: {
      // The DOM element that represents this field. It may seem bizarre to have
      // a DOM element in a Backbone Model, but we need to be able to map fields
      // in the DOM to FieldModels in memory.
      el: null,
      // A field ID, of the form
      // "<entity type>/<id>/<field name>/<language>/<view mode>", e.g.
      // "node/1/field_tags/und/full".
      fieldID: null,
      // The unique ID of this field within its entity instance on the page, of
      // the form "<entity type>/<id>/<field name>/<language>/<view mode>[entity instance ID]",
      // e.g. "node/1/field_tags/und/full[0]".
      id: null,
      // A Drupal.quickedit.EntityModel. Its "fields" attribute, which is a
      // FieldCollection, is automatically updated to include this FieldModel.
      entity: null,
      // This field's metadata as returned by the QuickEditController::metadata().
      metadata: null,
      // Callback function for validating changes between states. Receives the
      // previous state, new state, context, and a callback
      acceptStateChange: null,
      // A logical field ID, of the form
      // "<entity type>/<id>/<field name>/<language>", i.e. the fieldID without
      // the view mode, to be able to identify other instances of the same field
      // on the page but rendered in a different view mode. e.g. "node/1/field_tags/und".
      logicalFieldID: null,

      // The attributes below are stateful. The ones above will never change
      // during the life of a FieldModel instance.

      // In-place editing state of this field. Defaults to the initial state.
      // Possible values: @see Drupal.quickedit.FieldModel.states.
      state: 'inactive',
      // The field is currently in the 'changed' state or one of the following
      // states in which the field is still changed.
      isChanged: false,
      // Is tracked by the EntityModel, is mirrored here solely for decorative
      // purposes: so that FieldDecorationView.renderChanged() can react to it.
      inTempStore: false,
      // The full HTML representation of this field (with the element that has
      // the data-quickedit-field-id as the outer element). Used to propagate
      // changes from this field instance to other instances of the same field.
      html: null,
      // An object containing the full HTML representations (values) of other view
      // modes (keys) of this field, for other instances of this field displayed
      // in a different view mode.
      htmlForOtherViewModes: null
    },

    /**
     * {@inheritdoc}
     */
    initialize: function (options) {
      // Store the original full HTML representation of this field.
      this.set('html', options.el.outerHTML);

      // Enlist field automatically in the associated entity's field collection.
      this.get('entity').get('fields').add(this);

      // Automatically generate the logical field ID.
      this.set('logicalFieldID', this.get('fieldID').split('/').slice(0, 4).join('/'));

      // Call Drupal.quickedit.BaseModel's initialize() method.
      Drupal.quickedit.BaseModel.prototype.initialize.call(this, options);
    },

    /**
     * {@inheritdoc}
     */
    destroy: function (options) {
      if (this.get('state') !== 'inactive') {
        throw new Error("FieldModel cannot be destroyed if it is not inactive state.");
      }
      Drupal.quickedit.BaseModel.prototype.destroy.call(this, options);
    },

    /**
     * {@inheritdoc}
     */
    sync: function () {
      // We don't use REST updates to sync.
      return;
    },

    /**
     * {@inheritdoc}
     */
    validate: function (attrs, options) {
      var current = this.get('state');
      var next = attrs.state;
      if (current !== next) {
        // Ensure it's a valid state.
        if (_.indexOf(this.constructor.states, next) === -1) {
          return '"' + next + '" is an invalid state';
        }
        // Check if the acceptStateChange callback accepts it.
        if (!this.get('acceptStateChange')(current, next, options, this)) {
          return 'state change not accepted';
        }
      }
    },

    /**
     * Extracts the entity ID from this field's ID.
     *
     * @return String
     *   An entity ID: a string of the format `<entity type>/<id>`.
     */
    getEntityID: function () {
      return this.get('fieldID').split('/').slice(0, 2).join('/');
    },

    /**
     * Extracts the view mode ID from this field's ID.
     *
     * @return String
     *   A view mode ID.
     */
    getViewMode: function () {
      return this.get('fieldID').split('/').pop();
    },

    /**
     * Find other instances of this field with different view modes.
     *
     * @return Array
     *   An array containing view mode IDs.
     */
    findOtherViewModes: function () {
      var currentField = this;
      var otherViewModes = [];
      Drupal.quickedit.collections.fields
        // Find all instances of fields that display the same logical field (same
        // entity, same field, just a different instance and maybe a different
        // view mode).
        .where({ logicalFieldID: currentField.get('logicalFieldID') })
        .forEach(function (field) {
          // Ignore the current field.
          if (field === currentField) {
            return;
          }
          // Also ignore other fields with the same view mode.
          else if (field.get('fieldID') === currentField.get('fieldID')) {
            return;
          }
          else {
            otherViewModes.push(field.getViewMode());
          }
        });
      return otherViewModes;
    }

  }, {

    /**
     * A list (sequence) of all possible states a field can be in during in-place
     * editing.
     */
    states: [
      // The field associated with this FieldModel is linked to an EntityModel;
      // the user can choose to start in-place editing that entity (and
      // consequently this field). No in-place editor (EditorView) is associated
      // with this field, because this field is not being in-place edited.
      // This is both the initial (not yet in-place editing) and the end state (
      // finished in-place editing).
      'inactive',
      // The user is in-place editing this entity, and this field is a candidate
      // for in-place editing. In-place editor should not
      // - Trigger: user.
      // - Guarantees: entity is ready, in-place editor (EditorView) is associated
      //   with the field.
      // - Expected behavior: visual indicators around the field indicate it is
      //   available for in-place editing, no in-place editor presented yet.
      'candidate',
      // User is highlighting this field.
      // - Trigger: user.
      // - Guarantees: see 'candidate'.
      // - Expected behavior: visual indicators to convey highlighting, in-place
      //   editing toolbar shows field's label.
      'highlighted',
      // User has activated the in-place editing of this field; in-place editor is
      // activating.
      // - Trigger: user.
      // - Guarantees: see 'candidate'.
      // - Expected behavior: loading indicator, in-place editor is loading remote
      //   data (e.g. retrieve form from back-end). Upon retrieval of remote data,
      //   the in-place editor transitions the field's state to 'active'.
      'activating',
      // In-place editor has finished loading remote data; ready for use.
      // - Trigger: in-place editor.
      // - Guarantees: see 'candidate'.
      // - Expected behavior: in-place editor for the field is ready for use.
      'active',
      // User has modified values in the in-place editor.
      // - Trigger: user.
      // - Guarantees: see 'candidate', plus in-place editor is ready for use.
      // - Expected behavior: visual indicator of change.
      'changed',
      // User is saving changed field data in in-place editor to TempStore. The
      // save mechanism of the in-place editor is called.
      // - Trigger: user.
      // - Guarantees: see 'candidate' and 'active'.
      // - Expected behavior: saving indicator, in-place editor is saving field
      //   data into TempStore. Upon successful saving (without validation
      //   errors), the in-place editor transitions the field's state to 'saved',
      //   but to 'invalid' upon failed saving (with validation errors).
      'saving',
      // In-place editor has successfully saved the changed field.
      // - Trigger: in-place editor.
      // - Guarantees: see 'candidate' and 'active'.
      // - Expected behavior: transition back to 'candidate' state because the
      //   deed is done. Then: 1) transition to 'inactive' to allow the field to
      //   be rerendered, 2) destroy the FieldModel (which also destroys attached
      //   views like the EditorView), 3) replace the existing field HTML with the
      //   existing HTML and 4) attach behaviors again so that the field becomes
      //   available again for in-place editing.
      'saved',
      // In-place editor has failed to saved the changed field: there were
      // validation errors.
      // - Trigger: in-place editor.
      // - Guarantees: see 'candidate' and 'active'.
      // - Expected behavior: remain in 'invalid' state, let the user make more
      //   changes so that he can save it again, without validation errors.
      'invalid'
    ],

    /**
     * Indicates whether the 'from' state comes before the 'to' state.
     *
     * @param String from
     *   One of Drupal.quickedit.FieldModel.states.
     * @param String to
     *   One of Drupal.quickedit.FieldModel.states.
     * @return Boolean
     */
    followsStateSequence: function (from, to) {
      return _.indexOf(this.states, from) < _.indexOf(this.states, to);
    }

  });

  Drupal.quickedit.FieldCollection = Backbone.Collection.extend({
    model: Drupal.quickedit.FieldModel
  });

}(_, Backbone, Drupal));
