"use strict";
var __extends = (this && this.__extends) || function (d, b) {
    for (var p in b) /* istanbul ignore next */  if (b.hasOwnProperty(p)) d[p] = b[p];
    function __() { this.constructor = d; }
    /* istanbul ignore next */  d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
var utils = require("../../utils/utils");
var utils_1 = require("../../utils/utils");
var parser_public_1 = require("../parser_public");
var gast_public_1 = require("./gast_public");
var gast_1 = require("./gast");
var tokens_public_1 = require("../../scan/tokens_public");
var first_1 = require("./first");
var lookahead_1 = require("./lookahead");
function validateGrammar(topLevels, maxLookahead, ignoredIssues) {
    var duplicateErrors = utils.map(topLevels, validateDuplicateProductions);
    var leftRecursionErrors = utils.map(topLevels, function (currTopRule) { return validateNoLeftRecursion(currTopRule, currTopRule); });
    var emptyAltErrors = utils_1.map(topLevels, validateEmptyOrAlternative);
    var ambiguousAltsErrors = utils_1.map(topLevels, function (currTopRule) {
        return validateAmbiguousAlternationAlternatives(currTopRule, maxLookahead, ignoredIssues);
    });
    return utils.flatten(duplicateErrors.concat(leftRecursionErrors, emptyAltErrors, ambiguousAltsErrors));
}
exports.validateGrammar = validateGrammar;
function validateDuplicateProductions(topLevelRule) {
    var collectorVisitor = new OccurrenceValidationCollector();
    topLevelRule.accept(collectorVisitor);
    var allRuleProductions = collectorVisitor.allProductions;
    var productionGroups = utils.groupBy(allRuleProductions, identifyProductionForDuplicates);
    var duplicates = utils.pick(productionGroups, function (currGroup) {
        return currGroup.length > 1;
    });
    var errors = utils.map(utils.values(duplicates), function (currDuplicates) {
        var firstProd = utils.first(currDuplicates);
        var msg = createDuplicatesErrorMessage(currDuplicates, topLevelRule.name);
        var dslName = gast_1.getProductionDslName(firstProd);
        var defError = {
            message: msg,
            type: parser_public_1.ParserDefinitionErrorType.DUPLICATE_PRODUCTIONS,
            ruleName: topLevelRule.name,
            dslName: dslName,
            occurrence: firstProd.occurrenceInParent
        };
        var param = getExtraProductionArgument(firstProd);
        if (param) {
            defError.parameter = param;
        }
        return defError;
    });
    return errors;
}
function createDuplicatesErrorMessage(duplicateProds, topLevelName) {
    var firstProd = utils.first(duplicateProds);
    var index = firstProd.occurrenceInParent;
    var dslName = gast_1.getProductionDslName(firstProd);
    var extraArgument = getExtraProductionArgument(firstProd);
    var msg = "->" + dslName + "<- with occurrence index: ->" + index + "<-\n                  " + (extraArgument ? "and argument: " + extraArgument : "") + "\n                  appears more than once (" + duplicateProds.length + " times) in the top level rule: " + topLevelName + ".\n                  " + (index === 1 ? "note that " + dslName + " and " + dslName + "1 both have the same occurrence index 1}" : "") + "}\n                  to fix this make sure each usage of " + dslName + " " + (extraArgument ? "with the argument: " + extraArgument : "") + "\n                  in the rule " + topLevelName + " has a different occurrence index (1-5), as that combination acts as a unique\n                  position key in the grammar, which is needed by the parsing engine.";
    // white space trimming time! better to trim afterwards as it allows to use WELL formatted multi line template strings...
    msg = msg.replace(/[ \t]+/g, " ");
    msg = msg.replace(/\s\s+/g, "\n");
    return msg;
}
function identifyProductionForDuplicates(prod) {
    return gast_1.getProductionDslName(prod) + "_#_" + prod.occurrenceInParent + "_#_" + getExtraProductionArgument(prod);
}
exports.identifyProductionForDuplicates = identifyProductionForDuplicates;
function getExtraProductionArgument(prod) {
    if (prod instanceof gast_public_1.gast.Terminal) {
        return tokens_public_1.tokenName(prod.terminalType);
    }
    else if (prod instanceof gast_public_1.gast.NonTerminal) {
        return prod.nonTerminalName;
    }
    else {
        return "";
    }
}
var OccurrenceValidationCollector = (function (_super) {
    __extends(OccurrenceValidationCollector, _super);
    function OccurrenceValidationCollector() {
        _super.apply(this, arguments);
        this.allProductions = [];
    }
    OccurrenceValidationCollector.prototype.visitNonTerminal = function (subrule) {
        this.allProductions.push(subrule);
    };
    OccurrenceValidationCollector.prototype.visitOption = function (option) {
        this.allProductions.push(option);
    };
    OccurrenceValidationCollector.prototype.visitRepetitionWithSeparator = function (manySep) {
        this.allProductions.push(manySep);
    };
    OccurrenceValidationCollector.prototype.visitRepetitionMandatory = function (atLeastOne) {
        this.allProductions.push(atLeastOne);
    };
    OccurrenceValidationCollector.prototype.visitRepetitionMandatoryWithSeparator = function (atLeastOneSep) {
        this.allProductions.push(atLeastOneSep);
    };
    OccurrenceValidationCollector.prototype.visitRepetition = function (many) {
        this.allProductions.push(many);
    };
    OccurrenceValidationCollector.prototype.visitAlternation = function (or) {
        this.allProductions.push(or);
    };
    OccurrenceValidationCollector.prototype.visitTerminal = function (terminal) {
        this.allProductions.push(terminal);
    };
    return OccurrenceValidationCollector;
}(gast_public_1.gast.GAstVisitor));
exports.OccurrenceValidationCollector = OccurrenceValidationCollector;
var ruleNamePattern = /^[a-zA-Z_]\w*$/;
function validateRuleName(ruleName, className) {
    var errors = [];
    var errMsg;
    if (!ruleName.match(ruleNamePattern)) {
        errMsg = "Invalid Grammar rule name: ->" + ruleName + "<- it must match the pattern: ->" + ruleNamePattern.toString() + "<-";
        errors.push({
            message: errMsg,
            type: parser_public_1.ParserDefinitionErrorType.INVALID_RULE_NAME,
            ruleName: ruleName
        });
    }
    return errors;
}
exports.validateRuleName = validateRuleName;
function validateRuleDoesNotAlreadyExist(ruleName, definedRulesNames, className) {
    var errors = [];
    var errMsg;
    if ((utils.contains(definedRulesNames, ruleName))) {
        errMsg = "Duplicate definition, rule: ->" + ruleName + "<- is already defined in the grammar: ->" + className + "<-";
        errors.push({
            message: errMsg,
            type: parser_public_1.ParserDefinitionErrorType.DUPLICATE_RULE_NAME,
            ruleName: ruleName
        });
    }
    return errors;
}
exports.validateRuleDoesNotAlreadyExist = validateRuleDoesNotAlreadyExist;
// TODO: is there anyway to get only the rule names of rules inherited from the super grammars?
function validateRuleIsOverridden(ruleName, definedRulesNames, className) {
    var errors = [];
    var errMsg;
    if (!(utils.contains(definedRulesNames, ruleName))) {
        errMsg = ("Invalid rule override, rule: ->" + ruleName + "<- cannot be overridden in the grammar: ->" + className + "<-") +
            "as it is not defined in any of the super grammars ";
        errors.push({
            message: errMsg,
            type: parser_public_1.ParserDefinitionErrorType.INVALID_RULE_OVERRIDE,
            ruleName: ruleName
        });
    }
    return errors;
}
exports.validateRuleIsOverridden = validateRuleIsOverridden;
function validateNoLeftRecursion(topRule, currRule, path) {
    if (path === void 0) { path = []; }
    var errors = [];
    var nextNonTerminals = getFirstNoneTerminal(currRule.definition);
    if (utils.isEmpty(nextNonTerminals)) {
        return [];
    }
    else {
        var ruleName = topRule.name;
        var foundLeftRecursion = utils.contains(nextNonTerminals, topRule);
        var pathNames = utils.map(path, function (currRule) { return currRule.name; });
        var leftRecursivePath = ruleName + " --> " + pathNames.concat([ruleName]).join(" --> ");
        if (foundLeftRecursion) {
            var errMsg = "Left Recursion found in grammar.\n" +
                ("rule: <" + ruleName + "> can be invoked from itself (directly or indirectly)\n") +
                ("without consuming any Tokens. The grammar path that causes this is: \n " + leftRecursivePath + "\n") +
                " To fix this refactor your grammar to remove the left recursion.\n" +
                "see: https://en.wikipedia.org/wiki/LL_parser#Left_Factoring.";
            errors.push({
                message: errMsg,
                type: parser_public_1.ParserDefinitionErrorType.LEFT_RECURSION,
                ruleName: ruleName
            });
        }
        // we are only looking for cyclic paths leading back to the specific topRule
        // other cyclic paths are ignored, we still need this difference to avoid infinite loops...
        var validNextSteps = utils.difference(nextNonTerminals, path.concat([topRule]));
        var errorsFromNextSteps = utils.map(validNextSteps, function (currRefRule) {
            var newPath = utils.cloneArr(path);
            newPath.push(currRefRule);
            return validateNoLeftRecursion(topRule, currRefRule, newPath);
        });
        return errors.concat(utils.flatten(errorsFromNextSteps));
    }
}
exports.validateNoLeftRecursion = validateNoLeftRecursion;
function getFirstNoneTerminal(definition) {
    var result = [];
    if (utils.isEmpty(definition)) {
        return result;
    }
    var firstProd = utils.first(definition);
    if (firstProd instanceof gast_public_1.gast.NonTerminal) {
        result.push(firstProd.referencedRule);
    }
    else if (firstProd instanceof gast_public_1.gast.Flat ||
        firstProd instanceof gast_public_1.gast.Option ||
        firstProd instanceof gast_public_1.gast.RepetitionMandatory ||
        firstProd instanceof gast_public_1.gast.RepetitionMandatoryWithSeparator ||
        firstProd instanceof gast_public_1.gast.RepetitionWithSeparator ||
        firstProd instanceof gast_public_1.gast.Repetition) {
        result = result.concat(getFirstNoneTerminal(firstProd.definition));
    }
    else if (firstProd instanceof gast_public_1.gast.Alternation) {
        // each sub definition in alternation is a FLAT
        result = utils.flatten(utils.map(firstProd.definition, function (currSubDef) { return getFirstNoneTerminal(currSubDef.definition); }));
    }/* istanbul ignore else */ 
    else if (firstProd instanceof gast_public_1.gast.Terminal) {
    }
    else {
        /* istanbul ignore next */ throw Error("non exhaustive match");
    }
    var isFirstOptional = gast_1.isOptionalProd(firstProd);
    var hasMore = definition.length > 1;
    if (isFirstOptional && hasMore) {
        var rest = utils.drop(definition);
        return result.concat(getFirstNoneTerminal(rest));
    }
    else {
        return result;
    }
}
exports.getFirstNoneTerminal = getFirstNoneTerminal;
var OrCollector = (function (_super) {
    __extends(OrCollector, _super);
    function OrCollector() {
        _super.apply(this, arguments);
        this.alternations = [];
    }
    OrCollector.prototype.visitAlternation = function (node) {
        this.alternations.push(node);
    };
    return OrCollector;
}(gast_public_1.gast.GAstVisitor));
function validateEmptyOrAlternative(topLevelRule) {
    var orCollector = new OrCollector();
    topLevelRule.accept(orCollector);
    var ors = orCollector.alternations;
    var errors = utils.reduce(ors, function (errors, currOr) {
        var exceptLast = utils.dropRight(currOr.definition);
        var currErrors = utils.map(exceptLast, function (currAlternative, currAltIdx) {
            if (utils.isEmpty(first_1.first(currAlternative))) {
                return {
                    message: ("Ambiguous empty alternative: <" + (currAltIdx + 1) + ">") +
                        (" in <OR" + currOr.occurrenceInParent + "> inside <" + topLevelRule.name + "> Rule.\n") +
                        "Only the last alternative may be an empty alternative.",
                    type: parser_public_1.ParserDefinitionErrorType.NONE_LAST_EMPTY_ALT,
                    ruleName: topLevelRule.name,
                    occurrence: currOr.occurrenceInParent,
                    alternative: currAltIdx + 1
                };
            }
            else {
                return null;
            }
        });
        return errors.concat(utils.compact(currErrors));
    }, []);
    return errors;
}
exports.validateEmptyOrAlternative = validateEmptyOrAlternative;
function validateAmbiguousAlternationAlternatives(topLevelRule, maxLookahead, ignoredIssues) {
    var orCollector = new OrCollector();
    topLevelRule.accept(orCollector);
    var ors = orCollector.alternations;
    var ignoredIssuesForCurrentRule = ignoredIssues[topLevelRule.name];
    if (ignoredIssuesForCurrentRule) {
        ors = utils_1.reject(ors, function (currOr) { return ignoredIssuesForCurrentRule[gast_1.getProductionDslName(currOr) + currOr.occurrenceInParent]; });
    }
    var errors = utils.reduce(ors, function (result, currOr) {
        var currOccurrence = currOr.occurrenceInParent;
        var alternatives = lookahead_1.getLookaheadPathsForOr(currOccurrence, topLevelRule, maxLookahead);
        var altsAmbiguityErrors = checkAlternativesAmbiguities(alternatives);
        var currErrors = utils.map(altsAmbiguityErrors, function (currAmbDescriptor) {
            var ambgIndices = utils_1.map(currAmbDescriptor.alts, function (currAltIdx) { return currAltIdx + 1; });
            var pathMsg = utils_1.map(currAmbDescriptor.path, function (currtok) { return tokens_public_1.tokenLabel(currtok); }).join(", ");
            var currMessage = ("Ambiguous alternatives: <" + ambgIndices.join(" ,") + "> in <OR" + currOccurrence + ">") +
                (" inside <" + topLevelRule.name + "> Rule,\n") +
                ("<" + pathMsg + "> may appears as a prefix path in all these alternatives.\n");
            // Should this information be on the error message or in some common errors docs?
            currMessage = currMessage + "To Resolve this, try one of of the following: \n" +
                "1. Refactor your grammar to be LL(K) for the current value of k (by default k=5)\n" +
                "2. Increase the value of K for your grammar by providing a larger 'maxLookahead' value in the parser's config\n" +
                "3. This issue can be ignored (if you know what you are doing...), see" +
                " http://sap.github.io/chevrotain/documentation/0_9_0/interfaces/iparserconfig.html for\n";
            return {
                message: currMessage,
                type: parser_public_1.ParserDefinitionErrorType.AMBIGUOUS_ALTS,
                ruleName: topLevelRule.name,
                occurrence: currOr.occurrenceInParent,
                alternatives: [currAmbDescriptor.alts]
            };
        });
        return result.concat(currErrors);
    }, []);
    return errors;
}
exports.validateAmbiguousAlternationAlternatives = validateAmbiguousAlternationAlternatives;
function checkAlternativesAmbiguities(alternatives) {
    var foundAmbiguousPaths = [];
    var identicalAmbiguities = utils_1.reduce(alternatives, function (result, currAlt, currAltIdx) {
        utils_1.forEach(currAlt, function (currPath) {
            var altsCurrPathAppearsIn = [currAltIdx];
            utils_1.forEach(alternatives, function (currOtherAlt, currOtherAltIdx) {
                if (currAltIdx !== currOtherAltIdx && lookahead_1.containsPath(currOtherAlt, currPath)) {
                    altsCurrPathAppearsIn.push(currOtherAltIdx);
                }
            });
            if (altsCurrPathAppearsIn.length > 1 && !lookahead_1.containsPath(foundAmbiguousPaths, currPath)) {
                foundAmbiguousPaths.push(currPath);
                result.push({
                    alts: altsCurrPathAppearsIn,
                    path: currPath
                });
            }
        });
        return result;
    }, []);
    return identicalAmbiguities;
}
