"use strict";

const visitor      = require("../visitor");
const asts         = require("../asts");
const GrammarError = require("../../grammar-error");

const ALWAYS_MATCH = 1;
const SOMETIMES_MATCH = 0;
const NEVER_MATCH = -1;

// Inference match result of the each node. Can be:
// -1: negative result, matching of that node always fails
//  0: neutral result, may be fail, may be match
//  1: positive result, always match
function inferenceMatchResult(ast) {
  function sometimesMatch(node) { return (node.match = SOMETIMES_MATCH); }
  function alwaysMatch(node) {
    // eslint-disable-next-line no-use-before-define -- Mutual recursion
    inference(node.expression);

    return (node.match = ALWAYS_MATCH);
  }

  function inferenceExpression(node) {
    // eslint-disable-next-line no-use-before-define -- Mutual recursion
    return (node.match = inference(node.expression));
  }
  function inferenceElements(elements, forChoice) {
    const length = elements.length;
    let always = 0;
    let never = 0;

    for (let i = 0; i < length; ++i) {
      // eslint-disable-next-line no-use-before-define -- Mutual recursion
      const result = inference(elements[i]);

      if (result === ALWAYS_MATCH) { ++always; }
      if (result === NEVER_MATCH)  { ++never;  }
    }

    if (always === length) {
      return ALWAYS_MATCH;
    }
    if (forChoice) {
      return never === length ? NEVER_MATCH : SOMETIMES_MATCH;
    }

    return never > 0 ? NEVER_MATCH : SOMETIMES_MATCH;
  }

  const inference = visitor.build({
    rule(node) {
      // eslint-disable-next-line init-declarations
      let oldResult;
      let count = 0;

      // If property not yet calculated, do that
      if (typeof node.match === "undefined") {
        node.match = SOMETIMES_MATCH;
        do {
          oldResult = node.match;
          node.match = inference(node.expression);
          // 6 == 3! -- permutations count for all transitions from one match
          // state to another.
          // After 6 iterations the cycle with guarantee begins
          // For example, an input of `start = [] start` will generate the
          // sequence: 0 -> -1 -> -1 (then stop)
          //
          // A more complex grammar theoretically would generate the
          // sequence: 0 -> 1 -> 0 -> -1 -> 0 -> 1 -> ... (then cycle)
          // but there are no examples of such grammars yet (possible, they
          // do not exist at all)

          // istanbul ignore next  This is canary test, shouldn't trigger in real life
          if (++count > 6) {
            throw new GrammarError(
              "Infinity cycle detected when trying to evaluate node match result",
              node.location
            );
          }
        } while (oldResult !== node.match);
      }

      return node.match;
    },
    named:        inferenceExpression,
    choice(node) {
      return (node.match = inferenceElements(node.alternatives, true));
    },
    action:       inferenceExpression,
    sequence(node) {
      return (node.match = inferenceElements(node.elements, false));
    },
    labeled:      inferenceExpression,
    text:         inferenceExpression,
    simple_and:   inferenceExpression,
    simple_not(node) {
      return (node.match = -inference(node.expression));
    },
    optional:     alwaysMatch,
    zero_or_more: alwaysMatch,
    one_or_more:  inferenceExpression,
    repeated(node) {
      const match = inference(node.expression);
      const dMatch = node.delimiter ? inference(node.delimiter) : NEVER_MATCH;
      // If minimum is `null` it is equals to maximum (parsed from `|exact|` syntax)
      const min = node.min ? node.min : node.max;

      // If any boundary are variable - it can be negative, and it that case
      // node does not match, but it may be match with some other values
      if (min.type !== "constant" || node.max.type !== "constant") {
        return (node.match = SOMETIMES_MATCH);
      }
      // Now both boundaries is constants
      // If the upper boundary is zero or minimum exceeds maximum,
      // matching is impossible
      if (node.max.value === 0
       || (node.max.value !== null && min.value > node.max.value)
      ) {
        return (node.match = NEVER_MATCH);
      }

      if (match === NEVER_MATCH) {
        // If an expression always fails, a range will also always fail
        // (with the one exception - never matched expression repeated
        //  zero times always match and returns an empty array).
        return (node.match = min.value === 0 ? ALWAYS_MATCH : NEVER_MATCH);
      }
      if (match === ALWAYS_MATCH) {
        if (node.delimiter && min.value >= 2) {
          // If an expression always match the final result determined only
          // by the delimiter, but delimiter used only when count of elements
          // two and more
          return (node.match = dMatch);
        }

        return (node.match = ALWAYS_MATCH);
      }

      // Here `match === SOMETIMES_MATCH`
      if (node.delimiter && min.value >= 2) {
        // If an expression always match the final result determined only
        // by the delimiter, but delimiter used only when count of elements
        // two and more
        return (
          // If a delimiter never match then the range also never match (because
          // there at least one delimiter)
          node.match = dMatch === NEVER_MATCH ? NEVER_MATCH : SOMETIMES_MATCH
        );
      }

      return (node.match = min.value === 0 ? ALWAYS_MATCH : SOMETIMES_MATCH);
    },
    group:        inferenceExpression,
    semantic_and: sometimesMatch,
    semantic_not: sometimesMatch,
    rule_ref(node) {
      const rule = asts.findRule(ast, node.name);
      if (!rule) {
        return SOMETIMES_MATCH;
      }
      return (node.match = inference(rule));
    },
    library_ref() {
      // Can't look into pre-compiled rules.
      return 0;
    },
    literal(node) {
      // Empty literal always match on any input
      const match = node.value.length === 0 ? ALWAYS_MATCH : SOMETIMES_MATCH;

      return (node.match = match);
    },
    class(node) {
      // Empty character class never match on any input
      const match = node.parts.length === 0 ? NEVER_MATCH : SOMETIMES_MATCH;

      return (node.match = match);
    },
    // |any| not match on empty input
    any:          sometimesMatch,
  });

  inference(ast);
}

inferenceMatchResult.ALWAYS_MATCH    = ALWAYS_MATCH;
inferenceMatchResult.SOMETIMES_MATCH = SOMETIMES_MATCH;
inferenceMatchResult.NEVER_MATCH     = NEVER_MATCH;

module.exports = inferenceMatchResult;
