'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; }; }(); /**
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      * The MIT License (MIT)
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      * Copyright (c) 2015-present Dmitry Soshnikov <dmitry.soshnikov@gmail.com>
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      */

/**
 * An abstraction for an items set (kernel plus added),
 * known as a "closure". Recursively closes over
 * all added items, eventually forming an LR-parsing state.
 *
 * Usually there is one kernel item in a state, however there are
 * cases when kernel may contain several items. E.g. being in the state:
 *
 * S' -> • S
 * S  -> • S "a"
 *     | • "b"
 *
 * and having a transition on S, we get both first two items in the
 * kernel of the next state:
 *
 * S' -> S •
 * S  -> S • "a"
 */

var _lrItem = require('./lr-item');

var _lrItem2 = _interopRequireDefault(_lrItem);

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

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

var State = function () {
  /**
   * A closure state may have several kernel items. An initial kernel
   * item can be passed in the constructor, other kernel items can
   * be added later via `add` method.
   */
  function State(kernelItems, grammar, canonicalCollection) {
    _classCallCheck(this, State);

    this._kernelItems = kernelItems;
    this._items = [];
    this._grammar = grammar;
    this._canonicalCollection = canonicalCollection;
    this._number = null;

    // A map from transition symbol to the next state.
    this._transitionsForSymbol = null;

    // To avoid infinite recursion in case if an added item
    // is for a recursive production, S -> S "a".
    this._itemsMap = {};

    // Also items map, but by LR(0) key.
    this._lr0ItemsMap = {};

    // Add initial items, and closure them if needed.
    this.addItems(this._kernelItems);

    // And register the state in the collection.
    this._canonicalCollection.registerState(this);
  }

  /**
   * State number in the canonical collection.
   */


  _createClass(State, [{
    key: 'getNumber',
    value: function getNumber() {
      return this._number;
    }

    /**
     * Canonical collection can assign a specific
     * number to this state.
     */

  }, {
    key: 'setNumber',
    value: function setNumber(number) {
      this._number = number;
    }

    /**
     * Kernel items for which the closure is built.
     */

  }, {
    key: 'getKernelItems',
    value: function getKernelItems() {
      return this._kernelItems;
    }

    /**
     * All items in this closure (kernel plus all expanded).
     */

  }, {
    key: 'getItems',
    value: function getItems() {
      return this._items;
    }

    /**
     * Whether this state is final.
     */

  }, {
    key: 'isFinal',
    value: function isFinal() {
      return this._items.length === 1 && this._items[0].isFinal();
    }

    /**
     * Whether the state is accepting.
     */

  }, {
    key: 'isAccept',
    value: function isAccept() {
      return this.isFinal() && this._items[0].getProduction().isAugmented();
    }

    /**
     * Returns all reduce items in this set.
     */

  }, {
    key: 'getReduceItems',
    value: function getReduceItems() {
      if (!this._reduceItems) {
        this._reduceItems = this._items.filter(function (item) {
          return item.isReduce();
        });
      }
      return this._reduceItems;
    }

    /**
     * Calculates all the conflicts in the state, marking the
     * state as inadequate in case of "s/r" or "r/r" conflicts.
     *
     * Note: conflicts only possible in states that contain
     * reduce items, plus another reduce or shift item.
     */

  }, {
    key: 'analyzeConflicts',
    value: function analyzeConflicts() {
      var _this = this;

      this._conflicts = { sr: [], rr: [] };

      var reduceItems = this.getReduceItems();

      // "reduce-reduce" conflicts.
      if (reduceItems.length > 1) {
        var _iteratorNormalCompletion = true;
        var _didIteratorError = false;
        var _iteratorError = undefined;

        try {
          reduceSet: for (var _iterator = reduceItems[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
            var reduceItem = _step.value;
            var _iteratorNormalCompletion2 = true;
            var _didIteratorError2 = false;
            var _iteratorError2 = undefined;

            try {
              for (var _iterator2 = reduceItems[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
                var reduceItemToCheck = _step2.value;

                if (reduceItem !== reduceItemToCheck && reduceItem.conflictsWithReduceItem(reduceItemToCheck)) {
                  this._conflicts.rr = reduceItems;
                  break reduceSet;
                }
              }
            } catch (err) {
              _didIteratorError2 = true;
              _iteratorError2 = err;
            } finally {
              try {
                if (!_iteratorNormalCompletion2 && _iterator2.return) {
                  _iterator2.return();
                }
              } finally {
                if (_didIteratorError2) {
                  throw _iteratorError2;
                }
              }
            }
          }
        } catch (err) {
          _didIteratorError = true;
          _iteratorError = err;
        } finally {
          try {
            if (!_iteratorNormalCompletion && _iterator.return) {
              _iterator.return();
            }
          } finally {
            if (_didIteratorError) {
              throw _iteratorError;
            }
          }
        }
      }

      // "shift-reduce" conflicts.
      this._items.forEach(function (item) {
        if (!item.isShift()) {
          return;
        }
        reduceItems.forEach(function (reduceItem) {
          if (reduceItem.conflictsWithShiftSymbol(item.getCurrentSymbol())) {
            _this._conflicts.sr.push([item, reduceItem]);
          }
        });
      });

      return this._conflicts;
    }

    /**
     * Gets conflicts of this set.
     */

  }, {
    key: 'getConflicts',
    value: function getConflicts() {
      if (!this._conflicts) {
        this._conflicts = this.analyzeConflicts();
      }
      return this._conflicts;
    }

    /**
     * Gets "reduce-reduce" conflicts.
     */

  }, {
    key: 'getRRConflicts',
    value: function getRRConflicts() {
      return this.getConflicts().rr;
    }

    /**
     * Gets "shift-reduce" conflicts.
     */

  }, {
    key: 'getSRConflicts',
    value: function getSRConflicts() {
      return this.getConflicts().sr;
    }

    /**
     * Whether the state is inadequate, i.e. has conflicts.
     */

  }, {
    key: 'isInadequate',
    value: function isInadequate() {
      return this.getConflicts().sr.length > 0 || this.getConflicts().rr.length > 0;
    }
  }, {
    key: 'hasTransitionOnSymbol',
    value: function hasTransitionOnSymbol(symbol) {
      return this._transitionsForSymbol.hasOwnProperty(symbol);
    }
  }, {
    key: 'getTransitionOnSymbol',
    value: function getTransitionOnSymbol(symbol) {
      if (!this.hasTransitionOnSymbol(symbol)) {
        return null;
      }
      return this._transitionsForSymbol[symbol];
    }
  }, {
    key: 'setSymbolTransition',
    value: function setSymbolTransition(item, state) {
      var symbol = item.getCurrentSymbol().getSymbol();

      if (!this.hasTransitionOnSymbol(symbol)) {
        this._transitionsForSymbol[symbol] = {
          items: [],
          itemsMap: {},
          state: state
        };
      }

      var transitionsForSymbol = this._transitionsForSymbol[symbol];

      if (!transitionsForSymbol.itemsMap.hasOwnProperty(item.getKey())) {
        transitionsForSymbol.itemsMap[item.getKey()] = item;
        transitionsForSymbol.items.push(item);
      }

      if (state) {
        transitionsForSymbol.state = state;
        item.connect(state);
      }
    }
  }, {
    key: 'getItemTransion',
    value: function getItemTransion(itemKey) {
      return this.getItemTransionInfo(itemKey).state;
    }
  }, {
    key: 'getItemTransionInfo',
    value: function getItemTransionInfo(itemKey) {
      if (!this._itemsMap.hasOwnProperty(itemKey)) {
        throw new Error('Item ' + itemKey + ' is not in the state ' + this._number + '.');
      }

      var item = this._itemsMap[itemKey];

      if (item.isFinal()) {
        throw new Error('Item ' + itemKey + ' is final.');
      }

      return this.getTransitionOnSymbol(item.getCurrentSymbol().getSymbol());
    }

    /**
     * `Goto` operation from the items set. If the state has several items with
     * the same transition symbol, they all go to the same outer state. If
     * a transition for this kernel items set was already calculated in some
     * previous state, then a new state is not created, instead the items are
     * connected to the calculated state.
     */

  }, {
    key: 'goto',
    value: function goto() {
      if (this._visited) {
        return;
      }

      // Init the transition to null, it will be set
      // new state in the `goto` operation.
      if (!this._transitionsForSymbol) {
        this._transitionsForSymbol = {};
        for (var i = 0; i < this._items.length; i++) {
          var item = this._items[i];
          if (item.isFinal()) {
            continue;
          }
          this.setSymbolTransition(item, /* state */null);
        }
      }

      // Build the outer states if needed.
      for (var symbol in this._transitionsForSymbol) {
        var transitionsForSymbol = this.getTransitionOnSymbol(symbol);

        // Already calculated the outer state, exit.
        if (transitionsForSymbol.state) {
          continue;
        }

        var items = transitionsForSymbol.items;

        // See if we already calculated transition for this kernel set.
        var outerState = this._canonicalCollection.getTranstionForItems(items);

        // If not, create a new outer state with advanced kernel items.
        if (!outerState) {
          outerState = new State(
          /* kernelItems */items.map(function (item) {
            return item.advance();
          }),
          /* grammar */this._grammar,
          /* canonicalCollection */this._canonicalCollection);

          this._canonicalCollection.registerTranstionForItems(items, outerState);
        }

        // And connect our items to it.
        for (var _i = 0; _i < items.length; _i++) {
          this.setSymbolTransition(items[_i], /* state */outerState);
        }
      }

      this._visited = true;

      // Recursively goto further in the graph.
      for (var _symbol in this._transitionsForSymbol) {
        this.getTransitionOnSymbol(_symbol).state.goto();
      }
    }
  }, {
    key: 'isKernelItem',
    value: function isKernelItem(item) {
      return this._kernelItems.indexOf(item) !== -1;
    }

    /**
     * Adds items to this state, and closures them if needed.
     */

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

      items.map(function (item) {
        return _this2.addItem(item);
      }).forEach(function (item) {
        return item && item.closure();
      });
    }

    /**
     * Adds an item to this state.
     */

  }, {
    key: 'addItem',
    value: function addItem(item) {
      if (this._itemsMap.hasOwnProperty(item.getKey())) {
        return;
      }

      this._items.push(item);
      this._itemsMap[item.getKey()] = item;

      if (!this._lr0ItemsMap[item.getLR0Key()]) {
        this._lr0ItemsMap[item.getLR0Key()] = [];
      }
      this._lr0ItemsMap[item.getLR0Key()].push(item);

      item.setState(this);

      return item;
    }

    /**
     * Returns an item by key.
     */

  }, {
    key: 'getItemByKey',
    value: function getItemByKey(key) {
      return this._itemsMap[key];
    }

    /**
     * Returns an item by LR(0) key.
     */

  }, {
    key: 'getItemByLR0Key',
    value: function getItemByLR0Key(lr0Key) {
      var lr0ItemsCount = this._lr0ItemsMap[lr0Key].length;

      if (lr0ItemsCount !== 1) {
        throw new Error('Number of LR0 items for ' + lr0Key + ' is not 1 in ' + this.getNumber() + '. ' + 'Call mergeLR0Items before accessing this method.');
      }

      return this._lr0ItemsMap[lr0Key][lr0ItemsCount - 1];
    }

    /**
     * Merges items with the same LR(0) parts for LALR(1).
     */

  }, {
    key: 'mergeLR0Items',
    value: function mergeLR0Items() {
      for (var lr0Key in this._lr0ItemsMap) {
        var items = this._lr0ItemsMap[lr0Key];
        var rootItem = items[0];

        // Merge the items, keeping only one.
        while (items.length > 1) {
          this.mergeTwoItems(rootItem, items.pop());
        }
      }
    }
  }, {
    key: 'mergeTwoItems',
    value: function mergeTwoItems(first, second) {
      var transition = !first.isFinal() ? this.getItemTransionInfo(first.getKey()) : null;

      delete this._itemsMap[first.getKey()];

      if (transition) {
        delete transition.itemsMap[first.getKey()];
      }

      var secondKey = second.getKey();

      first.mergeLookaheadSet(second.getLookaheadSet());

      // And remove it from all collections.
      delete this._itemsMap[secondKey];

      var itemIndex = this._items.indexOf(second);
      if (itemIndex !== -1) {
        this._items.splice(itemIndex, 1);
      }

      itemIndex = this._kernelItems.indexOf(second);
      if (itemIndex !== -1) {
        this._kernelItems.splice(itemIndex, 1);
      }

      if (transition) {
        delete transition.itemsMap[secondKey];

        itemIndex = transition.items.indexOf(second);
        if (itemIndex !== -1) {
          transition.items.splice(itemIndex, 1);
        }
      }

      this._itemsMap[first.getKey()] = first;

      if (transition) {
        transition.itemsMap[first.getKey()] = first;
      }
    }

    /**
     * Merges the state with another one, that has the same
     * LR(0) items, but which differs only in lookaheads.
     * This is used in LALR(1) mode when is compressed from CLR(1).
     */

  }, {
    key: 'mergeWithState',
    value: function mergeWithState(state) {
      var _this3 = this;

      if (this._items.length !== this._items.length) {
        throw new Error('LALR(1): State ' + state.getNumber() + ' is not compatible ' + ('with state ' + this.getNumber() + '.'));
      }

      if (_lrItem2.default.keyForItems(this.getKernelItems()) !== _lrItem2.default.keyForItems(state.getKernelItems())) {
        this._items.forEach(function (item) {
          var thatItem = state.getItemByLR0Key(item.getLR0Key());

          if (!thatItem) {
            throw new Error('Item ' + item.toString() + ' presents in state ' + _this3.getNumber() + ', ' + ('but is absent in the ' + state.getNumber() + '.'));
          }

          _this3.mergeTwoItems(item, thatItem);
        });
      }

      // After merging lookaheads, we should un-register this new
      // idential state, since it was registered in the constructor.
      this._canonicalCollection.unregisterState(state);

      return this;
    }
  }]);

  return State;
}();

exports.default = State;