var tree = require(“../tree”),

Visitor = require("./visitor");

var CSSVisitorUtils = function(context) {

this._visitor = new Visitor(this);
this._context = context;

};

CSSVisitorUtils.prototype = {

containsSilentNonBlockedChild: function(bodyRules) {
    var rule;
    if (bodyRules == null) {
        return false;
    }
    for (var r = 0; r < bodyRules.length; r++) {
        rule = bodyRules[r];
        if (rule.isSilent && rule.isSilent(this._context) && !rule.blocksVisibility()) {
            //the directive contains something that was referenced (likely by extend)
            //therefore it needs to be shown in output too
            return true;
        }
    }
    return false;
},

keepOnlyVisibleChilds: function(owner) {
    if (owner == null || owner.rules == null) {
        return ;
    }

    owner.rules = owner.rules.filter(function(thing) {
            return thing.isVisible();
        }
    );
},

isEmpty: function(owner) {
    if (owner == null || owner.rules == null) {
        return true;
    }
    return owner.rules.length === 0;
},

hasVisibleSelector: function(rulesetNode) {
    if (rulesetNode == null || rulesetNode.paths == null) {
        return false;
    }
    return rulesetNode.paths.length > 0;
},

resolveVisibility: function (node, originalRules) {
    if (!node.blocksVisibility()) {
        if (this.isEmpty(node) && !this.containsSilentNonBlockedChild(originalRules)) {
            return ;
        }

        return node;
    }

    var compiledRulesBody = node.rules[0];
    this.keepOnlyVisibleChilds(compiledRulesBody);

    if (this.isEmpty(compiledRulesBody)) {
        return ;
    }

    node.ensureVisibility();
    node.removeVisibilityBlock();

    return node;
},

isVisibleRuleset: function(rulesetNode) {
    if (rulesetNode.firstRoot) {
        return true;
    }

    if (this.isEmpty(rulesetNode)) {
        return false;
    }

    if (!rulesetNode.root && !this.hasVisibleSelector(rulesetNode)) {
        return false;
    }

    return true;
}

};

var ToCSSVisitor = function(context) {

this._visitor = new Visitor(this);
this._context = context;
this.utils = new CSSVisitorUtils(context);

};

ToCSSVisitor.prototype = {

isReplacing: true,
run: function (root) {
    return this._visitor.visit(root);
},

visitRule: function (ruleNode, visitArgs) {
    if (ruleNode.blocksVisibility() || ruleNode.variable) {
        return;
    }
    return ruleNode;
},

visitMixinDefinition: function (mixinNode, visitArgs) {
    // mixin definitions do not get eval'd - this means they keep state
    // so we have to clear that state here so it isn't used if toCSS is called twice
    mixinNode.frames = [];
},

visitExtend: function (extendNode, visitArgs) {
},

visitComment: function (commentNode, visitArgs) {
    if (commentNode.blocksVisibility() || commentNode.isSilent(this._context)) {
        return;
    }
    return commentNode;
},

visitMedia: function(mediaNode, visitArgs) {
    var originalRules = mediaNode.rules[0].rules;
    mediaNode.accept(this._visitor);
    visitArgs.visitDeeper = false;

    return this.utils.resolveVisibility(mediaNode, originalRules);
},

visitImport: function (importNode, visitArgs) {
    if (importNode.blocksVisibility()) {
        return ;
    }
    return importNode;
},

visitDirective: function(directiveNode, visitArgs) {
    if (directiveNode.rules && directiveNode.rules.length) {
        return this.visitDirectiveWithBody(directiveNode, visitArgs);
    } else {
        return this.visitDirectiveWithoutBody(directiveNode, visitArgs);
    }
    return directiveNode;
},

visitDirectiveWithBody: function(directiveNode, visitArgs) {
    //if there is only one nested ruleset and that one has no path, then it is
    //just fake ruleset
    function hasFakeRuleset(directiveNode) {
        var bodyRules = directiveNode.rules;
        return bodyRules.length === 1 && (!bodyRules[0].paths || bodyRules[0].paths.length === 0);
    }
    function getBodyRules(directiveNode) {
        var nodeRules = directiveNode.rules;
        if (hasFakeRuleset(directiveNode)) {
            return nodeRules[0].rules;
        }

        return nodeRules;
    }
    //it is still true that it is only one ruleset in array
    //this is last such moment
    //process childs
    var originalRules = getBodyRules(directiveNode);
    directiveNode.accept(this._visitor);
    visitArgs.visitDeeper = false;

    if (!this.utils.isEmpty(directiveNode)) {
        this._mergeRules(directiveNode.rules[0].rules);
    }

    return this.utils.resolveVisibility(directiveNode, originalRules);
},

visitDirectiveWithoutBody: function(directiveNode, visitArgs) {
    if (directiveNode.blocksVisibility()) {
        return;
    }

    if (directiveNode.name === "@charset") {
        // Only output the debug info together with subsequent @charset definitions
        // a comment (or @media statement) before the actual @charset directive would
        // be considered illegal css as it has to be on the first line
        if (this.charset) {
            if (directiveNode.debugInfo) {
                var comment = new tree.Comment("/* " + directiveNode.toCSS(this._context).replace(/\n/g, "") + " */\n");
                comment.debugInfo = directiveNode.debugInfo;
                return this._visitor.visit(comment);
            }
            return;
        }
        this.charset = true;
    }

    return directiveNode;
},

checkPropertiesInRoot: function(rules) {
    var ruleNode;
    for (var i = 0; i < rules.length; i++) {
        ruleNode = rules[i];
        if (ruleNode instanceof tree.Rule && !ruleNode.variable) {
            throw { message: "properties must be inside selector blocks, they cannot be in the root.",
                index: ruleNode.index, filename: ruleNode.currentFileInfo ? ruleNode.currentFileInfo.filename : null};
        }
    }
},

visitRuleset: function (rulesetNode, visitArgs) {
    //at this point rulesets are nested into each other
    var rule, rulesets = [];
    if (rulesetNode.firstRoot) {
        this.checkPropertiesInRoot(rulesetNode.rules);
    }
    if (! rulesetNode.root) {
        //remove invisible paths
        this._compileRulesetPaths(rulesetNode);

        // remove rulesets from this ruleset body and compile them separately
        var nodeRules = rulesetNode.rules, nodeRuleCnt = nodeRules ? nodeRules.length : 0;
        for (var i = 0; i < nodeRuleCnt; ) {
            rule = nodeRules[i];
            if (rule && rule.rules) {
                // visit because we are moving them out from being a child
                rulesets.push(this._visitor.visit(rule));
                nodeRules.splice(i, 1);
                nodeRuleCnt--;
                continue;
            }
            i++;
        }
        // accept the visitor to remove rules and refactor itself
        // then we can decide nogw whether we want it or not
        // compile body
        if (nodeRuleCnt > 0) {
            rulesetNode.accept(this._visitor);
        } else {
            rulesetNode.rules = null;
        }
        visitArgs.visitDeeper = false;

    } else { //if (! rulesetNode.root) {
        rulesetNode.accept(this._visitor);
        visitArgs.visitDeeper = false;
    }

    if (rulesetNode.rules) {
        this._mergeRules(rulesetNode.rules);
        this._removeDuplicateRules(rulesetNode.rules);
    }

    //now decide whether we keep the ruleset
    if (this.utils.isVisibleRuleset(rulesetNode)) {
        rulesetNode.ensureVisibility();
        rulesets.splice(0, 0, rulesetNode);
    }

    if (rulesets.length === 1) {
        return rulesets[0];
    }
    return rulesets;
},

_compileRulesetPaths: function(rulesetNode) {
    if (rulesetNode.paths) {
        rulesetNode.paths = rulesetNode.paths
            .filter(function(p) {
                var i;
                if (p[0].elements[0].combinator.value === ' ') {
                    p[0].elements[0].combinator = new(tree.Combinator)('');
                }
                for (i = 0; i < p.length; i++) {
                    if (p[i].isVisible() && p[i].getIsOutput()) {
                        return true;
                    }
                }
                return false;
            });
    }
},

_removeDuplicateRules: function(rules) {
    if (!rules) { return; }

    // remove duplicates
    var ruleCache = {},
        ruleList, rule, i;

    for (i = rules.length - 1; i >= 0 ; i--) {
        rule = rules[i];
        if (rule instanceof tree.Rule) {
            if (!ruleCache[rule.name]) {
                ruleCache[rule.name] = rule;
            } else {
                ruleList = ruleCache[rule.name];
                if (ruleList instanceof tree.Rule) {
                    ruleList = ruleCache[rule.name] = [ruleCache[rule.name].toCSS(this._context)];
                }
                var ruleCSS = rule.toCSS(this._context);
                if (ruleList.indexOf(ruleCSS) !== -1) {
                    rules.splice(i, 1);
                } else {
                    ruleList.push(ruleCSS);
                }
            }
        }
    }
},

_mergeRules: function (rules) {
    if (!rules) { return; }

    var groups = {},
        parts,
        rule,
        key;

    for (var i = 0; i < rules.length; i++) {
        rule = rules[i];

        if ((rule instanceof tree.Rule) && rule.merge) {
            key = [rule.name,
                rule.important ? "!" : ""].join(",");

            if (!groups[key]) {
                groups[key] = [];
            } else {
                rules.splice(i--, 1);
            }

            groups[key].push(rule);
        }
    }

    Object.keys(groups).map(function (k) {

        function toExpression(values) {
            return new (tree.Expression)(values.map(function (p) {
                return p.value;
            }));
        }

        function toValue(values) {
            return new (tree.Value)(values.map(function (p) {
                return p;
            }));
        }

        parts = groups[k];

        if (parts.length > 1) {
            rule = parts[0];
            var spacedGroups = [];
            var lastSpacedGroup = [];
            parts.map(function (p) {
                if (p.merge === "+") {
                    if (lastSpacedGroup.length > 0) {
                        spacedGroups.push(toExpression(lastSpacedGroup));
                    }
                    lastSpacedGroup = [];
                }
                lastSpacedGroup.push(p);
            });
            spacedGroups.push(toExpression(lastSpacedGroup));
            rule.value = toValue(spacedGroups);
        }
    });
},

visitAnonymous: function(anonymousNode, visitArgs) {
    if (anonymousNode.blocksVisibility()) {
        return ;
    }
    anonymousNode.accept(this._visitor);
    return anonymousNode;
}

};

module.exports = ToCSSVisitor;