/*
	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 getNCheck         = require("nth-check"),
	BaseFuncs         = require("boolbase"),
	attributesFactory = require("./attributes.js"),
	trueFunc          = BaseFuncs.trueFunc,
	falseFunc         = BaseFuncs.falseFunc;

function filtersFactory(adapter){
	var attributes  = attributesFactory(adapter),
		checkAttrib = attributes.rules.equals;

	//helper methods
	function equals(a, b){
		if(typeof adapter.equals === "function") return adapter.equals(a, b);

		return a === b;
	}

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

	function getChildFunc(next){
		return function(elem){
			return !!adapter.getParent(elem) && next(elem);
		};
	}

	var filters = {
		contains: function(next, text){
			return function contains(elem){
				return next(elem) && adapter.getText(elem).indexOf(text) >= 0;
			};
		},
		icontains: function(next, text){
			var itext = text.toLowerCase();
			return function icontains(elem){
				return next(elem) &&
					adapter.getText(elem).toLowerCase().indexOf(itext) >= 0;
			};
		},

		//location specific methods
		"nth-child": function(next, rule){
			var func = getNCheck(rule);

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

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

				for(var i = 0, pos = 0; i < siblings.length; i++){
					if(adapter.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 getChildFunc(next);

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

				for(var pos = 0, i = siblings.length - 1; i >= 0; i--){
					if(adapter.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 getChildFunc(next);

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

				for(var pos = 0, i = 0; i < siblings.length; i++){
					if(adapter.isTag(siblings[i])){
						if(siblings[i] === elem) break;
						if(adapter.getName(siblings[i]) === adapter.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 getChildFunc(next);

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

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

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

		//TODO determine the actual root element
		root: function(next){
			return function(elem){
				return !adapter.getParent(elem) && next(elem);
			};
		},

		scope: function(next, rule, options, context){
			if(!context || context.length === 0){
				//equivalent to :root
				return filters.root(next);
			}

			if(context.length === 1){
				//NOTE: can't be unpacked, as :has uses this for side-effects
				return function(elem){
					return equals(context[0], elem) && next(elem);
				};
			}

			return function(elem){
				return context.indexOf(elem) >= 0 && 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")
	};
	return filters;
}

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

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

		"first-child": function(elem){
			return getFirstElement(adapter.getSiblings(elem)) === elem;
		},
		"last-child": function(elem){
			var siblings = adapter.getSiblings(elem);

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

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

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

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

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

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

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

			return true;
		},
		"only-child": function(elem){
			var siblings = adapter.getSiblings(elem);

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

			return true;
		},

		//:matches(a, area, link)[href]
		link: function(elem){
			return adapter.hasAttrib(elem, "href");
		},
		visited: falseFunc, //seems to be a valid implementation
		//TODO: :any-link once the name is finalized (as an alias of :link)

		//forms
		//to consider: :target

		//:matches([selected], select:not([multiple]):not(> option[selected]) > option:first-of-type)
		selected: function(elem){
			if(adapter.hasAttrib(elem, "selected")) return true;
			else if(adapter.getName(elem) !== "option") return false;

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

			if(
				!parent ||
				adapter.getName(parent) !== "select" ||
				adapter.hasAttrib(parent, "multiple")
			) return false;

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

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

			return sawElem;
		},
		//https://html.spec.whatwg.org/multipage/scripting.html#disabled-elements
		//:matches(
		//  :matches(button, input, select, textarea, menuitem, optgroup, option)[disabled],
		//  optgroup[disabled] > option),
		// fieldset[disabled] * //TODO not child of first <legend>
		//)
		disabled: function(elem){
			return adapter.hasAttrib(elem, "disabled");
		},
		enabled: function(elem){
			return !adapter.hasAttrib(elem, "disabled");
		},
		//:matches(:matches(:radio, :checkbox)[checked], :selected) (TODO menuitem)
		checked: function(elem){
			return adapter.hasAttrib(elem, "checked") || pseudos.selected(elem);
		},
		//:matches(input, select, textarea)[required]
		required: function(elem){
			return adapter.hasAttrib(elem, "required");
		},
		//:matches(input, select, textarea):not([required])
		optional: function(elem){
			return !adapter.hasAttrib(elem, "required");
		},

		//jQuery extensions

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

		//:matches(button, input[type=button])
		button: function(elem){
			var name = adapter.getName(elem);
			return name === "button" ||
					name === "input" &&
					adapter.getAttributeValue(elem, "type") === "button";
		},
		//:matches(input, textarea, select, button)
		input: function(elem){
			var name = adapter.getName(elem);
			return name === "input" ||
					name === "textarea" ||
					name === "select" ||
					name === "button";
		},
		//input:matches(:not([type!='']), [type='text' i])
		text: function(elem){
			var attr;
			return adapter.getName(elem) === "input" && (
				!(attr = adapter.getAttributeValue(elem, "type")) ||
				attr.toLowerCase() === "text"
			);
		}
	};

	return pseudos;
}

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

//FIXME this feels hacky
var re_CSS3 = /^(?:(?:nth|last|first|only)-(?:child|of-type)|root|empty|(?:en|dis)abled|checked|not)$/;

function factory(adapter){
	var pseudos = pseudosFactory(adapter);
	var filters = filtersFactory(adapter);

	return {
		compile: function(next, data, options, context){
			var name = data.name,
				subselect = data.data;

			if(options && options.strict && !re_CSS3.test(name)){
				throw new Error(":" + name + " isn't part of CSS3");
			}

			if(typeof filters[name] === "function"){
				verifyArgs(filters[name], name,  subselect);
				return filters[name](next, subselect, options, context);
			} else if(typeof pseudos[name] === "function"){
				var func = pseudos[name];
				verifyArgs(func, name, subselect);

				if(next === trueFunc) return func;

				return function pseudoArgs(elem){
					return func(elem, subselect) && next(elem);
				};
			} else {
				throw new Error("unmatched pseudo-class :" + name);
			}
		},
		filters: filters,
		pseudos: pseudos
	};
}

module.exports = factory;
