/*
	pseudo selectors
	
	---
	
	they are available in two forms:
	* filters called when the selector 
	  is compiled and return a function
	  that needs to return next()
	* pseudos get called on execution
	  they need to return a boolean
*/

var DomUtils    = require("domutils"),
    isTag       = DomUtils.isTag,
    getText     = DomUtils.getText,
    getParent   = DomUtils.getParent,
    getChildren = DomUtils.getChildren,
    getSiblings = DomUtils.getSiblings,
    hasAttrib   = DomUtils.hasAttrib,
    getName     = DomUtils.getName,
    getAttribute= DomUtils.getAttributeValue,
    getNCheck   = require("./nth-check.js"),
    checkAttrib = require("./attributes.js").rules.equals,
    BaseFuncs   = require("./basefunctions.js"),
    rootFunc    = BaseFuncs.rootFunc,
    trueFunc    = BaseFuncs.trueFunc,
    falseFunc   = BaseFuncs.falseFunc;

//helper methods
function getFirstElement(elems){
	for(var i = 0; elems && i < elems.length; i++){
		if(isTag(elems[i])) return elems[i];
	}
}

function getAttribFunc(name, value){
	var data = {name: name, value: value};
	return function attribFunc(next){
		return checkAttrib(next, data);
	};
}

var filters = {
	contains: function(next, text){
		if(
			(text.charAt(0) === "\"" || text.charAt(0) === "'") &&
			text.charAt(0) === text.substr(-1)
		){
			text = text.slice(1, -1);
		}
		return function contains(elem){
			return getText(elem).indexOf(text) >= 0 && next(elem);
		};
	},

	//location specific methods
	//first- and last-child methods return as soon as they find another element
	"first-child": function(next){
		return function firstChild(elem){
			return getFirstElement(getSiblings(elem)) === elem && next(elem);
		};
	},
	"last-child": function(next){
		return function lastChild(elem){
			var siblings = getSiblings(elem);

			for(var i = siblings.length - 1; i >= 0; i--){
				if(siblings[i] === elem) return next(elem);
				if(isTag(siblings[i])) break;
			}

			return false;
		};
	},
	"first-of-type": function(next){
		return function firstOfType(elem){
			var siblings = getSiblings(elem);

			for(var i = 0; i < siblings.length; i++){
				if(isTag(siblings[i])){
					if(siblings[i] === elem) return next(elem);
					if(getName(siblings[i]) === getName(elem)) break;
				}
			}

			return false;
		};
	},
	"last-of-type": function(next){
		return function lastOfType(elem){
			var siblings = getSiblings(elem);

			for(var i = siblings.length-1; i >= 0; i--){
				if(isTag(siblings[i])){
					if(siblings[i] === elem) return next(elem);
					if(getName(siblings[i]) === getName(elem)) break;
				}
			}

			return false;
		};
	},
	"only-of-type": function(next){
		return function onlyOfType(elem){
			var siblings = getSiblings(elem);

			for(var i = 0, j = siblings.length; i < j; i++){
				if(isTag(siblings[i])){
					if(siblings[i] === elem) continue;
					if(getName(siblings[i]) === getName(elem)) return false;
				}
			}

			return next(elem);
		};
	},
	"only-child": function(next){
		return function onlyChild(elem){
			var siblings = getSiblings(elem);

			for(var i = 0; i < siblings.length; i++){
				if(isTag(siblings[i]) && siblings[i] !== elem) return false;
			}

			return next(elem);
		};
	},
	"nth-child": function(next, rule){
		var func = getNCheck(rule);

		if(func === falseFunc) return func;
		if(func === trueFunc){
			return next === rootFunc ? func : next;
		}

		return function nthChild(elem){
			var siblings = getSiblings(elem);

			for(var i = 0, pos = 0; i < siblings.length; i++){
				if(isTag(siblings[i])){
					if(siblings[i] === elem) break;
					else pos++;
				}
			}

			return func(pos) && next(elem);
		};
	},
	"nth-last-child": function(next, rule){
		var func = getNCheck(rule);

		if(func === falseFunc) return func;
		if(func === trueFunc){
			return next === rootFunc ? func : next;
		}

		return function nthLastChild(elem){
			var siblings = getSiblings(elem);

			for(var pos = 0, i = siblings.length - 1; i >= 0; i--){
				if(isTag(siblings[i])){
					if(siblings[i] === elem) break;
					else pos++;
				}
			}

			return func(pos) && next(elem);
		};
	},
	"nth-of-type": function(next, rule){
		var func = getNCheck(rule);

		if(func === falseFunc) return func;
		if(func === trueFunc){
			return next === rootFunc ? func : next;
		}

		return function nthOfType(elem){
			var siblings = getSiblings(elem);

			for(var pos = 0, i = 0; i < siblings.length; i++){
				if(isTag(siblings[i])){
					if(siblings[i] === elem) break;
					if(getName(siblings[i]) === getName(elem)) pos++;
				}
			}

			return func(pos) && next(elem);
		};
	},
	"nth-last-of-type": function(next, rule){
		var func = getNCheck(rule);

		if(func === falseFunc) return func;
		if(func === trueFunc){
			return next === rootFunc ? func : next;
		}

		return function nthLastOfType(elem){
			var siblings = getSiblings(elem);

			for(var pos = 0, i = siblings.length-1; i >= 0; i--){
				if(siblings[i] === elem) break;
				if(getName(siblings[i]) === getName(elem)) pos++;
			}

			return func(pos) && next(elem);
		};
	},

	//jQuery extensions (others follow as pseudos)
	checkbox: getAttribFunc("type", "checkbox"),
	file: getAttribFunc("type", "file"),
	password: getAttribFunc("type", "password"),
	radio: getAttribFunc("type", "radio"),
	reset: getAttribFunc("type", "reset"),
	image: getAttribFunc("type", "image"),
	submit: getAttribFunc("type", "submit")
};

//while filters are precompiled, pseudos get called when they are needed
var pseudos = {
	root: function(elem){
		return !getParent(elem);
	},
	empty: function(elem){
		return !getChildren(elem).some(function(elem){
			return isTag(elem) || elem.type === "text";
		});
	},

	//forms
	//to consider: :target, :enabled
	selected: function(elem){
		if(hasAttrib(elem, "selected")) return true;
		else if(getName(elem) !== "option") return false;

		//the first <option> in a <select> is also selected
		var parent = getParent(elem);

		if(!parent || getName(parent) !== "select") return false;

		var siblings = getChildren(parent),
			sawElem  = false;

		for(var i = 0; i < siblings.length; i++){
			if(isTag(siblings[i])){
				if(siblings[i] === elem){
					sawElem = true;
				} else if(!sawElem){
					return false;
				} else if(hasAttrib(siblings[i], "selected")){
					return false;
				}
			}
		}

		return sawElem;
	},
	disabled: function(elem){
		return hasAttrib(elem, "disabled");
	},
	enabled: function(elem){
		return !hasAttrib(elem, "disabled");
	},
	checked: function(elem){
		return hasAttrib(elem, "checked") || pseudos.selected(elem);
	},

	//jQuery extensions

	//:parent is the inverse of :empty
	parent: function(elem){
		return !pseudos.empty(elem);
	},
	header: function(elem){
		var name = getName(elem);
		return name === "h1" ||
		       name === "h2" ||
		       name === "h3" ||
		       name === "h4" ||
		       name === "h5" ||
		       name === "h6";
	},

	button: function(elem){
		var name = getName(elem);
		return name === "button" ||
		       name === "input" &&
		       getAttribute(elem, "type") === "button";
	},
	input: function(elem){
		var name = getName(elem);
		return name === "input" ||
		       name === "textarea" ||
		       name === "select" ||
		       name === "button";
	},
	text: function(elem){
		var attr;
		return getName(elem) === "input" && (
			!(attr = getAttribute(elem, "type")) ||
			attr.toLowerCase() === "text"
		);
	}
};

function verifyArgs(func, name, subselect){
	if(subselect === null){
		if(func.length > 1){
			throw new SyntaxError("pseudo-selector :" + name + " requires an argument");
		}
	} else {
		if(func.length === 1){
			throw new SyntaxError("pseudo-selector :" + name + " doesn't have any arguments");
		}	
	}
}

module.exports = {
	compile: function(next, data){
		var name = data.name,
			subselect = data.data;

		if(typeof filters[name] === "function"){
			verifyArgs(filters[name], name,  subselect);
			return filters[name](next, subselect);
		} else if(typeof pseudos[name] === "function"){
			var func = pseudos[name];
			verifyArgs(func, name, subselect);
			return function pseudoArgs(elem){
				return func(elem, subselect) && next(elem);
			};
		} else {
			throw new SyntaxError("unmatched pseudo-class :" + name);
		}
	},
	filters: filters,
	pseudos: pseudos
};
