var Selector = 
{
	result			: new Array(),
	currentPseudo	: new Array('not', 'enabled', 'disabled', 'checked', 'readonly', 'selected'),
	afterPseudo		: new Array('last-child', 'first-child', 'nth-child'),
	expFull			: /^((\.|\#|\:|\*|\>|\+|\~)?([\w\-]+)?)(\..*?\b)?(\[.*?\])?(\:.*?)?$/mi,
	assocFull		: ['full', 'name', 'pre', 'className', 'attribute', 'pseudo'],
	expPseudo		: /\:([a-z\-]+)(\((.*?)\))?/mig,
	assocPseudo		: ['full', 'name', 'tmp', 'param'],
	expAttribute	: /\[(\w+)(.*?)([\w-]+)?\]/mig,
	assocAttribute	: ['full', 'name', 'rel', 'value'],
	pseudos			: {
		'root'			: {type:'root'},
		
		'not'			: {type:'test', fn:'_pseudoMatch'},
		'enabled'		: {type:'test', fn:'_pseudoMatch'},
		'disabled'		: {type:'test', fn:'_pseudoMatch'},
		'checked'		: {type:'test', fn:'_pseudoMatch'},
		'readonly'		: {type:'test', fn:'_pseudoMatch'},
		'selected'		: {type:'test', fn:'_pseudoMatch'},
		'selected'		: {type:'test', fn:'_pseudoMatch'},
		
		'last-child'	: {type:'test', fn:'_applyPseudo'},
		'first-child'	: {type:'test', fn:'_applyPseudo'},		
		'only-child'	: {type:'test', fn:'_applyPseudo'},		
		'nth-child'		: {type:'getElement', fn:'_applyPseudo'},
		'self'			: {type:'getElement', fn:'_applyPseudo'}
	},
	cache			: {},
	
	/**
	 * Értelmezi a teljes kifejezést és végre is hajtja azt, majd a találati listával tér vissza.
	 *
	 * @param HTMLElement parent Erre az elemre kell viszgálni a kifejezést
	 * @param String selector A vizsgálandó kifejezés
	 * @param Boolean refresh Ha TRUE, akkor nem használ cachet (bizonyos esetekben lassabb lehet)
	 * @return Array|HTMLElement Akkor HTMLElement, ha a kifejezés értékei: body, :root, #id, minden más esetben Array
	 */
	parse: function(parent, selector, refresh, alwaysReturnArray)
	{
		var sl 			= selector.toLowerCase().split(',');
		var result 		= new Array();
		this.result		= new Array();
		
		if( typeof alwaysReturnArray == 'undefined' )
			alwaysReturnArray = false;
			
		this.refresh	= refresh || false;
		this.XPath		= !!document.evaluate;
		this.cache		= {childSelector: false, adjacentSelector: false, followingSelector: false};
		
		for(var k=0, sl_length = sl.length ; k<sl_length ; k++)
		{
			var t = this._helper.str.trim(sl[k]).split(' ');
			this.result[k] = new Array( (parent || document) );
			
			for(var i=0 ; i<t.length ; i++)
				this._parseTag(this._helper.str.trim(t[i]), k);
			
			//result = this._helper.array.extend(result, this.result[k]);
		}
		
		/*result = new jsl.HTMLNodeList(result);*/
		
		if( !alwaysReturnArray )
		{
			if( selector == 'body' && this.result[0][0] )
				return this.result[0][0];
			
			else if( selector.slice(0,1) == '#' && sl.length == 1 && this._helper.str.trim(sl[0]).split(' ').length == 1 && this.result.length == 1 && this.result[0].length == 1 )
				return this.result[0][0];
		}
		
        //return result;
		this.result.each = function(fn, bind)
		{
			var p = bind || this;
			for(var i=0 ; i<this.length ; i++)
				for(var k=0 ; k<this[i].length ; k++)
					fn.call(p, this[i][k]); // ide kéne majd a new HTMLElement rész...
			return this;
		};
		
		return this.result;
		return result;
	},	
	
	/**
	 * Egy kifejezést illeszt az adott elementre.
	 * pl.: a.className[href][name]:nth-child(2)
	 *
	 * @param HTMLElelement node Az element amire illeszteni kívánjuk a kifejezést
	 * @param String selector A kifejezés amit illeszteni akarunk
	 * @param Array parsedSelector Belső sebességoptimalizálás miatt meglehet adni a már beparseolt selectort is
	 * @return Boolean TRUE ha illeszkedik, egyébként FALSE
	 */
	match: function(node, selector, parsedSelector)
	{
		if(parsedSelector)
			match = parsedSelector;
		else if( (match = this.expFull.exec(selector)) )
			this.expFull.lastIndex = 0;
		
		var result = false;
		
		for(var k=0, current ; k<match.length ; k++)
		{
			if( k==0 || k==2 || k==3 || !match[k] ) continue;
			current = match[k];
			switch(current.slice(0,1))
			{
				case '.':
					result = this._helper.match.className(node, current.slice(1));
				break;
			
				case '#':
					result = node.id == current.slice(1);
				break;
			
				case ':':
					current = current.toLowerCase();
					var pass = false;
					while( (pseudo = this.expPseudo.exec(current)) )
					{
						var name = pseudo[1];
						var param = pseudo[3];
						var data = this.pseudos[name] || {};							
						switch(data.type)
						{
							case 'getElement':
								if(param == 'even') param = '2n+2'; else
								if(param == 'odd') param = '2n-1';									
								param = new RegExp('(?:(?:\\-|\\+)?(\\d*)(?:n))?((?:\\-|\\+)?\\d*)', 'img').exec(param);
								param = {a:param[1]-0, b:param[2]-0, n:(param[0].indexOf('n') != -1)};									
							case 'test':
								if( !(pass = !!this[data.fn](name, param, node)) )
									return false;
							break;
						}
					}
					this.expPseudo.lastIndex = 0;
					result = pass;
				break;
			
				case '[':
					var pass = false;
					while( (attribute = this.expAttribute.exec(current)) )
					{
						if( !(pass = this._attributeMatch(attribute[1], attribute[3], attribute[2], node)) )
							break;
					}
					this.expAttribute.lastIndex = 0;
					result = pass;
				break;
			
				default:
					if(current.toUpperCase() == node.tagName) result = true;
					else if(current == '*') result = true;											
				break;
			}
			if(!result) return result;
		}
		
		return result;
	},
	
	/**
	 * Egy kifejezést értelmez, ami szóköztől szóközig tart.
	 * pl.: div.className[attribute=attr_value]:nth-child(2)
	 *
	 * @param String item A kifejezés amit értelmezni kell
	 * @param Integer index A kifejezés sorszáma
	 */
	_parseTag: function(item, index)
	{
		if( (match = this.expFull.exec(item)) )
			match = match;
		
		var chkClass = this._helper.match.className;
		var full = this.result[index];
		this.expFull.lastIndex = 0;
		
		// E > F
		if(this.cache.childSelector)
		{
			var result = [];
			for(var i=0, node ; node = full[i] ; i++)
			{
				for(var child = node.firstChild ; child ; child = child.nextSibling)
				{
					if(this.match(child, null, match))
						result.push(child);					
				}				
			}
			
			full = result;
			this.cache.childSelector = false;
		}
		// E + F
		else if(this.cache.adjacentSelector)
		{
			var result = [];
			for(var i=0, node ; node = full[i] ; i++)
			{
				for(var next = node.nextSibling ; next ; next = next.nextSibling)
				{
					if(next.nodeType == 1)
					{
						if(this.match(next, null, match))
							result.push(next);
						break;
					}
				}
			}
			
			full = result;
			this.cache.adjacentSelector = false;
		}
		// E ~ F
		else if(this.cache.followingSelector)
		{
			// TODO: sebesség optimalizálása
			var result = [];
			var cache = {};
			for(var i=0, node ; node = full[i] ; i++)
			{
				for(var next = node.nextSibling ; next ; next = next.nextSibling)
				{
					if(next.nodeType == 1)
					{
						var id = jsl.Element.getCacheID(next);
						if(cache[id]) break;
						if(this.match(next, null, match))
						{
							cache[id] = true;
							result.push(next);
						}						
					}
				}
			}
			
			full = result;
			this.cache.followingSelector = false;
		}
		else if(match)
		{
			for(var k=0 ; k<match.length ; k++)
			{
				if( k==0 || k==2 || k==3 || !match[k] ) continue;
				
				var current = match[k];
				switch(current.slice(0,1))
				{
					//className
					case '.':
						//ha className-el kezdődik a selector
						var className = current.slice(1);
						var result = [];
						if(k==0)
						{
							//lényegesen gyorsabb ahol támoatott a document.evaluate
							if(this.XPath)
							{
								for(var i=0 ; i<full.length ; i++)
								{
									var tmp = document.evaluate(".//*[contains(concat(' ', @class, ' '), ' "+className+" ')]", full[i], null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
									for(var z=0, length = tmp.snapshotLength ; z<length ; z++)
										result.push(tmp.snapshotItem(z));							
								}
								full = result;
							}
							else
							{
								for(var i=0 ; i<full.length ; i++)
								{
									var tmp = full[i].getElementsByTagName('*');
									for(var z=0, node; node = tmp[z] ; z++)
									{
										if(node.className.length == 0) continue;
										if(chkClass(node, className))
											result.push(node);
									}
								}
								full = result;
							}
						}
						else
						{
							for(var i=0 ; i<full.length ; i++)
							{
								if(full[i].className.length == 0) continue;
								if(chkClass(full[i], className))
									result.push(full[i]);
							}
							
							full = result;
						}
					break;
				
					//id
					case '#':
						full = new Array(document.getElementById(current.slice(1)));                        
					break;
				
					//pseudo
					case ':':
						if(k==0){ this._parseTag('*', index); full = this.result[index]; }						
						current = current.toLowerCase();
						while( (pseudo = this.expPseudo.exec(current)) )
						{
							var name = pseudo[1];
							var param = pseudo[3];
							var data = this.pseudos[name] || {};							
							switch(data.type)
							{
								case 'root':
									for(var node = document.firstChild ; node ; node = node.nextSibling)
										if(node.nodeType == 1){ full = new Array(elm); break; }									
								break;
							
								case 'getElement':
									if(param == 'even') param = '2n+2'; else
									if(param == 'odd') param = '2n-1';									
									param = new RegExp('(?:(?:\\-|\\+)?(\\d*)(?:n))?((?:\\-|\\+)?\\d*)', 'img').exec(param);
									param = {a:param[1]-0, b:param[2]-0, n:(param[0].indexOf('n') != -1)};
									//alert('a:'+param.a+' b:'+param.b);
								case 'test':
									var result = [];
									for(var i=0 ; i<full.length ; i++)
										if( !!(res = this[data.fn](name, param, full[i])) )
										{
											result.push(full[i]);
											if(res == 2) break;
										}
									
									full = result;
								break;
							
								default:
									throw 'Undefined pseudo class: "'+name+'"';
								break;
							}							
						}
						this.expPseudo.lastIndex = 0;
					break;
					
					//attribute
					case '[':
						if(k==0){ this._parseTag('*', index); full = this.result[index]; }
						var result = [];
						var listAttrib = [];
						while( (attribute = this.expAttribute.exec(current)) )
							listAttrib.push(attribute);
						this.expAttribute.lastIndex = 0;
						for(var i=0 ; i<full.length ; i++)
						{
							var pass = true;
							for(var z=0, attribute; attribute = listAttrib[z] ; z++)
							{
								if( !(pass = this._attributeMatch(attribute[1], attribute[3], attribute[2], full[i])) )
									break;								
							}
							
							if(pass)
								result.push(full[i]);
						}
						full = result;
						
					break;
				
					case '>':
						this.cache.childSelector = !this.cache.childSelector;
					break;
				
					case '+':
						this.cache.adjacentSelector = !this.cache.adjacentSelector;
					break;
				
					case '~':
						this.cache.followingSelector = !this.cache.followingSelector;
					break;
				
					//tagName,*
					default:
						var result = [];
						for(var i=0 ; i<full.length ; i++)
						{
							var nodes = full[i].getElementsByTagName(current);
							for(var z=0, node; node = nodes[z] ; z++)
								result.push(node);
						}
						full = result;						
					break;
				}
			}
		}
		else
		{
			throw 'Wrong selector: "'+item+'"';
		}
		
		this.result[index] = full;
		delete full;
		delete chkClass;
		delete result;		
	},
	
	/**
	 * Pseudo kifejezést illeszt egy elementre és TRUE vagy FALSE-al tér vissza attól függően,
	 * hogy illeszkedik-e rá vagy sem
	 *
	 * @param String pse Pseudo kifejezés this.currentPseudo változóban található értékek
	 * @param String param Pseudo kifejezésnek adott paraméter
	 * @param HTMLElement elm Element amire illeszteni kel
	 * @return Booean TRUE ha illeszkedik, egyébként FALSE
	 */
	// TODO: tesztelni és optimalizálni
	_pseudoMatch: function(pse, param, elm)
	{
		switch(pse)
		{
			case 'not':
				if(param.slice(0,1) == '.')
					return !this._helper.match.className(elm, param.slice(1));
				else if(param.slice(0,1) == '#')
					return elm.id != param.slice(1);
				else
					return !this.match(elm, param);
			break;
		
			case 'enabled':
				return this._helper.match.chkAttrib(elm, 'disabled', false, '!=');
			break;
		
			case 'disabled':
			case 'checked':
			case 'readonly':
			case 'selected':
				return this._helper.match.chkAttrib(elm, pse, true, '==');
			break;
		
			case 'self':
				return true;
			break;
		}
		return false;
	},
	
	/**
	 * Megvizsgálja egy node attributumát hogy illeszkedik-e a megadott paramétereknek megfelelően.
	 * Kapcsolatok:
	 * 		- "=": egyenlő-e az adott attribute az adott értékkel
	 * 		- "~=": szóközzel elválasztott listában szerepel-e az adott érték
	 * 		- "!=": nem egyenlő-e az adott attribute az adott értékkel
	 * 		- "*=": az adott attribute értékében szerepel-e vlahol az adott érték
	 * 		- "|=": '-' jellel elválasztott listában szerepel-e az adott érték az első helyen
	 * 		- "$=": az adott érték az attributeum végén szerepel-e
	 * 		- "^=": az adott érték az attributeum elején szerepel-e
	 *
	 * @param String attrName Attribute neve
	 * @param String attrValue Attribute értéke, ha NULL akkor csak azt vizsgálja hogy létezik-e az attribute
	 * @param String rel Kapcsolat a megadott érték és a vizsgálandó között
	 * @param HTMLElement element Ezt az elementet vizsgélja
	 * @return Boolean TRUE ha átment a vizsgán, egyébként FALSE
	 */
	_attributeMatch: function(attrName, attrValue, rel, element)
	{
		if( (attr = element.getAttribute(attrName)) )
			switch(rel)
			{
				case '=':
					return attr == attrValue;
				break;
			
				case '~=':
					return (' '+attr+' ').indexOf(' '+attrValue+' ') != -1;
				break;
			
				case '!=':
					return attr != attrValue;
				break;
			
				case '*=':
					return attr.indexOf(attrValue) != -1;
				break;
			
				case '|=':
					return attr.split('-')[0] == attrValue;
				break;
			
				case '$=':
					return attr.slice(attr.length-attrValue.length) == attrValue;
				break;
			
				case '^=':
					return attr.indexOf(attrValue) == 0;
				break;
			
				default:
					if(!attrValue) return true;
				break;
			}
		
		return false;
	},
	
	/**
	 * Nodeokat vizsgálnak másik nodeokhoz viszonyítva
	 * 		- fist-child
	 * 		- last-child
	 * 		- only-child
	 * 		- nth-child
	 *
	 * @param String name Pseudo selector neve
	 * @param String param Pseudo selector paramétere
	 * @param HTMLElement element Az lement amire illeszteni kell a selectort
	 * @return Array találati lista
	 */
	_applyPseudo: function(name, param, element)
	{
		switch(name)
		{
			case 'first-child':
				var node = element;
				while( (node = node.previousSibling) )
					if(node.nodeType == 1) return 0;
				return 1;
			break;
		
			case 'last-child':
				var node = element;
				while( (node = node.nextSibling) )
					if(node.nodeType == 1) return 0;
				return 1;
			break;
		
			case 'only-child':
				return this._applyPseudo('last-child', null, element) && this._applyPseudo('first-child', null, element);
			break;
		
			// BUG: n+1,
			case 'nth-child':
				if( !element.nthIndex || this.refresh )
				{
					var c = 1;
					for(var node = element.previousSibling ; node ; node = node.previousSibling)
					{
						if(node.nodeType != 1) continue;
						if(node.nthIndex != undefined){ element.nthIndex = node.nthIndex + c; break; }
						c++;
					}
					if( !element.nthIndex )
						element.nthIndex = c;
				}
				var idx 	= element.nthIndex;
				var loop 	= param.a ? true : (!param.a && !param.b ? true : false);
				var a		= nth = param.a || 1;
				var b		= param.b || 0;
				var first 	= loop ? (b || a || 1) : b;
				
				if( !loop ){ if(idx == first) return 1; } else
				if( (idx - first) % nth == 0 ) return 1;
				
				return 0;
			break;
		
			default:
				return 0;
			break;
		}
		return 0;
	},	
	
	_helper:
	{
		match:
		{
			'className': function(e,p){ return (' '+e.className+' ').indexOf(' '+p+' ') != -1 },			
			'chkAttrib': function(elm, name, value, rel, attrVal)
			{
				if(elm.hasAttribute(name))
					return elm[name] == value || new Function('a', 'b', 'return a.toLowerCase() '+rel+' b.toLowerCase()')(elm.getAttribute(name), name);	
				return elm[name] == value;
			}
		},
        
        str:
        {
            trim: function(str){ return str.replace( /^\s+|\s+$/g, "" ); }
        }
	}
};

function $(selector, node, alwaysReturnArray, refresh)
{
    if( typeof node == 'undefined' || !node )
        node = document;
        
    if( typeof refresh != 'boolean' )
        refresh = false;
    return Selector.parse(node, selector, refresh, alwaysReturnArray);
}