import $ from "./dependencyLibs/inputmask.dependencyLib";
import MaskToken from "./masktoken";
import Inputmask from "./inputmask";

export {generateMaskSet, analyseMask};

function generateMaskSet(opts, nocache) {
    var ms;

    function generateMask(mask, metadata, opts) {
        var regexMask = false;
        if (mask === null || mask === "") {
            regexMask = opts.regex !== null;
            if (regexMask) {
                mask = opts.regex;
                mask = mask.replace(/^(\^)(.*)(\$)$/, "$2");
            } else {
                regexMask = true;
                mask = ".*";
            }
        }
        if (mask.length === 1 && opts.greedy === false && opts.repeat !== 0) {
            opts.placeholder = "";
        } //hide placeholder with single non-greedy mask
        if (opts.repeat > 0 || opts.repeat === "*" || opts.repeat === "+") {
            var repeatStart = opts.repeat === "*" ? 0 : (opts.repeat === "+" ? 1 : opts.repeat);
            mask = opts.groupmarker[0] + mask + opts.groupmarker[1] + opts.quantifiermarker[0] + repeatStart + "," + opts.repeat + opts.quantifiermarker[1];
        }

        // console.log(mask);
        var masksetDefinition, maskdefKey;
        maskdefKey = regexMask ? "regex_" + opts.regex : opts.numericInput ? mask.split("").reverse().join("") : mask;
        if (opts.keepStatic !== null) { //keepstatic modifies the output from the testdefinitions ~ so differentiate in the maskcache
            maskdefKey = "ks_" + opts.keepStatic + maskdefKey;
        }

        if (Inputmask.prototype.masksCache[maskdefKey] === undefined || nocache === true) {
            masksetDefinition = {
                "mask": mask,
                "maskToken": Inputmask.prototype.analyseMask(mask, regexMask, opts),
                "validPositions": {},
                "_buffer": undefined,
                "buffer": undefined,
                "tests": {},
                "excludes": {}, //excluded alternations
                "metadata": metadata,
                "maskLength": undefined,
                "jitOffset": {}
            };
            if (nocache !== true) {
                Inputmask.prototype.masksCache[maskdefKey] = masksetDefinition;
                masksetDefinition = $.extend(true, {}, Inputmask.prototype.masksCache[maskdefKey]);
            }
        } else {
            masksetDefinition = $.extend(true, {}, Inputmask.prototype.masksCache[maskdefKey]);
        }

        return masksetDefinition;
    }

    if (typeof opts.mask === "function") { //allow mask to be a preprocessing fn - should return a valid mask
        opts.mask = opts.mask(opts);
    }
    if (Array.isArray(opts.mask)) {
        if (opts.mask.length > 1) {
            if (opts.keepStatic === null) { //enable by default when passing multiple masks when the option is not explicitly specified
                opts.keepStatic = true;
            }
            var altMask = opts.groupmarker[0];
            (opts.isRTL ? opts.mask.reverse() : opts.mask).forEach(function (msk) {
                if (altMask.length > 1) {
                    altMask += opts.alternatormarker;
                }
                if (msk.mask !== undefined && typeof msk.mask !== "function") {
                    altMask += msk.mask;
                } else {
                    altMask += msk;
                }
            });
            altMask += opts.groupmarker[1];
            // console.log(altMask);
            return generateMask(altMask, opts.mask, opts);
        } else {
            opts.mask = opts.mask.pop();
        }
    }
    if (opts.mask && opts.mask.mask !== undefined && typeof opts.mask.mask !== "function") {
        ms = generateMask(opts.mask.mask, opts.mask, opts);
    } else {
        ms = generateMask(opts.mask, opts.mask, opts);
    }
    if (opts.keepStatic === null) opts.keepStatic = false;
    return ms;
}

function analyseMask(mask, regexMask, opts) {
    const tokenizer = /(?:[?*+]|\{[0-9+*]+(?:,[0-9+*]*)?(?:\|[0-9+*]*)?\})|[^.?*+^${[]()|\\]+|./g,
        //Thx to https://github.com/slevithan/regex-colorizer for the regexTokenizer regex
        regexTokenizer = /\[\^?]?(?:[^\\\]]+|\\[\S\s]?)*]?|\\(?:0(?:[0-3][0-7]{0,2}|[4-7][0-7]?)?|[1-9][0-9]*|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|c[A-Za-z]|[\S\s]?)|\((?:\?[:=!]?)?|(?:[?*+]|\{[0-9]+(?:,[0-9]*)?\})\??|[^.?*+^${[()|\\]+|./g;
    var escaped = false,
        currentToken = new MaskToken(),
        match,
        m,
        openenings = [],
        maskTokens = [],
        openingToken,
        currentOpeningToken,
        alternator,
        lastMatch,
        closeRegexGroup = false;

    //test definition => {fn: RegExp/function, static: true/false optionality: bool, newBlockMarker: bool, casing: null/upper/lower, def: definitionSymbol, placeholder: placeholder, mask: real maskDefinition}
    function insertTestDefinition(mtoken, element, position) {
        position = position !== undefined ? position : mtoken.matches.length;
        var prevMatch = mtoken.matches[position - 1];
        if (regexMask) {
            if (element.indexOf("[") === 0 || (escaped && /\\d|\\s|\\w/i.test(element)) || element === ".") {
                mtoken.matches.splice(position++, 0, {
                    fn: new RegExp(element, opts.casing ? "i" : ""),
                    static: false,
                    optionality: false,
                    newBlockMarker: prevMatch === undefined ? "master" : prevMatch.def !== element,
                    casing: null,
                    def: element,
                    placeholder: undefined,
                    nativeDef: element
                });
            } else {
                if (escaped) element = element[element.length - 1];
                element.split("").forEach(function (lmnt, ndx) {
                    prevMatch = mtoken.matches[position - 1];
                    mtoken.matches.splice(position++, 0, {
                        fn: /[a-z]/i.test((opts.staticDefinitionSymbol || lmnt)) ? new RegExp("[" + (opts.staticDefinitionSymbol || lmnt) + "]", opts.casing ? "i" : "") : null,
                        static: true,
                        optionality: false,
                        newBlockMarker: prevMatch === undefined ? "master" : (prevMatch.def !== lmnt && prevMatch.static !== true),
                        casing: null,
                        def: opts.staticDefinitionSymbol || lmnt,
                        placeholder: opts.staticDefinitionSymbol !== undefined ? lmnt : undefined,
                        nativeDef: (escaped ? "'" : "") + lmnt
                    });
                });
            }
            escaped = false;
        } else {
            var maskdef = (opts.definitions && opts.definitions[element]) || (opts.usePrototypeDefinitions && Inputmask.prototype.definitions[element]);
            if (maskdef && !escaped) {
                mtoken.matches.splice(position++, 0, {
                    fn: maskdef.validator ? typeof maskdef.validator == "string" ? new RegExp(maskdef.validator, opts.casing ? "i" : "") : new function () {
                        this.test = maskdef.validator;
                    } : new RegExp("."),
                    static: maskdef.static || false,
                    optionality: maskdef.optional || false,
                    newBlockMarker: (prevMatch === undefined || maskdef.optional) ? "master" : prevMatch.def !== (maskdef.definitionSymbol || element),
                    casing: maskdef.casing,
                    def: maskdef.definitionSymbol || element,
                    placeholder: maskdef.placeholder,
                    nativeDef: element,
                    generated: maskdef.generated
                });
            } else {
                mtoken.matches.splice(position++, 0, {
                    fn: /[a-z]/i.test((opts.staticDefinitionSymbol || element)) ? new RegExp("[" + (opts.staticDefinitionSymbol || element) + "]", opts.casing ? "i" : "") : null,
                    static: true,
                    optionality: false,
                    newBlockMarker: prevMatch === undefined ? "master" : (prevMatch.def !== element && prevMatch.static !== true),
                    casing: null,
                    def: opts.staticDefinitionSymbol || element,
                    placeholder: opts.staticDefinitionSymbol !== undefined ? element : undefined,
                    nativeDef: (escaped ? "'" : "") + element
                });
                escaped = false;
            }
        }
    }

    function verifyGroupMarker(maskToken) {
        if (maskToken && maskToken.matches) {
            maskToken.matches.forEach(function (token, ndx) {
                var nextToken = maskToken.matches[ndx + 1];
                if ((nextToken === undefined || (nextToken.matches === undefined || nextToken.isQuantifier === false)) && token && token.isGroup) { //this is not a group but a normal mask => convert
                    token.isGroup = false;
                    if (!regexMask) {
                        insertTestDefinition(token, opts.groupmarker[0], 0);
                        if (token.openGroup !== true) {
                            insertTestDefinition(token, opts.groupmarker[1]);
                        }
                    }
                }
                verifyGroupMarker(token);
            });
        }
    }

    function defaultCase() {
        if (openenings.length > 0) {
            currentOpeningToken = openenings[openenings.length - 1];
            insertTestDefinition(currentOpeningToken, m);
            if (currentOpeningToken.isAlternator) { //handle alternator a | b case
                alternator = openenings.pop();
                for (var mndx = 0; mndx < alternator.matches.length; mndx++) {
                    if (alternator.matches[mndx].isGroup) alternator.matches[mndx].isGroup = false; //don't mark alternate groups as group
                }
                if (openenings.length > 0) {
                    currentOpeningToken = openenings[openenings.length - 1];
                    currentOpeningToken.matches.push(alternator);
                } else {
                    currentToken.matches.push(alternator);
                }
            }
        } else {
            insertTestDefinition(currentToken, m);
        }
    }

    function reverseTokens(maskToken) {
        function reverseStatic(st) {
            if (st === opts.optionalmarker[0]) {
                st = opts.optionalmarker[1];
            } else if (st === opts.optionalmarker[1]) {
                st = opts.optionalmarker[0];
            } else if (st === opts.groupmarker[0]) {
                st = opts.groupmarker[1];
            } else if (st === opts.groupmarker[1]) st = opts.groupmarker[0];

            return st;
        }

        maskToken.matches = maskToken.matches.reverse();
        for (var match in maskToken.matches) {
            if (Object.prototype.hasOwnProperty.call(maskToken.matches, match)) {
                var intMatch = parseInt(match);
                if (maskToken.matches[match].isQuantifier && maskToken.matches[intMatch + 1] && maskToken.matches[intMatch + 1].isGroup) { //reposition quantifier
                    var qt = maskToken.matches[match];
                    maskToken.matches.splice(match, 1);
                    maskToken.matches.splice(intMatch + 1, 0, qt);
                }
                if (maskToken.matches[match].matches !== undefined) {
                    maskToken.matches[match] = reverseTokens(maskToken.matches[match]);
                } else {
                    maskToken.matches[match] = reverseStatic(maskToken.matches[match]);
                }
            }
        }

        return maskToken;
    }

    function groupify(matches) {
        var groupToken = new MaskToken(true);
        groupToken.openGroup = false;
        groupToken.matches = matches;
        return groupToken;
    }

    function closeGroup() {
        // Group closing
        openingToken = openenings.pop();
        openingToken.openGroup = false; //mark group as complete
        if (openingToken !== undefined) {
            if (openenings.length > 0) {
                currentOpeningToken = openenings[openenings.length - 1];
                currentOpeningToken.matches.push(openingToken);
                if (currentOpeningToken.isAlternator) { //handle alternator (a) | (b) case
                    alternator = openenings.pop();
                    let altMatchesLength = alternator.matches[0].matches ? alternator.matches[0].matches.length : 1;
                    for (var mndx = 0; mndx < alternator.matches.length; mndx++) {
                        alternator.matches[mndx].isGroup = false; //don't mark alternate groups as group
                        alternator.matches[mndx].alternatorGroup = false;
                        if (opts.keepStatic === null && altMatchesLength < (alternator.matches[mndx].matches ? alternator.matches[mndx].matches.length : 1)) { //enable by default when passing multiple masks when the option is not explicitly specified
                            opts.keepStatic = true;
                        }
                        altMatchesLength = alternator.matches[mndx].matches ? alternator.matches[mndx].matches.length : 1;
                    }
                    if (openenings.length > 0) {
                        currentOpeningToken = openenings[openenings.length - 1];
                        currentOpeningToken.matches.push(alternator);
                    } else {
                        currentToken.matches.push(alternator);
                    }
                }
            } else {
                currentToken.matches.push(openingToken);
            }
        } else {
            defaultCase();
        }
    }

    function groupQuantifier(matches) {
        var lastMatch = matches.pop();
        if (lastMatch.isQuantifier) {
            lastMatch = groupify([matches.pop(), lastMatch]);
        }
        return lastMatch;
    }

    if (regexMask) {
        opts.optionalmarker[0] = undefined;
        opts.optionalmarker[1] = undefined;
    }
    while ((match = regexMask ? regexTokenizer.exec(mask) : tokenizer.exec(mask))) {
        m = match[0];

        if (regexMask) {
            switch (m.charAt(0)) {
                //Quantifier
                case "?":
                    m = "{0,1}";
                    break;
                case "+":
                case "*":
                    m = "{" + m + "}";
                    break;
                case "|":
                    //regex mask alternator  ex: [01][0-9]|2[0-3] => ([01][0-9]|2[0-3])
                    if (openenings.length === 0) { //wrap the mask in a group to form a regex alternator  ([01][0-9]|2[0-3])
                        var altRegexGroup = groupify(currentToken.matches);
                        altRegexGroup.openGroup = true;
                        openenings.push(altRegexGroup);
                        currentToken.matches = [];
                        closeRegexGroup = true;
                    }
                    break;
            }
            switch (m) {
                case "\\d":
                    m = "[0-9]";
                    break;
                case "(?=": //lookahead
                    // openenings.push(new MaskToken(true));
                    break;
                case "(?!": //negative lookahead
                    // openenings.push(new MaskToken(true));
                    break;
                case "(?<=": //lookbehind
                    // openenings.push(new MaskToken(true));
                    break;
                case "(?<!": //negative lookbehind
                    // openenings.push(new MaskToken(true));
                    break;
            }
        }

        if (escaped) {
            defaultCase();
            continue;
        }
        switch (m.charAt(0)) {
            case "$":
            case "^":
                //ignore beginswith and endswith as in masking this makes no point
                if (!regexMask) {
                    defaultCase();
                }
                break;
            case opts.escapeChar:
                escaped = true;
                if (regexMask) defaultCase();
                break;
            // optional closing
            case opts.optionalmarker[1]:
            case opts.groupmarker[1]:
                closeGroup();
                break;
            case opts.optionalmarker[0]:
                // optional opening
                openenings.push(new MaskToken(false, true));
                break;
            case opts.groupmarker[0]:
                // Group opening
                openenings.push(new MaskToken(true));
                break;
            case opts.quantifiermarker[0]:
                //Quantifier
                var quantifier = new MaskToken(false, false, true);

                m = m.replace(/[{}?]/g, ""); //? matches lazy quantifiers
                var mqj = m.split("|"),
                    mq = mqj[0].split(","),
                    mq0 = isNaN(mq[0]) ? mq[0] : parseInt(mq[0]),
                    mq1 = mq.length === 1 ? mq0 : (isNaN(mq[1]) ? mq[1] : parseInt(mq[1])),
                    mqJit = isNaN(mqj[1]) ? mqj[1] : parseInt(mqj[1]);
                if (mq0 === "*" || mq0 === "+") {
                    mq0 = mq1 === "*" ? 0 : 1;
                }
                quantifier.quantifier = {
                    min: mq0,
                    max: mq1,
                    jit: mqJit
                };
                var matches = openenings.length > 0 ? openenings[openenings.length - 1].matches : currentToken.matches;
                match = matches.pop();
                if (match.isAlternator) { //handle quantifier in an alternation [0-9]{2}|[0-9]{3}
                    matches.push(match); //push back alternator
                    matches = match.matches; //remap target matches
                    var groupToken = new MaskToken(true);
                    var tmpMatch = matches.pop();
                    matches.push(groupToken); //push the group
                    matches = groupToken.matches;
                    match = tmpMatch;
                }
                if (!match.isGroup) {
                    // if (regexMask && match.fn === null) { //why is this needed???
                    //     if (match.def === ".") match.fn = new RegExp(match.def, opts.casing ? "i" : "");
                    // }

                    match = groupify([match]);
                }
                matches.push(match);
                matches.push(quantifier);

                break;
            case opts.alternatormarker:
                if (openenings.length > 0) {
                    currentOpeningToken = openenings[openenings.length - 1];
                    var subToken = currentOpeningToken.matches[currentOpeningToken.matches.length - 1];
                    if (currentOpeningToken.openGroup && //regexp alt syntax
                        (subToken.matches === undefined || (subToken.isGroup === false && subToken.isAlternator === false))) { //alternations within group
                        lastMatch = openenings.pop();
                    } else {
                        lastMatch = groupQuantifier(currentOpeningToken.matches);
                    }
                } else {
                    lastMatch = groupQuantifier(currentToken.matches);
                }
                if (lastMatch.isAlternator) {
                    openenings.push(lastMatch);
                } else {
                    if (lastMatch.alternatorGroup) {
                        alternator = openenings.pop();
                        lastMatch.alternatorGroup = false;
                    } else {
                        alternator = new MaskToken(false, false, false, true);
                    }
                    alternator.matches.push(lastMatch);
                    openenings.push(alternator);
                    if (lastMatch.openGroup) { //regexp alt syntax
                        lastMatch.openGroup = false;
                        var alternatorGroup = new MaskToken(true);
                        alternatorGroup.alternatorGroup = true;
                        openenings.push(alternatorGroup);
                    }
                }
                break;
            default:
                defaultCase();
        }
    }

    if (closeRegexGroup) closeGroup();

    while (openenings.length > 0) {
        openingToken = openenings.pop();
        currentToken.matches.push(openingToken);
    }
    if (currentToken.matches.length > 0) {
        verifyGroupMarker(currentToken);
        maskTokens.push(currentToken);
    }

    if (opts.numericInput || opts.isRTL) {
        reverseTokens(maskTokens[0]);
    }
    // console.log(JSON.stringify(maskTokens));
    return maskTokens;
}
