'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 _grammarSymbol = require('./grammar/grammar-symbol');

var _grammarSymbol2 = _interopRequireDefault(_grammarSymbol);

var _tablePrinter = require('./table-printer');

var _tablePrinter2 = _interopRequireDefault(_tablePrinter);

var _specialSymbols = require('./special-symbols');

var _debug = require('./debug');

var _debug2 = _interopRequireDefault(_debug);

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"); } }

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } /**
                                                                                                                                                                                                                   * The MIT License (MIT)
                                                                                                                                                                                                                   * Copyright (c) 2015-present Dmitry Soshnikov <dmitry.soshnikov@gmail.com>
                                                                                                                                                                                                                   */

// Default exclude set on merging.
var EXCLUDE_EPSILON = _defineProperty({}, _specialSymbols.EPSILON, true);

/**
 * Pasrsing sets generator.
 */

var SetsGenerator = function () {
  /**
   * Constructs First, Follow, and Predict sets
   * for a given grammar.
   */
  function SetsGenerator(_ref) {
    var grammar = _ref.grammar;

    _classCallCheck(this, SetsGenerator);

    this._grammar = grammar;
    this._firstSets = {};
    this._followSets = {};
    this._predictSets = {};
  }

  /**
   * Rules for First Sets
   *
   * - If X is a terminal then First(X) is just X!
   * - If there is a Production X → ε then add ε to first(X)
   * - If there is a Production X → Y1Y2..Yk then add first(Y1Y2..Yk) to First(X)
   * - First(Y1Y2..Yk) is either
   *     - First(Y1) (if First(Y1) doesn't contain ε)
   *     - OR (if First(Y1) does contain ε) then First(Y1Y2..Yk) is everything
   *       in First(Y1) <except for ε > as well as everything in First(Y2..Yk)
   *     - If First(Y1) First(Y2)..First(Yk) all contain ε then add ε
   *       to First(Y1Y2..Yk) as well.
   */


  _createClass(SetsGenerator, [{
    key: 'getFirstSets',
    value: function getFirstSets() {
      _debug2.default.time('Building First sets');
      this._buildSet(this.firstOf);
      _debug2.default.timeEnd('Building First sets');
      return this._firstSets;
    }

    /**
     * Returns First set for a particular symbol.
     *
     * If we have alternative productions for `S` as:
     *
     *   1. S -> "a" B
     *   2. S -> Y X
     *   3. Y -> "b"
     *
     * then the firstOf(S) is {"a": true, "b": true}, where "a" is gotten
     * directly, and "b" by following Y non-terminal.
     */

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

      if (symbol instanceof _grammarSymbol2.default) {
        symbol = symbol.getSymbol();
      }

      // A set may already be built from some previous analysis
      // of a RHS, so check whether it's already there and don't rebuild.
      if (this._firstSets[symbol]) {
        return this._firstSets[symbol];
      }

      var firstSet = this._firstSets[symbol] = {};

      // If it's a terminal, its First set contains just itself.
      if (this._grammar.isTokenSymbol(symbol) || _grammarSymbol2.default.isEpsilon(symbol) || _grammarSymbol2.default.isEOF(symbol)) {
        firstSet[symbol] = true;
        return this._firstSets[symbol];
      }

      var productionsForSymbol = this._grammar.getProductionsForSymbol(symbol);

      productionsForSymbol.forEach(function (production) {
        var RHS = production.getRHS();
        _this._mergeSets(firstSet, _this.firstOfRHS(RHS));
      });

      return firstSet;
    }

    /**
     * Returns First set of the whole RHS, excluding derived epsilons.
     */

  }, {
    key: 'firstOfRHS',
    value: function firstOfRHS(RHS) {
      var firstSet = {};

      for (var i = 0; i < RHS.length; i++) {
        var productionSymbol = RHS[i];

        // Direct epsilon goes to the First set.
        if (productionSymbol.isEpsilon()) {
          firstSet[_specialSymbols.EPSILON] = true;
          break;
        }

        // Calculate First of current symbol on RHS.
        var firstOfCurrent = this.firstOf(productionSymbol);

        // Put the First set of this non-terminal in our set,
        // excluding the EPSILON.
        this._mergeSets(firstSet, firstOfCurrent, EXCLUDE_EPSILON);

        // And if there was no EPSILON, we're done (otherwise, we
        // don't break the loop, and proceed to the next symbol of the RHS.
        if (!firstOfCurrent.hasOwnProperty(_specialSymbols.EPSILON)) {
          break;
        }

        // If all symbols on RHS are eliminated, or the last
        // symbol contains EPSILON, add it to the set.
        else if (i === RHS.length - 1) {
            firstSet[_specialSymbols.EPSILON] = true;
          }
      }

      return firstSet;
    }

    /**
     * Rules for Follow Sets
     *
     * - First put $ (the end of input marker) in Follow(S) (S is the start symbol)
     * - If there is a production A → aBb, (where a can be a whole string)
     *   then everything in FIRST(b) except for ε is placed in FOLLOW(B).
     * - If there is a production A → aB, then everything in
     *   FOLLOW(A) is in FOLLOW(B)
     * - If there is a production A → aBb, where FIRST(b) contains ε,
     *   then everything in FOLLOW(A) is in FOLLOW(B)
     */

  }, {
    key: 'getFollowSets',
    value: function getFollowSets() {
      _debug2.default.time('Building Follow sets');
      this._buildSet(this.followOf);
      _debug2.default.timeEnd('Building Follow sets');
      return this._followSets;
    }

    /**
     * Returns Follow set for a particular symbol.
     */

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

      if (symbol instanceof _grammarSymbol2.default) {
        symbol = symbol.getSymbol();
      }

      // If was already calculated from some previous run.
      if (this._followSets[symbol]) {
        return this._followSets[symbol];
      }

      // Else init and calculate.
      var followSet = this._followSets[symbol] = {};

      // Start symbol always contain `$` in its follow set.
      if (symbol === this._grammar.getStartSymbol()) {
        followSet[_specialSymbols.EOF] = true;
      }

      // We need to analyze all productions where our
      // symbol is used (i.e. where it appears on RHS).
      var productionsWithSymbol = this._grammar.getProductionsWithSymbol(symbol);

      productionsWithSymbol.forEach(function (production) {
        var RHS = production.getRHSSymbols();
        var symbolIndex = void 0;

        // Get the follow symbol of our symbol. A symbol can appear
        // several times on the RHS, e.g. S -> AaAb, so Follow set should
        // be {a, b}. Also a symbol may appear as the last symbol, in
        // which case it should be Follow(LHS).
        while ((symbolIndex = RHS.indexOf(symbol)) !== -1) {
          var followPart = RHS.slice(symbolIndex + 1);

          // followOf(symbol) is firstOf(followSymbol),
          // excluding epsilon.
          if (followPart.length > 0) {
            while (followPart.length > 0) {
              var productionSymbol = followPart[0];
              var firstOfFollow = _this2.firstOf(productionSymbol);
              _this2._mergeSets(followSet, firstOfFollow, EXCLUDE_EPSILON);
              // If no epsilon in the First of follow, we're done.
              if (!firstOfFollow.hasOwnProperty(_specialSymbols.EPSILON)) {
                break;
              }
              // Else, move to the next symbol, eliminating this one.
              followPart.shift();
            }
          }

          // If nothing following our symbol, or all following symbols
          // were eliminated (i.e. they all contained only epsilons)
          // we should merge followOf(LHS) to the Follow set of our symbol.
          if (followPart.length === 0) {
            var LHS = production.getLHS();
            if (!LHS.isSymbol(symbol)) {
              // To avoid cases like: B -> aB
              _this2._mergeSets(followSet, _this2.followOf(LHS));
            }
          }

          // Search in the next part.
          RHS = followPart;
        }
      });

      return followSet;
    }

    /**
     * The Predict set for a production is the First set of this production,
     * plus, if the First set contains ε, the Follow set.
     *
     * Predict(A -> α) = First(α) ∪ (if α =>* ε) then Follow(A) else ∅
     */

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

      this._predictSets = {};
      _debug2.default.time('Building Predict sets');

      this._grammar.getProductions().forEach(function (production) {
        var LHS = production.getLHS();
        var RHS = production.getRHS();

        if (RHS.length == 0 || RHS[0].isEpsilon()) {
          return;
        }

        var setKey = production.getNumber() + '. ' + production.toString();

        // Predict set for this production.
        var predictSet = _this3._predictSets[setKey] = {};

        // Consists of the First set.
        var firstSet = _this3.firstOfRHS(RHS);
        _this3._mergeSets(predictSet, firstSet);

        // Plus, the Follow set if First set contains epsilon.
        if (firstSet.hasOwnProperty(_specialSymbols.EPSILON)) {
          _this3._mergeSets(predictSet, _this3.followOf(LHS));
        }
      });

      _debug2.default.timeEnd('Building Predict sets');

      return this._predictSets;
    }

    /**
     * Outputs a set with the label in readable format.
     */

  }, {
    key: 'printSet',
    value: function printSet(set) {
      var lhsHeader = void 0;
      var rhsHeader = void 0;

      switch (set) {
        case this._firstSets:
          lhsHeader = 'Symbol';
          rhsHeader = 'First set';
          break;
        case this._followSets:
          lhsHeader = 'Symbol';
          rhsHeader = 'Follow set';
          break;
        case this._predictSets:
          lhsHeader = 'Production';
          rhsHeader = 'Predict set';
          break;
        default:
          throw new Error('Unknow set');
      }

      console.info('\n' + rhsHeader + ':\n');

      var printer = new _tablePrinter2.default({
        head: [lhsHeader, rhsHeader]
      });

      for (var symbol in set) {
        printer.push([symbol, Object.keys(set[symbol]).join(', ')]);
      }

      console.info(printer.toString());
      console.info('');
    }

    /**
     * Builds a set based on the `builder` function.
     */

  }, {
    key: '_buildSet',
    value: function _buildSet(builder) {
      var _this4 = this;

      this._grammar.getProductions().forEach(function (production) {
        builder.call(_this4, production.getLHS());
      });
    }
  }, {
    key: '_mergeSets',
    value: function _mergeSets(to, from, exclude) {
      exclude || (exclude = []);
      for (var k in from) {
        if (from.hasOwnProperty(k) && !exclude.hasOwnProperty(k)) {
          to[k] = from[k];
        }
      }
    }
  }]);

  return SetsGenerator;
}();

exports.default = SetsGenerator;