'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _uniq = require('lodash/uniq');

var _uniq2 = _interopRequireDefault(_uniq);

var _xor = require('lodash/xor');

var _xor2 = _interopRequireDefault(_xor);

var _immutable = require('immutable');

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }

function _toArray(arr) { return Array.isArray(arr) ? arr : Array.from(arr); }

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

/**
 * Snapshot, with a state-like shape.
 */

var Snapshot = new _immutable.Record({
  document: null,
  selection: null,
  steps: new _immutable.List()
});

/**
 * Step.
 */

var Step = new _immutable.Record({
  type: null,
  args: null
});

/**
 * Document transforms.
 */

var DOCUMENT_TRANSFORMS = ['deleteAtRange', 'deleteBackwardAtRange', 'deleteForwardAtRange', 'insertFragmentAtRange', 'insertTextAtRange', 'markAtRange', 'setBlockAtRange', 'setInlineAtRange', 'splitBlockAtRange', 'splitInlineAtRange', 'unmarkAtRange', 'unwrapBlockAtRange', 'unwrapInlineAtRange', 'wrapBlockAtRange', 'wrapInlineAtRange'];

/**
 * Selection transforms.
 */

var SELECTION_TRANSFORMS = ['blur', 'collapseToAnchor', 'collapseToEnd', 'collapseToEndOf', 'collapseToFocus', 'collapseToStart', 'collapseToStartOf', 'extendBackward', 'extendForward', 'extendToEndOf', 'extendToStartOf', 'focus', 'moveBackward', 'moveForward', 'moveToOffsets', 'moveToRangeOf'];

/**
 * State transforms, that act on both the document and selection.
 */

var STATE_TRANSFORMS = ['collapseToEndOfNextBlock', 'collapseToEndOfNextText', 'collapseToEndOfPreviousBlock', 'collapseToEndOfPreviousText', 'collapseToStartOfNextBlock', 'collapseToStartOfNextText', 'collapseToStartOfPreviousBlock', 'collapseToStartOfPreviousText', 'delete', 'deleteBackward', 'deleteForward', 'insertFragment', 'insertText', 'mark', 'moveTo', 'setBlock', 'setInline', 'splitBlock', 'splitInline', 'unmark', 'unwrapBlock', 'unwrapInline', 'wrapBlock', 'wrapInline'];

/**
 * All transforms.
 */

var TRANSFORMS = [].concat(DOCUMENT_TRANSFORMS).concat(SELECTION_TRANSFORMS).concat(STATE_TRANSFORMS);

/**
 * Defaults.
 */

var DEFAULT_PROPERTIES = {
  state: null,
  steps: new _immutable.List()
};

/**
 * Transform.
 */

var Transform = function (_ref) {
  _inherits(Transform, _ref);

  function Transform() {
    _classCallCheck(this, Transform);

    return _possibleConstructorReturn(this, Object.getPrototypeOf(Transform).apply(this, arguments));
  }

  _createClass(Transform, [{
    key: 'snapshot',


    /**
     * Create a history-ready snapshot of the current state.
     *
     * @return {Snapshot} snapshot
     */

    value: function snapshot() {
      var state = this.state;
      var steps = this.steps;
      var document = state.document;
      var selection = state.selection;

      return new Snapshot({ document: document, selection: selection, steps: steps });
    }

    /**
     * Apply the transform and return the new state.
     *
     * @param {Object} options
     *   @property {Boolean} isNative
     * @return {State} state
     */

  }, {
    key: 'apply',
    value: function apply() {
      var _this2 = this;

      var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];

      var transform = this;
      var state = transform.state;
      var steps = transform.steps;
      var _state = state;
      var history = _state.history;
      var _history = history;
      var undos = _history.undos;
      var redos = _history.redos;

      // Determine whether we need to create a new snapshot.

      var shouldSnapshot = false;
      var previous = undos.peek();

      // If there isn't a previous state, snapshot.
      if (!previous) shouldSnapshot = true;

      // If there is a previous state but the steps are different, snapshot.
      if (!shouldSnapshot && previous) {
        var types = steps.map(function (step) {
          return step.type;
        });
        var prevTypes = previous.steps.map(function (step) {
          return step.type;
        });
        var diff = (0, _xor2.default)(types.toArray(), prevTypes.toArray());
        if (diff.length) shouldSnapshot = true;
      }

      // If the current steps aren't one of the "combinale" types, snapshot.
      if (!shouldSnapshot) {
        var allCombinable = steps.every(function (step) {
          return step.type == 'insertText';
        }) || steps.every(function (step) {
          return step.type == 'deleteForward';
        }) || steps.every(function (step) {
          return step.type == 'deleteBackward';
        });
        if (!allCombinable) shouldSnapshot = true;
      }

      // If we should, save a snapshot into the history before transforming.
      if (shouldSnapshot) {
        var snapshot = transform.snapshot();
        undos = undos.push(snapshot);
        if (undos.size > 100) undos = undos.take(100);
        redos = redos.clear();
        history = history.merge({ undos: undos, redos: redos });
        state = state.merge({ history: history });
      }

      // Apply each of the steps in the transform, arriving at a new state.
      state = steps.reduce(function (memo, step) {
        return _this2.applyStep(memo, step);
      }, state);

      // Apply the "isNative" flag, which is used to allow for natively-handled
      // content changes to skip rerendering the editor for performance.
      state = state.merge({
        isNative: !!options.isNative
      });

      return state;
    }

    /**
     * Apply a single `step` to a `state`, differentiating between types.
     *
     * @param {State} state
     * @param {Step} step
     * @return {State} state
     */

  }, {
    key: 'applyStep',
    value: function applyStep(state, step) {
      var type = step.type;
      var args = step.args;


      if (DOCUMENT_TRANSFORMS.includes(type)) {
        var _document;

        var _state2 = state;
        var document = _state2.document;
        var selection = _state2.selection;

        var _args = _toArray(args);

        var range = _args[0];

        var rest = _args.slice(1);

        range = range.normalize(document);
        document = (_document = document)[type].apply(_document, [range].concat(_toConsumableArray(rest)));
        selection = selection.normalize(document);
        state = state.merge({ document: document, selection: selection });
        return state;
      } else if (SELECTION_TRANSFORMS.includes(type)) {
        var _selection2;

        var _state3 = state;
        var _document2 = _state3.document;
        var _selection = _state3.selection;

        _selection = (_selection2 = _selection)[type].apply(_selection2, _toConsumableArray(args));
        _selection = _selection.normalize(_document2);
        state = state.merge({ selection: _selection });
        return state;
      } else if (STATE_TRANSFORMS.includes(type)) {
        var _state4;

        state = (_state4 = state)[type].apply(_state4, _toConsumableArray(args));
        return state;
      }
    }

    /**
     * Undo to the previous state in the history.
     *
     * @return {State} state
     */

  }, {
    key: 'undo',
    value: function undo() {
      var transform = this;
      var state = transform.state;
      var _state5 = state;
      var history = _state5.history;
      var _history2 = history;
      var undos = _history2.undos;
      var redos = _history2.redos;

      // If there's no previous snapshot, return the current state.

      var previous = undos.peek();
      if (!previous) return state;

      // Remove the previous snapshot from the undo stack.
      undos = undos.pop();

      // Snapshot the current state, and move it into the redos stack.
      var snapshot = transform.snapshot();
      redos = redos.push(snapshot);

      // Return the previous state, with the updated history.
      var document = previous.document;
      var selection = previous.selection;

      history = history.merge({ undos: undos, redos: redos });
      state = state.merge({ document: document, selection: selection, history: history });
      return state;
    }

    /**
     * Redo to the next state in the history.
     *
     * @return {State} state
     */

  }, {
    key: 'redo',
    value: function redo() {
      var transform = this;
      var state = transform.state;
      var _state6 = state;
      var history = _state6.history;
      var _history3 = history;
      var undos = _history3.undos;
      var redos = _history3.redos;

      // If there's no next snapshot, return the current state.

      var next = redos.peek();
      if (!next) return state;

      // Remove the next history from the redo stack.
      redos = redos.pop();

      // Snapshot the current state, and move it into the undos stack.
      var snapshot = transform.snapshot();
      undos = undos.push(snapshot);

      // Return the next state, with the updated history.
      var document = next.document;
      var selection = next.selection;

      history = history.merge({ undos: undos, redos: redos });
      state = state.merge({ document: document, selection: selection, history: history });
      return state;
    }
  }]);

  return Transform;
}(new _immutable.Record(DEFAULT_PROPERTIES));

/**
 * Add a step-creating method for each of the transforms.
 */

TRANSFORMS.forEach(function (type) {
  Transform.prototype[type] = function () {
    for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
      args[_key] = arguments[_key];
    }

    var transform = this;
    var _transform = transform;
    var steps = _transform.steps;

    steps = steps.push(new Step({ type: type, args: args }));
    transform = transform.merge({ steps: steps });
    return transform;
  };
});

/**
 * Export.
 */

exports.default = Transform;