// DMS2.0 :: Dust-Me Selectors
//***************************************************************************
// Copyright 2007 SitePoint Pty Ltd -- http://www.sitepoint.com/
// Written by brothercake -- http://www.brothercake.com/
// MIT License -- http://opensource.org/licenses/mit-license.php
//***************************************************************************



//dust-me selectors object
DustMeSelectors = new Object();

//define the running property
DustMeSelectors.running = false;



//when the chrome has loaded
window.addEventListener('load', function()
{
	//try and instantiate dom utilities, which comes with DOM Inspector
	//this will throw an exception if the inspector isn't available
	//in which case it won't be possible to show line numbers for CSS rules
	//(btw. I got this code, and the basis of code for working with it, from firebug)
	try
	{
		DustMeSelectors.domutils = Components.classes["@mozilla.org/inspector/dom-utils;1"]
			.getService(Components.interfaces['inIDOMUtils']);

	}
	catch (err)
	{
		DustMeSelectors.domutils = null;
	}

}, false);



//get all the selectors in all the stylesheets on the page we're viewing
DustMeSelectors.getAllSelectors = function(prefname, doc)
{
	//retrieve any saved selectors for this host, or create a new object
	var selectors = DustMeSelectors_tools.getDataObject(prefname, 'selectors', {});

	//save the document object
	DustMeSelectors.document = doc;

	//get CSS selectors from all the stylesheets in the browser document's styleSheets collection
	//passing the existing object of selectors, and the stylesheets collection
	//(we're passing the stylesheets collection here, rather than inside getSelectors itself
	// so that getSelectors can be recursive)
	selectors = DustMeSelectors.getSelectors(selectors, DustMeSelectors.document.styleSheets);

	//pass the selectors object to finalize selectors
	DustMeSelectors.finalizeSelectors(selectors, prefname);
};



//finalize the selectors object by waiting for remote stylesheets to finish loading
//and then grabbing rules from any IE conditional stylesheets
DustMeSelectors.finalizeSelectors = function(selectors, prefname)
{
	//look for selectors in stylesheets included using IE conditional comments
	DustMeSelectors.getConditionalSelectors(selectors);

	//now start a setInterval, polling for the waiting count being zero
	//so we know we're not waiting for selectors from remote stylesheets
	var waitingtimer = window.setInterval(function()
	{
		//if we're not still waiting
		if(DustMeSelectors.waiting == 0)
		{
			//clear the interval and proceed
			window.clearInterval(waitingtimer);

			//pass the selectors object to evaluate selectors
			DustMeSelectors.evaluateSelectors(selectors, prefname);
		}

	//polling interval is short enough to be basically unnoticeable
	//(unnoticeable in terms of the latency of an HTTP request, anyway!)
	//but long enough not to be a CPU hog
	}, 250);
};



//evaluate the final object of CSS selectors
//to see if they correspond to present HTML
//and then output the final information to firebug
DustMeSelectors.evaluateSelectors = function(selectors, prefname)
{
	//we're going to pass each selector to base2's matchAll method
	//(I developed originally with cssQuery, but base2 is literally twice as fast!)
	//using timeout-based iteration instead of a simple for loop
	//which prevents the browser freezing up from the heavy activity
	//because it passes the processing into a separate thread

	//get the object of used selectors, or create an empty object
	var used = DustMeSelectors_tools.getDataObject(prefname, 'used', {});

	//so begin by creating an array of selector group keys
	//  and along the way, re-key each selector group
	//  to cater for items that have been removed when using saved selectors
	//  which we need to do since we'll be iterating with a timer and a manual increment
	//  rather than using a conventional for loop
	//and also add members to the used object if necessary
	var groupkeys = [];
	for(var i in selectors)
	{
		if(!selectors.hasOwnProperty(i) || typeof selectors[i] == 'string') { continue; }
		groupkeys.push(i);
		selectors[i] = DustMeSelectors.rekey(selectors[i]);
		if(typeof used[i] == 'undefined') { used[i] = {}; }
	}

	//if we don't have any selector groups
	//proceed straight to save and output
	if(groupkeys.length == 0)
	{
			//if we have a spider object
			//pass the selectors object back to the spider's save and continue function
			if(typeof DustMeSelectors_spider != 'undefined')
			{
				DustMeSelectors_spider.saveAndContinue(selectors, used);
			}

			//otherwise save and output in the normal way
			else
			{
				DustMeSelectors_browser.saveAndOutput(selectors, used);
			}

			//stop the interval
			window.clearInterval(processtimer);

			//and we're done here
			return;
	}

	//create a key variable for tracking progress through the groupkeys array
	//and values for storing the current key
	//and the current and previous index of the rule within each object
	var key = 0, rulescount = 0, current_key = groupkeys[key],
		previous_key = null, current_index = 0, previous_index = -1;

	//get the rule testing preferences
	var accountforhacks = DustMeSelectors_preferences.getPreference('boolean', 'accountforhacks');
	var includeelements = DustMeSelectors_preferences.getPreference('boolean', 'includeelements');

	//now create an interval to do the processing
	var processtimer = window.setInterval(function()
	{
		//if the running flag is false then a stop command has been issued
		if(!DustMeSelectors.running)
		{
			//stop the interval
			window.clearInterval(processtimer);
		}

		//othewise if the current index and previous index are different
		//then we're ready to proceed to processing the next selector
		else if(current_index != previous_index)
		{
			//set the previous index to the current index value
			//to indicate that we're mid process
			previous_index = current_index;

			//if we've reached the end of this selectors object (or it's empty)
			if(typeof selectors[current_key][current_index] == 'undefined')
			{
				//if we have a spider object, update the spider status label
				if(typeof DustMeSelectors_spider != 'undefined')
				{
					DustMeSelectors_spider.updateStatusLabel(null, rulescount, key + 1);
				}
				//otherwise update the browser status label in the normal way
				else
				{
					DustMeSelectors_browser.updateStatusLabel(null, rulescount, key + 1);
				}

				//update values to point to the next selectors object
				current_key = groupkeys[++key];
				current_index = 0;
				previous_index = -1;

				//if we've run out of selector objects then we've finished processing
				//so reset the timer, then save and output the results of the evaluation
				if(typeof selectors[current_key] == 'undefined')
				{
					//if we have a spider object
					//pass the selectors object back to the spider's save and continue function
					if(typeof DustMeSelectors_spider != 'undefined')
					{
						DustMeSelectors_spider.saveAndContinue(selectors, used);
					}

					//otherwise save and output in the normal way
					else
					{
						DustMeSelectors_browser.saveAndOutput(selectors, used);
					}

					//stop the interval
					window.clearInterval(processtimer);
				}
			}

			//or if this selector is marked as deleted, just skip over it
			else if(selectors[current_key][current_index].indexOf('{D}') != -1)
			{
				//increase the current index value
				current_index ++;

				//if we have a spider object, update the spider status label
				if(typeof DustMeSelectors_spider != 'undefined')
				{
					DustMeSelectors_spider.updateStatusLabel(null, rulescount, key + 1);
				}
				//otherwise update the browser status label in the normal way
				else
				{
					DustMeSelectors_browser.updateStatusLabel(null, rulescount, key + 1);
				}
			}

			//otherwise we still have selectors to check
			else
			{
				//we have to do this bit using try..catch to catch base2 exceptions
				//when the input selector is not a valid selector (base2-dom.js, line 779)
				try
				{
					//delete any existing untestable message
					selectors[current_key][current_index] = selectors[current_key][current_index]
						.replace(DustMeSelectors_tools.bundle.getString('error.untestable') + ' ', '');

					//for testing the selector, parse out line numbers,
					//and remove pseudo classes, and selected pseudo elements
					//(those that don't have a source-element equivalent or can't be tested)
					var testselector = selectors[current_key][current_index]
						.replace(/\{[0-9]+\}/g, '')
						.replace(/([\:]{1,2}(active|focus|hover|lang|link|visited|first\-letter|before|after|target|enabled|disabled|checked))/g, '');

					//convert some common css hacks, if that preference is true
					if(accountforhacks)
					{
						testselector = testselector.replace(/^(([\s]*\*[\s]*)|([\s]*\*[\s]*((\:first\-child)[\s]*)?\+[\s]*))(html)/,'html');
					}

					//evaluate this selector and delete it if it's in use
					//or delete it anyway if it's a simple element selector
					//and the preference include element selectors is false
					//but before deleting, copy it to used selectors object
					if(base2.DOM.Document.matchAll(DustMeSelectors.document, testselector).length > 0
						|| (!includeelements && /^[a-z1-6]+$/i.test(testselector)))
					{
						used[current_key][current_index] = selectors[current_key][current_index];
						delete selectors[current_key][current_index];
					}

					//otherwise this rule is redundent
					else
					{
						if(typeof used[current_key][current_index] != 'undefined')
						{
							delete used[current_key][current_index];
						}

						//increase the total rule count for status display
						++rulescount;
					}
				}

				//if we catch an exception
				catch(err)
				{
						//increase the total rule count for status display
						++rulescount;

						//add a message to the selector to denote that it was untestable
						selectors[current_key][current_index] = DustMeSelectors_tools.bundle.getString('error.untestable')
							+ ' ' + selectors[current_key][current_index];
				}

				//in either case
				finally
				{
					//increase the current index value
					current_index ++;

					//if we have a spider object, update the spider status label
					if(typeof DustMeSelectors_spider != 'undefined')
					{
						DustMeSelectors_spider.updateStatusLabel(null, rulescount, key + 1);
					}
					//otherwise update the browser status label in the normal way
					else
					{
						DustMeSelectors_browser.updateStatusLabel(null, rulescount, key + 1);
					}
				}
			}
		}

		//if they current and previous index are the same then do nothing this iteration
		//because we're still processing the selector from the last iteration
		//the timer will continue to the next iteration
		//where it will check again, and so forth, until we're done

	//we only want a very short timeout so that processing isn't slowed too much
	//but not so short that the timer itself is a CPU hog
	}, 10);
};



//get the CSS selectors from a collection of stylesheets
DustMeSelectors.getSelectors = function(selectors, stylesheets, explicitindex)
{
	//for each stylesheet
	for(var i=0; i<stylesheets.length; i++)
	{
		//if we have an explicit index argument
		//use that as the group index
		if(typeof explicitindex != 'undefined')
		{
			var index = explicitindex;
		}

		//or if we don't, but this is a <style> block, we're not interested in its rules
		//but we are interested in any @import statements it contains
		else if(stylesheets[i].ownerNode != null
			&& stylesheets[i].ownerNode.nodeName.toLowerCase() == 'style')
		{
			//get the collection of rules for this stylesheet
			var rules = stylesheets[i].cssRules;

			//iterate through the rules
			for(var j=0; j<rules.length; j++)
			{
				//if this is an @import rule
				if(typeof rules.item(j).href != 'undefined')
				{
					//get the stylesheet it refers to and pass it back to getSelectors
					//(inside an array, because the stylesheets argument is expecting a collection)
					//we need to be recursive here in case the imported stylesheet
					//also contains another @import rule
					selectors = DustMeSelectors.getSelectors(selectors, [rules.item(j).styleSheet]);
				}
			}

			//and continue to the next iteration
			continue;
		}

		//otherwise get the href of the stylesheet to use as index
		else
		{
			index = stylesheets[i].href;

			//if the ignore query paramter is true, remove any query string
			if(DustMeSelectors_preferences.getPreference('boolean', 'ignorequery'))
			{
				index = index.split('?')[0];
			}
		}

		//don't include chrome stylesheets
		if(index.indexOf('chrome://') != -1) { continue; }

		//if we already have a group for this index
		//but its first member is a string error message
		//delete this group so that it gets checked again
		//errors like this will be HTTP errors like 404, or Connection Failure
		//so we want to check them every time, in case they've become available now
		if(typeof selectors[index] == 'string')
		{
			delete selectors[index];
		}

		//if we don't already have an array of selectors for this index
		//or we do, but this index was passed in explicitly
		if(typeof selectors[index] == 'undefined' || typeof explicitindex != 'undefined')
		{
			//if this group already existed, store it as it is now
			if(typeof selectors[index] != 'undefined')
			{
				var oldselectors = selectors[index];
			}

			//create a new object with this index
			selectors[index] = {};

			//create a counter for numbering the properties
			//(because selectors[index] is an object with numbered properties, not an array)
			var n = 0;

			//get the collection of rules for this stylesheet
			//since we're in the chrome context this will work
			//for remote stylesheets as well as local stylesheets
			try
			{
				rules = stylesheets[i].cssRules;
			}
			catch(err)
			{
				var str = 'Stylesheet URI = ' + index + '\n\n';
				for(var r in err)
				{
					if(r.toUpperCase() == r) { continue; }
					str += r + ': ' + err[r] + '\n';
				}
				//alert(str);
				
				var dom = 'DOM CSS, FOR URI = ' + index + '\n\n';
				dom += 'ownerRule: ' + stylesheets[i].ownerRule + '\n';
				dom += 'ownerNode: ' + stylesheets[i].ownerNode + '\n';
			}

			//if we don't have an explicit index and this stylesheet has no rules
			//(we have to check for explicit index to avoid unwanted recursion)
			if(typeof explicitindex == 'undefined' && rules.length == 0)
			{
				//it may not exist at all, so to check that, try to load it as a remote stylesheet
				//if it turns out not to exist, it will be logged as Not Found
				//(or indeed any other HTTP error that prevented it from loading)
				DustMeSelectors.getRemoteSelectors(selectors, index);

				//and then continue to the next stylesheet
				continue;
			}

			//if we're still going then we have a set of CSS rules
			//so iterate through the rules we have
			for(j=0; j<rules.length; j++)
			{
				//if this is an @charset rule, just ignore it and continue
				if(typeof rules.item(j).encoding != 'undefined')
				{
					continue;
				}

				//if this is an @import rule
				if(typeof rules.item(j).href != 'undefined')
				{
					//get the stylesheet it refers to and pass it back to getSelectors
					//(inside an array, because the stylesheets argument is expecting a collection)
					//we need to be recursive here in case the imported stylesheet
					//also contains another @import rule
					selectors = DustMeSelectors.getSelectors(selectors, [rules.item(j).styleSheet]);
				}

				//or if this is an @media rule
				else if(typeof rules.item(j).media != 'undefined')
				{
					//get the rules collection inside it
					//(this won't handle nested @media rules, but that shouldn't matter
					//because nesting @media rules is not allowed)
					var subrules = rules.item(j).cssRules;

					//for each of those rules
					for(var k=0; k<subrules.length; k++)
					{
						//parse rule selector text for comma delimiters
						//passing the existing selectors sub-object and n, so the data can be saved into it
						//and also the oldselectors object
						//then save the return values back to this selectors object and n
						//so we end up with the same object but with these new values
						var rvalues = DustMeSelectors.parseSelectorText(subrules.item(k), selectors[index], n);
						selectors[index] = rvalues.selectors;
						n = rvalues.n;
					}
				}

				//or if it's a regular style rule
				else if(typeof rules.item(j).selectorText != 'undefined')
				{
					//parse rule selector and save back to selectors object
					var rvalues = DustMeSelectors.parseSelectorText(rules.item(j), selectors[index], n);
					selectors[index] = rvalues.selectors;
					n = rvalues.n;
				}

				//for anything else (such as @page, @font-face and @namespace rules)
				//just ignore it and continue to the next iteration
				else
				{
					continue;
				}
			}

			//if we have oldselectors stored, we need to compare
			//and mark any selectors from this object
			//that were previously marked as deleted
			if(typeof oldselectors != 'undefined')
			{
				for(i in oldselectors)
				{
					if(!oldselectors.hasOwnProperty(i)) { continue; }
					for(j in selectors[index])
					{
						if(!selectors[index].hasOwnProperty(j)) { continue; }
						if(oldselectors[i].indexOf('{D}') != -1
							&& (selectors[index][j]
								== oldselectors[i].replace('{D}', '')))
						{
							selectors[index][j] += '{D}';
						}
					}
				}
			}
		}
	}

	//retun the complete selectors object
	return selectors;
};



//get the CSS selectors from a single remote stylesheet
DustMeSelectors.getRemoteSelectors = function(inselectors, index)
{
	//add to the waiting count
	DustMeSelectors.waiting++;

	//create a new XMLHttpRequest to load the stylesheet
	//this isn't limited by same-origin policy because we're in the chrome scope
	var request = new XMLHttpRequest();

	//define the readystatechange function, and when it completes
	request.onreadystatechange = function()
	{
		if(request.readyState == 4)
		{
			//try to read its status
			try
			{
				//if the request succeeds
				if(/(200|304)/.test(request.status.toString()))
				{
					//create a new style node and write the CSS text into it
					//then append the style node to the head section
					var stylenode = DustMeSelectors.document.createElement('style');
					stylenode.setAttribute('type', 'text/css');
					stylenode.innerHTML = request.responseText;
					DustMeSelectors.document.getElementsByTagName('head')[0].appendChild(stylenode);

					//now get a reference to that style node from the document.styleSheets collection
					//it will be the last item in the collection, because we just added it at the end
					var newsheet = DustMeSelectors.document.styleSheets.item(DustMeSelectors.document.styleSheets.length - 1);

					//immediately set the disabled property so it has no effect
					newsheet.disabled = true;

					//if the ignore query paramter is true, remove any query string
					if(DustMeSelectors_preferences.getPreference('boolean', 'ignorequery'))
					{
						index = index.split('?')[0];
					}

					//now that we have that stylesheet reference
					//we can simply pass it back to getSelectors to get its rules :-D
					//we also have to pass this index explictly, so that it's stored in the right object group
					selectors = DustMeSelectors.getSelectors(inselectors, [newsheet], index);

					//then delete the stylesheet node, for cleanup
					newsheet.ownerNode.parentNode.removeChild(newsheet.ownerNode);
				}

				//otherwise if it fails
				else
				{
					//log the error in the selectors object
					inselectors[index] = DustMeSelectors_tools.bundle.getString('view.error').replace('%1', request.statusText);
					selectors = inselectors;
				}
			}

			//if we fail to read the status
			catch(err)
			{
				//** first err.name is what condition is for,
				//** but the second error, I don't know why that happens, I only know the circumstances
				//name = NS_ERROR_NOT_AVAILABLE				-> when net is disconnected for remote stylesheet
				//name = NS_ERROR_DOM_INVALID_ACCESS_ERR	-> when an IE stylesheet contains an @import statement

				//log the error in the selectors object as a connection failure
				inselectors[index] = DustMeSelectors_tools.bundle.getString('view.error').replace('%1',
					DustMeSelectors_tools.bundle.getString('view.noconnect'));
				selectors = inselectors;
			}

			//in any case
			finally
			{
				//subtract from the waiting count
				DustMeSelectors.waiting--;
			}
		}
	}

	//make an asynchronous request for the given URI
	request.open('GET', index, true);
	request.send(null);
};



//get selectors from stylesheets included using IE conditional comments
DustMeSelectors.getConditionalSelectors = function(selectors)
{
	//create a regex for testing a comment to see if it's an IE conditional comment
	var cc = /((\[(if )((lt|gt)[e]?[ ])?(IE)([ ]?[0-9](\.[0-9])?)?\]>)((.|\n|\r)*)(<\!\[endif\]))/i;

	//create an array for storing any stylesheets we find
	var ieStylesheets = [];

	//then get and iterate through all child nodes of the head section
	//(if we have one - we might be checking a non-HTML document)
	var head = DustMeSelectors.document.getElementsByTagName('head');
	if(head.length == 0) { return; }
	else { head = head.item(0); }
	var nodes = head.childNodes;
	for(var i=0; i<nodes.length; i++)
	{
		//if this node is a comment and it matches the conditional comment regex
		if(nodes[i].nodeName == '#comment' && cc.test(nodes[i].nodeValue))
		{
			//extract what's inside the conditional comment,
			//trimmed of leading and trailing whitespace
			var content = nodes[i].nodeValue.replace(cc, '$9').replace(/^[\s]+((.|\n|\r)*)[\s]+/i, '$1');

			//split the content by <link fragments
			var fragments = content.split('<link');
			
			//if we have fewer than 2 fragments there are no stylesheets in this comment
			//it actually only needs to be equal to 0, because there will either be 0 or 2+
			//but there might as well be a safety condition in case something screws up and there's 1
			if(fragments.length < 2) { continue; }

			//now remove the first one will, which is just junk, and possibly empty junk
			fragments.splice(0, 1);
			
			//now iterate through the fragments looking for a stylesheet
			//which we do by testing its rel value, 
			//then extracting the href from any that are a stylesheet
			var paths = [];
			for(var f=0; f<fragments.length; f++)
			{
				if(/(rel=\"(alternate )?stylesheet\")/.test(fragments[f])) 
				{ 
					var href = fragments[f].split('href="');
					if(href.length == 1) { continue; }
					paths.push(href[1].split('"')[0]);
				}
			}
			
			//if we don't have any paths then we found no stylesheets
			if(paths.length == 0) { continue; }

			//now for each of the resulting paths
			for(var j=0; j<paths.length; j++)
			{
				//qualify the href into a complete URI
				//then get the selectors from it as a remote stylesheet
				//(which caters for the latency of it loading)
				DustMeSelectors.getRemoteSelectors(
					selectors, 
					DustMeSelectors_tools.qualifyHREF(paths[j])
					);
			}
		}
	}
};



//parse rule selector text for comma delimiters
//trim and add each individual selector to the selectors object
DustMeSelectors.parseSelectorText = function(rule, selectors, n, oldselectors)
{
	//get the selector text and split it by any comma delimiters
	var selector = rule.selectorText.split(',');

	//and add each individual selector to the selectors group
	for(var i=0; i<selector.length; i++)
	{
		//trim any leading whitespace
		selector[i] = selector[i].replace(/^[\s]*/, '');

		//remove any namespace tokens
		selector[i] = selector[i].replace(/(\*\|)/g, '');

		//if dom utilities is available add the line number
		//in a format that won't happen in CSS rule text
		//that we can parse out or split as necssary
		if(DustMeSelectors.domutils)
		{
			if (rule instanceof CSSStyleRule)
			{
				selector[i] += '{'
					+ DustMeSelectors.domutils.getRuleLine(rule)
					+ '}';
			}
		}

		//add the final selector to the selectors group
		selectors[n++] = selector[i];
	}

	//return the selectors object and n
	return { 'selectors' : selectors, 'n' : n };
};



//re-key an object indexed by numeric keys
//input might be a string so we have to test for that
//and if so pass it through unchanged
DustMeSelectors.rekey = function(obj)
{
	var n = 0, newobj = {};
	if(typeof obj == 'object')
	{
		for(var i in obj)
		{
			if(!obj.hasOwnProperty(i)) { continue; }
			if(/^[0-9]+$/.test(i))
			{
				newobj[n++] = obj[i];
			}
		}
		return newobj;
	}
	else
	{
		return obj;
	}
};

