"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var tslib_1 = require("tslib");
/* eslint-disable no-unused-vars */
/**
 * @todo - Remove unused when JSDoc types are added for visitor methods
 */
var tree_1 = tslib_1.__importDefault(require("../tree"));
var visitor_1 = tslib_1.__importDefault(require("./visitor"));
var logger_1 = tslib_1.__importDefault(require("../logger"));
var utils = tslib_1.__importStar(require("../utils"));
/* jshint loopfunc:true */
var ExtendFinderVisitor = /** @class */ (function () {
    function ExtendFinderVisitor() {
        this._visitor = new visitor_1.default(this);
        this.contexts = [];
        this.allExtendsStack = [[]];
    }
    ExtendFinderVisitor.prototype.run = function (root) {
        root = this._visitor.visit(root);
        root.allExtends = this.allExtendsStack[0];
        return root;
    };
    ExtendFinderVisitor.prototype.visitDeclaration = function (declNode, visitArgs) {
        visitArgs.visitDeeper = false;
    };
    ExtendFinderVisitor.prototype.visitMixinDefinition = function (mixinDefinitionNode, visitArgs) {
        visitArgs.visitDeeper = false;
    };
    ExtendFinderVisitor.prototype.visitRuleset = function (rulesetNode, visitArgs) {
        if (rulesetNode.root) {
            return;
        }
        var i;
        var j;
        var extend;
        var allSelectorsExtendList = [];
        var extendList;
        // get &:extend(.a); rules which apply to all selectors in this ruleset
        var rules = rulesetNode.rules, ruleCnt = rules ? rules.length : 0;
        for (i = 0; i < ruleCnt; i++) {
            if (rulesetNode.rules[i] instanceof tree_1.default.Extend) {
                allSelectorsExtendList.push(rules[i]);
                rulesetNode.extendOnEveryPath = true;
            }
        }
        // now find every selector and apply the extends that apply to all extends
        // and the ones which apply to an individual extend
        var paths = rulesetNode.paths;
        for (i = 0; i < paths.length; i++) {
            var selectorPath = paths[i], selector = selectorPath[selectorPath.length - 1], selExtendList = selector.extendList;
            extendList = selExtendList ? utils.copyArray(selExtendList).concat(allSelectorsExtendList)
                : allSelectorsExtendList;
            if (extendList) {
                extendList = extendList.map(function (allSelectorsExtend) {
                    return allSelectorsExtend.clone();
                });
            }
            for (j = 0; j < extendList.length; j++) {
                this.foundExtends = true;
                extend = extendList[j];
                extend.findSelfSelectors(selectorPath);
                extend.ruleset = rulesetNode;
                if (j === 0) {
                    extend.firstExtendOnThisSelectorPath = true;
                }
                this.allExtendsStack[this.allExtendsStack.length - 1].push(extend);
            }
        }
        this.contexts.push(rulesetNode.selectors);
    };
    ExtendFinderVisitor.prototype.visitRulesetOut = function (rulesetNode) {
        if (!rulesetNode.root) {
            this.contexts.length = this.contexts.length - 1;
        }
    };
    ExtendFinderVisitor.prototype.visitMedia = function (mediaNode, visitArgs) {
        mediaNode.allExtends = [];
        this.allExtendsStack.push(mediaNode.allExtends);
    };
    ExtendFinderVisitor.prototype.visitMediaOut = function (mediaNode) {
        this.allExtendsStack.length = this.allExtendsStack.length - 1;
    };
    ExtendFinderVisitor.prototype.visitAtRule = function (atRuleNode, visitArgs) {
        atRuleNode.allExtends = [];
        this.allExtendsStack.push(atRuleNode.allExtends);
    };
    ExtendFinderVisitor.prototype.visitAtRuleOut = function (atRuleNode) {
        this.allExtendsStack.length = this.allExtendsStack.length - 1;
    };
    return ExtendFinderVisitor;
}());
var ProcessExtendsVisitor = /** @class */ (function () {
    function ProcessExtendsVisitor() {
        this._visitor = new visitor_1.default(this);
    }
    ProcessExtendsVisitor.prototype.run = function (root) {
        var extendFinder = new ExtendFinderVisitor();
        this.extendIndices = {};
        extendFinder.run(root);
        if (!extendFinder.foundExtends) {
            return root;
        }
        root.allExtends = root.allExtends.concat(this.doExtendChaining(root.allExtends, root.allExtends));
        this.allExtendsStack = [root.allExtends];
        var newRoot = this._visitor.visit(root);
        this.checkExtendsForNonMatched(root.allExtends);
        return newRoot;
    };
    ProcessExtendsVisitor.prototype.checkExtendsForNonMatched = function (extendList) {
        var indices = this.extendIndices;
        extendList.filter(function (extend) {
            return !extend.hasFoundMatches && extend.parent_ids.length == 1;
        }).forEach(function (extend) {
            var selector = '_unknown_';
            try {
                selector = extend.selector.toCSS({});
            }
            catch (_) { }
            if (!indices["".concat(extend.index, " ").concat(selector)]) {
                indices["".concat(extend.index, " ").concat(selector)] = true;
                /**
                 * @todo Shouldn't this be an error? To alert the developer
                 * that they may have made an error in the selector they are
                 * targeting?
                 */
                logger_1.default.warn("WARNING: extend '".concat(selector, "' has no matches"));
            }
        });
    };
    ProcessExtendsVisitor.prototype.doExtendChaining = function (extendsList, extendsListTarget, iterationCount) {
        //
        // chaining is different from normal extension.. if we extend an extend then we are not just copying, altering
        // and pasting the selector we would do normally, but we are also adding an extend with the same target selector
        // this means this new extend can then go and alter other extends
        //
        // this method deals with all the chaining work - without it, extend is flat and doesn't work on other extend selectors
        // this is also the most expensive.. and a match on one selector can cause an extension of a selector we had already
        // processed if we look at each selector at a time, as is done in visitRuleset
        var extendIndex;
        var targetExtendIndex;
        var matches;
        var extendsToAdd = [];
        var newSelector;
        var extendVisitor = this;
        var selectorPath;
        var extend;
        var targetExtend;
        var newExtend;
        iterationCount = iterationCount || 0;
        // loop through comparing every extend with every target extend.
        // a target extend is the one on the ruleset we are looking at copy/edit/pasting in place
        // e.g.  .a:extend(.b) {}  and .b:extend(.c) {} then the first extend extends the second one
        // and the second is the target.
        // the separation into two lists allows us to process a subset of chains with a bigger set, as is the
        // case when processing media queries
        for (extendIndex = 0; extendIndex < extendsList.length; extendIndex++) {
            for (targetExtendIndex = 0; targetExtendIndex < extendsListTarget.length; targetExtendIndex++) {
                extend = extendsList[extendIndex];
                targetExtend = extendsListTarget[targetExtendIndex];
                // look for circular references
                if (extend.parent_ids.indexOf(targetExtend.object_id) >= 0) {
                    continue;
                }
                // find a match in the target extends self selector (the bit before :extend)
                selectorPath = [targetExtend.selfSelectors[0]];
                matches = extendVisitor.findMatch(extend, selectorPath);
                if (matches.length) {
                    extend.hasFoundMatches = true;
                    // we found a match, so for each self selector..
                    extend.selfSelectors.forEach(function (selfSelector) {
                        var info = targetExtend.visibilityInfo();
                        // process the extend as usual
                        newSelector = extendVisitor.extendSelector(matches, selectorPath, selfSelector, extend.isVisible());
                        // but now we create a new extend from it
                        newExtend = new (tree_1.default.Extend)(targetExtend.selector, targetExtend.option, 0, targetExtend.fileInfo(), info);
                        newExtend.selfSelectors = newSelector;
                        // add the extend onto the list of extends for that selector
                        newSelector[newSelector.length - 1].extendList = [newExtend];
                        // record that we need to add it.
                        extendsToAdd.push(newExtend);
                        newExtend.ruleset = targetExtend.ruleset;
                        // remember its parents for circular references
                        newExtend.parent_ids = newExtend.parent_ids.concat(targetExtend.parent_ids, extend.parent_ids);
                        // only process the selector once.. if we have :extend(.a,.b) then multiple
                        // extends will look at the same selector path, so when extending
                        // we know that any others will be duplicates in terms of what is added to the css
                        if (targetExtend.firstExtendOnThisSelectorPath) {
                            newExtend.firstExtendOnThisSelectorPath = true;
                            targetExtend.ruleset.paths.push(newSelector);
                        }
                    });
                }
            }
        }
        if (extendsToAdd.length) {
            // try to detect circular references to stop a stack overflow.
            // may no longer be needed.
            this.extendChainCount++;
            if (iterationCount > 100) {
                var selectorOne = '{unable to calculate}';
                var selectorTwo = '{unable to calculate}';
                try {
                    selectorOne = extendsToAdd[0].selfSelectors[0].toCSS();
                    selectorTwo = extendsToAdd[0].selector.toCSS();
                }
                catch (e) { }
                throw { message: "extend circular reference detected. One of the circular extends is currently:".concat(selectorOne, ":extend(").concat(selectorTwo, ")") };
            }
            // now process the new extends on the existing rules so that we can handle a extending b extending c extending
            // d extending e...
            return extendsToAdd.concat(extendVisitor.doExtendChaining(extendsToAdd, extendsListTarget, iterationCount + 1));
        }
        else {
            return extendsToAdd;
        }
    };
    ProcessExtendsVisitor.prototype.visitDeclaration = function (ruleNode, visitArgs) {
        visitArgs.visitDeeper = false;
    };
    ProcessExtendsVisitor.prototype.visitMixinDefinition = function (mixinDefinitionNode, visitArgs) {
        visitArgs.visitDeeper = false;
    };
    ProcessExtendsVisitor.prototype.visitSelector = function (selectorNode, visitArgs) {
        visitArgs.visitDeeper = false;
    };
    ProcessExtendsVisitor.prototype.visitRuleset = function (rulesetNode, visitArgs) {
        if (rulesetNode.root) {
            return;
        }
        var matches;
        var pathIndex;
        var extendIndex;
        var allExtends = this.allExtendsStack[this.allExtendsStack.length - 1];
        var selectorsToAdd = [];
        var extendVisitor = this;
        var selectorPath;
        // look at each selector path in the ruleset, find any extend matches and then copy, find and replace
        for (extendIndex = 0; extendIndex < allExtends.length; extendIndex++) {
            for (pathIndex = 0; pathIndex < rulesetNode.paths.length; pathIndex++) {
                selectorPath = rulesetNode.paths[pathIndex];
                // extending extends happens initially, before the main pass
                if (rulesetNode.extendOnEveryPath) {
                    continue;
                }
                var extendList = selectorPath[selectorPath.length - 1].extendList;
                if (extendList && extendList.length) {
                    continue;
                }
                matches = this.findMatch(allExtends[extendIndex], selectorPath);
                if (matches.length) {
                    allExtends[extendIndex].hasFoundMatches = true;
                    allExtends[extendIndex].selfSelectors.forEach(function (selfSelector) {
                        var extendedSelectors;
                        extendedSelectors = extendVisitor.extendSelector(matches, selectorPath, selfSelector, allExtends[extendIndex].isVisible());
                        selectorsToAdd.push(extendedSelectors);
                    });
                }
            }
        }
        rulesetNode.paths = rulesetNode.paths.concat(selectorsToAdd);
    };
    ProcessExtendsVisitor.prototype.findMatch = function (extend, haystackSelectorPath) {
        //
        // look through the haystack selector path to try and find the needle - extend.selector
        // returns an array of selector matches that can then be replaced
        //
        var haystackSelectorIndex;
        var hackstackSelector;
        var hackstackElementIndex;
        var haystackElement;
        var targetCombinator;
        var i;
        var extendVisitor = this;
        var needleElements = extend.selector.elements;
        var potentialMatches = [];
        var potentialMatch;
        var matches = [];
        // loop through the haystack elements
        for (haystackSelectorIndex = 0; haystackSelectorIndex < haystackSelectorPath.length; haystackSelectorIndex++) {
            hackstackSelector = haystackSelectorPath[haystackSelectorIndex];
            for (hackstackElementIndex = 0; hackstackElementIndex < hackstackSelector.elements.length; hackstackElementIndex++) {
                haystackElement = hackstackSelector.elements[hackstackElementIndex];
                // if we allow elements before our match we can add a potential match every time. otherwise only at the first element.
                if (extend.allowBefore || (haystackSelectorIndex === 0 && hackstackElementIndex === 0)) {
                    potentialMatches.push({ pathIndex: haystackSelectorIndex, index: hackstackElementIndex, matched: 0,
                        initialCombinator: haystackElement.combinator });
                }
                for (i = 0; i < potentialMatches.length; i++) {
                    potentialMatch = potentialMatches[i];
                    // selectors add " " onto the first element. When we use & it joins the selectors together, but if we don't
                    // then each selector in haystackSelectorPath has a space before it added in the toCSS phase. so we need to
                    // work out what the resulting combinator will be
                    targetCombinator = haystackElement.combinator.value;
                    if (targetCombinator === '' && hackstackElementIndex === 0) {
                        targetCombinator = ' ';
                    }
                    // if we don't match, null our match to indicate failure
                    if (!extendVisitor.isElementValuesEqual(needleElements[potentialMatch.matched].value, haystackElement.value) ||
                        (potentialMatch.matched > 0 && needleElements[potentialMatch.matched].combinator.value !== targetCombinator)) {
                        potentialMatch = null;
                    }
                    else {
                        potentialMatch.matched++;
                    }
                    // if we are still valid and have finished, test whether we have elements after and whether these are allowed
                    if (potentialMatch) {
                        potentialMatch.finished = potentialMatch.matched === needleElements.length;
                        if (potentialMatch.finished &&
                            (!extend.allowAfter &&
                                (hackstackElementIndex + 1 < hackstackSelector.elements.length || haystackSelectorIndex + 1 < haystackSelectorPath.length))) {
                            potentialMatch = null;
                        }
                    }
                    // if null we remove, if not, we are still valid, so either push as a valid match or continue
                    if (potentialMatch) {
                        if (potentialMatch.finished) {
                            potentialMatch.length = needleElements.length;
                            potentialMatch.endPathIndex = haystackSelectorIndex;
                            potentialMatch.endPathElementIndex = hackstackElementIndex + 1; // index after end of match
                            potentialMatches.length = 0; // we don't allow matches to overlap, so start matching again
                            matches.push(potentialMatch);
                        }
                    }
                    else {
                        potentialMatches.splice(i, 1);
                        i--;
                    }
                }
            }
        }
        return matches;
    };
    ProcessExtendsVisitor.prototype.isElementValuesEqual = function (elementValue1, elementValue2) {
        if (typeof elementValue1 === 'string' || typeof elementValue2 === 'string') {
            return elementValue1 === elementValue2;
        }
        if (elementValue1 instanceof tree_1.default.Attribute) {
            if (elementValue1.op !== elementValue2.op || elementValue1.key !== elementValue2.key) {
                return false;
            }
            if (!elementValue1.value || !elementValue2.value) {
                if (elementValue1.value || elementValue2.value) {
                    return false;
                }
                return true;
            }
            elementValue1 = elementValue1.value.value || elementValue1.value;
            elementValue2 = elementValue2.value.value || elementValue2.value;
            return elementValue1 === elementValue2;
        }
        elementValue1 = elementValue1.value;
        elementValue2 = elementValue2.value;
        if (elementValue1 instanceof tree_1.default.Selector) {
            if (!(elementValue2 instanceof tree_1.default.Selector) || elementValue1.elements.length !== elementValue2.elements.length) {
                return false;
            }
            for (var i = 0; i < elementValue1.elements.length; i++) {
                if (elementValue1.elements[i].combinator.value !== elementValue2.elements[i].combinator.value) {
                    if (i !== 0 || (elementValue1.elements[i].combinator.value || ' ') !== (elementValue2.elements[i].combinator.value || ' ')) {
                        return false;
                    }
                }
                if (!this.isElementValuesEqual(elementValue1.elements[i].value, elementValue2.elements[i].value)) {
                    return false;
                }
            }
            return true;
        }
        return false;
    };
    ProcessExtendsVisitor.prototype.extendSelector = function (matches, selectorPath, replacementSelector, isVisible) {
        // for a set of matches, replace each match with the replacement selector
        var currentSelectorPathIndex = 0, currentSelectorPathElementIndex = 0, path = [], matchIndex, selector, firstElement, match, newElements;
        for (matchIndex = 0; matchIndex < matches.length; matchIndex++) {
            match = matches[matchIndex];
            selector = selectorPath[match.pathIndex];
            firstElement = new tree_1.default.Element(match.initialCombinator, replacementSelector.elements[0].value, replacementSelector.elements[0].isVariable, replacementSelector.elements[0].getIndex(), replacementSelector.elements[0].fileInfo());
            if (match.pathIndex > currentSelectorPathIndex && currentSelectorPathElementIndex > 0) {
                path[path.length - 1].elements = path[path.length - 1]
                    .elements.concat(selectorPath[currentSelectorPathIndex].elements.slice(currentSelectorPathElementIndex));
                currentSelectorPathElementIndex = 0;
                currentSelectorPathIndex++;
            }
            newElements = selector.elements
                .slice(currentSelectorPathElementIndex, match.index)
                .concat([firstElement])
                .concat(replacementSelector.elements.slice(1));
            if (currentSelectorPathIndex === match.pathIndex && matchIndex > 0) {
                path[path.length - 1].elements =
                    path[path.length - 1].elements.concat(newElements);
            }
            else {
                path = path.concat(selectorPath.slice(currentSelectorPathIndex, match.pathIndex));
                path.push(new tree_1.default.Selector(newElements));
            }
            currentSelectorPathIndex = match.endPathIndex;
            currentSelectorPathElementIndex = match.endPathElementIndex;
            if (currentSelectorPathElementIndex >= selectorPath[currentSelectorPathIndex].elements.length) {
                currentSelectorPathElementIndex = 0;
                currentSelectorPathIndex++;
            }
        }
        if (currentSelectorPathIndex < selectorPath.length && currentSelectorPathElementIndex > 0) {
            path[path.length - 1].elements = path[path.length - 1]
                .elements.concat(selectorPath[currentSelectorPathIndex].elements.slice(currentSelectorPathElementIndex));
            currentSelectorPathIndex++;
        }
        path = path.concat(selectorPath.slice(currentSelectorPathIndex, selectorPath.length));
        path = path.map(function (currentValue) {
            // we can re-use elements here, because the visibility property matters only for selectors
            var derived = currentValue.createDerived(currentValue.elements);
            if (isVisible) {
                derived.ensureVisibility();
            }
            else {
                derived.ensureInvisibility();
            }
            return derived;
        });
        return path;
    };
    ProcessExtendsVisitor.prototype.visitMedia = function (mediaNode, visitArgs) {
        var newAllExtends = mediaNode.allExtends.concat(this.allExtendsStack[this.allExtendsStack.length - 1]);
        newAllExtends = newAllExtends.concat(this.doExtendChaining(newAllExtends, mediaNode.allExtends));
        this.allExtendsStack.push(newAllExtends);
    };
    ProcessExtendsVisitor.prototype.visitMediaOut = function (mediaNode) {
        var lastIndex = this.allExtendsStack.length - 1;
        this.allExtendsStack.length = lastIndex;
    };
    ProcessExtendsVisitor.prototype.visitAtRule = function (atRuleNode, visitArgs) {
        var newAllExtends = atRuleNode.allExtends.concat(this.allExtendsStack[this.allExtendsStack.length - 1]);
        newAllExtends = newAllExtends.concat(this.doExtendChaining(newAllExtends, atRuleNode.allExtends));
        this.allExtendsStack.push(newAllExtends);
    };
    ProcessExtendsVisitor.prototype.visitAtRuleOut = function (atRuleNode) {
        var lastIndex = this.allExtendsStack.length - 1;
        this.allExtendsStack.length = lastIndex;
    };
    return ProcessExtendsVisitor;
}());
exports.default = ProcessExtendsVisitor;
//# sourceMappingURL=extend-visitor.js.map