/*

compiles a selector to an executable function

*/

module.exports = compile; module.exports.compileUnsafe = compileUnsafe; module.exports.compileToken = compileToken;

var parse = require(“css-what”),

DomUtils    = require("domutils"),
isTag       = DomUtils.isTag,
Rules       = require("./general.js"),
sortRules   = require("./sort.js"),
BaseFuncs   = require("boolbase"),
trueFunc    = BaseFuncs.trueFunc,
falseFunc   = BaseFuncs.falseFunc,
procedure   = require("./procedure.json");

function compile(selector, options, context){

var next = compileUnsafe(selector, options, context);
return wrap(next);

}

function wrap(next){

return function base(elem){
        return isTag(elem) && next(elem);
};

}

function compileUnsafe(selector, options, context){

var token = parse(selector, options);
return compileToken(token, options, context);

}

function includesScopePseudo(t){

return t.type === "pseudo" && (
    t.name === "scope" || (
        Array.isArray(t.data) &&
        t.data.some(function(data){
            return data.some(includesScopePseudo);
        })
    )
);

}

var DESCENDANT_TOKEN = {type: “descendant”},

SCOPE_TOKEN = {type: "pseudo", name: "scope"},
PLACEHOLDER_ELEMENT = {},
getParent = DomUtils.getParent;

//CSS 4 Spec (Draft): 3.3.1. Absolutizing a Scope-relative Selector //http://www.w3.org/TR/selectors4/#absolutizing function absolutize(token, context){

//TODO better check if context is document
var hasContext = !!context && !!context.length && context.every(function(e){
    return e === PLACEHOLDER_ELEMENT || !!getParent(e);
});

token.forEach(function(t){
    if(t.length > 0 && isTraversal(t[0]) && t[0].type !== "descendant"){
        //don't return in else branch
    } else if(hasContext && !includesScopePseudo(t)){
        t.unshift(DESCENDANT_TOKEN);
    } else {
        return;
    }

    t.unshift(SCOPE_TOKEN);
});

}

function compileToken(token, options, context){

token = token.filter(function(t){ return t.length > 0; });

    token.forEach(sortRules);

    var isArrayContext = Array.isArray(context);

context = (options && options.context) || context;

if(context && !isArrayContext) context = [context];

absolutize(token, context);

    return token
            .map(function(rules){ return compileRules(rules, options, context, isArrayContext); })
            .reduce(reduceRules, falseFunc);

}

function isTraversal(t){

return procedure[t.type] < 0;

}

function compileRules(rules, options, context, isArrayContext){

var acceptSelf = (isArrayContext && rules[0].name === "scope" && rules[1].type === "descendant");
return rules.reduce(function(func, rule, index){
        if(func === falseFunc) return func;
        return Rules[rule.type](func, rule, options, context, acceptSelf && index === 1);
}, options && options.rootFunc || trueFunc);

}

function reduceRules(a, b){

if(b === falseFunc || a === trueFunc){
        return a;
}
if(a === falseFunc || b === trueFunc){
        return b;
}

return function combine(elem){
        return a(elem) || b(elem);
};

}

//:not, :has and :matches have to compile selectors //doing this in lib/pseudos.js would lead to circular dependencies, //so we add them here

var Pseudos = require(“./pseudos.js”),

filters     = Pseudos.filters,
existsOne   = DomUtils.existsOne,
isTag       = DomUtils.isTag,
getChildren = DomUtils.getChildren;

function containsTraversal(t){

return t.some(isTraversal);

}

filters.not = function(next, token, options, context){

    var opts = {
            xmlMode: !!(options && options.xmlMode),
            strict: !!(options && options.strict)
        };

    if(opts.strict){
            if(token.length > 1 || token.some(containsTraversal)){
                    throw new SyntaxError("complex selectors in :not aren't allowed in strict mode");
            }
    }

var func = compileToken(token, opts, context);

    if(func === falseFunc) return next;
    if(func === trueFunc)  return falseFunc;

    return function(elem){
            return !func(elem) && next(elem);
    };

};

filters.has = function(next, token, options){

    var opts = {
            xmlMode: !!(options && options.xmlMode),
            strict: !!(options && options.strict)
    };

//FIXME: Uses an array as a pointer to the current element (side effects)
var context = token.some(containsTraversal) ? [PLACEHOLDER_ELEMENT] : null;

    var func = compileToken(token, opts, context);

    if(func === falseFunc) return falseFunc;
    if(func === trueFunc)  return function(elem){
                    return getChildren(elem).some(isTag) && next(elem);
            };

    func = wrap(func);

if(context){
    return function has(elem){
            return next(elem) && (
            (context[0] = elem), existsOne(func, getChildren(elem))
        );
    };
}

return function has(elem){
            return next(elem) && existsOne(func, getChildren(elem));
    };

};

filters.matches = function(next, token, options, context){

var opts = {
        xmlMode: !!(options && options.xmlMode),
        strict: !!(options && options.strict),
        rootFunc: next
};

return compileToken(token, opts, context);

};