// 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
//***************************************************************************

/* NOTE:
this script is included by browser.xul and spider.xul
however the dialogs are child windows of the main browser
so each of those instances expects a different object heirarchy

this script requires the presence of preferences.js and filesystem.js
*/

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


//when the chrome has loaded
window.addEventListener('load', function()
{
	//load the properties bundle
	DustMeSelectors_tools.bundle = document.getElementById('dms-bundle');

	//initialize the data saving options
	DustMeSelectors_tools.initialiseDataSavingOptions();

}, false);




//initialize the data saving options
DustMeSelectors_tools.initialiseDataSavingOptions = function()
{
	//get the dustmeselectors data directory
	//creating both it and the hostsindex file if they don't exist
	DustMeSelectors_filesystem.getDataFolder();

	//get the hosts index array
	this.hostsindex = this.getHostsIndex();

	//if we have a browser object
	if(typeof DustMeSelectors_browser != 'undefined')
	{
		//if the hosts index array is 0 length,
		if(this.hostsindex.length == 0)
		{
			//disable the view selectors menu items
			//and the parent menu of the clear selectors items
			DustMeSelectors_browser.setUIState('view-clearmenu', true);
		}

		//otherwise we have hosts
		else
		{
			//enable those menu items
			DustMeSelectors_browser.setUIState('view-clearmenu', false);
		}
	}

	//if we have an open view dialog
	//repopulate the hosts index selector, with no preselection
	//then disable the save and clear buttons
	//we do this so we don't have to forcibly clear the view
	//but because we can't work with the visible data anymore
	if(this.viewdialog && !this.viewdialog.closed)
	{
		var viewdoc = this.viewdialog.document;
		viewdoc.documentElement.getButton('extra1').setAttribute('disabled', true);
		viewdoc.documentElement.getButton('extra2').setAttribute('disabled', true);
		this.populateHostsIndexMenu(viewdoc, false);
	}
};



//get the hosts index, or create an empty array if it doesn't exist
DustMeSelectors_tools.getHostsIndex = function()
{
	//if hosts index create a new array
	//otherwise split the data to create an array from it
	var index = DustMeSelectors_filesystem.getFromFile('hostsindex');
	if(index == '')
	{
		index = [];
	}
	else
	{
		index = index.split(',');
	}

	//return the hosts index array
	return index;
};



//update the hosts index, either adding new hosts if they don't exist already
//or deleting exists hosts from the index
DustMeSelectors_tools.updateHostsIndex = function(inputhost, addthis)
{
	//iterate through the existing hosts in the index
	//to find out if the input host already exists
	//and if remove it if addthis is false
	var exists = false;
	for(var i=0; i<this.hostsindex.length; i++)
	{
		if(this.hostsindex[i] == inputhost)
		{
			exists = true;
			if(!addthis)
			{
				this.hostsindex.splice(i, 1);
			}
			break;
		}
	}

	//if we're adding and the host doesn't exist
	//add this host to the array
	if(addthis && !exists)
	{
		this.hostsindex.push(inputhost);
	}

	//save the joined array back to the hosts index file
	DustMeSelectors_filesystem.dumpToFile('hostsindex', this.hostsindex.join(','));

	//if we have a browser object
	if(typeof DustMeSelectors_browser != 'undefined')
	{
		//enable or disable the view menu items
		//and the parent menu of the clear selectors items
		//depending on wheher the hosts array has any length
		DustMeSelectors_browser.setUIState('view-clearmenu', this.hostsindex.length == 0);
	}
};




//get the preference name from location
DustMeSelectors_tools.getSelectorsDataPreferenceName = function(loc)
{
	//start with the default, global preference name
	var prefname = 'selectors';

	//if the save per host preference is true
	//we need to further process the location to get host and port information
	//depending on the values of the data saving preferences
	if(DustMeSelectors_preferences.getPreference('boolean', 'saveperhost'))
	{
		//get ignorewww and ignoreports preferences
		var ignorewww = DustMeSelectors_preferences.getPreference('boolean', 'ignorewww');
		var ignoreports = DustMeSelectors_preferences.getPreference('boolean', 'ignoreports');

		//if the input location is a string we need to parse it manually
		if(typeof loc == 'string')
		{
			//remove the protocol and split by slashes, saving the first part
			//which will give us the host including any port
			loc = loc.replace(/[a-z]+\:\/\//, '').split('/')[0];

			//if we're ignoring www, and the host starts with www, remove it
			if(ignorewww && /^(www\.)/i.test(loc))
			{
				prefname += '.' + loc.substr(4);
			}

			//otherwise add the host unedited
			else
			{
				prefname += '.' + loc;
			}

			//if we're merging ports, remove any port number
			if(ignoreports)
			{
				prefname = prefname.replace(/(\:[0-9]+)$/, '');
			}

			//otherwise convert the colon to a hash
			//so we can identify it later
			else
			{
				prefname = prefname.replace(/\:([0-9]+)$/, '#$1');
			}
		}

		//otherwise it will be a location object
		else
		{
			//try to get the hostname from input location
			//and only parse it if that succeeeds
			//we need to do this in case we don't have a hostname
			//for example, when the location is about:config
			try
			{
				var hostname = loc.hostname;

				//if that value is defined,
				if(typeof hostname != 'undefined')
				{
					//if it's empty then assume this is a local file
					//and add the localfiles token to the preference name
					if(hostname == '')
					{
						prefname += '.@local';
					}

					//otherwise use the host data for the preference name
					else
					{
						//if we're ignoring www, and the hostname starts with www, remove it
						if(ignorewww && /^(www\.)/i.test(hostname))
						{
							prefname += '.' + hostname.substr(4);
						}

						//otherwise add the hostname unedited
						else
						{
							prefname += '.' + hostname;
						}

						//add the port number if we're not merging ports
						//using a hash as the delimiter, so we can identify it later
						if(!ignoreports)
						{
							var port = loc.port;
							if(port != '')
							{
								prefname += '#' + port;
							}
						}
					}
				}
			}

			//if we fail to get the host, reset to the global name
			catch(err)
			{
				prefname = 'selectors';
			}
		}
	}

	//return the preference name
	return prefname;
};



//get and decode saved data object for (from JSON to an object) "selectors", "used" or "log"
//or return the default value if there isn't any
DustMeSelectors_tools.getDataObject = function(prefname, datatype, def)
{
	//if data is empty return the default
	//otherwise decode the data and return that object
	var data = DustMeSelectors_filesystem.getFromFile(prefname.replace('selectors', datatype) + '.json');
	if(data == '' || data == '{}')
	{
		return def;
	}
	else
	{
		return DustMeSelectors_tools.String_parseJSON(data);
	}
};




//qualify an HREF to form a complete URI
DustMeSelectors_tools.qualifyHREF = function(href)
{
	//use spider or regular browser document, as applicable
	if(typeof DustMeSelectors_spider != 'undefined')
	{
		var doc = DustMeSelectors_spider.browser.contentDocument;
	}
	else
	{
		doc = DustMeSelectors.document;
	}

	//get the document location href
	var here = doc.location.href;
	
	//look for a base element to use instead
	var bases = doc.getElementsByTagName('base');
	if(bases.length > 0) 
	{ 
		var basehref = bases[0].getAttribute('href'); 
		if(basehref && basehref != '')
		{
			here = basehref;
		}
	}
	
	//if the context argument is present and non-empty string, use that instead
	if(typeof context == 'string' && context != '')
	{
		here = context;
	}
	
	//extract the protocol, host and path
	//and create a location object with the data
	var parts = here.replace('//', '/').split('/');
	var loc = {
		'protocol' : parts[0], 
		'host' : parts[1]
		}
	parts.splice(0, 2);
	loc.pathname = '/' + parts.join('/');

	//build a base URI from the protocol plus host (which includes port if applicable)
	var uri = loc.protocol + '//' + loc.host;

	//if the input path is relative-from-here
	//just delete the ./ token to make it relative
	if(/^(\.\/)([^\/]?)/.test(href))
	{
		href = href.replace(/^(\.\/)([^\/]?)/, '$2');
	}

	//if the input href is already qualified, copy it unchanged
	if(/^([a-z]+)\:\/\//.test(href))
	{
		uri = href;
	}

	//or if the input href begins with a leading slash, then it's base relative
	//so just add the input href to the base URI
	else if(href.substr(0, 1) == '/')
	{
		uri += href;
	}

	//or if it's an up-reference we need to compute the path
	else if(/^((\.\.\/)+)([^\/].*$)/.test(href))
	{
		//get the last part of the path, minus up-references
		var lastpath = href.match(/^((\.\.\/)+)([^\/].*$)/);
		lastpath = lastpath[lastpath.length - 1];

		//count the number of up-references
		var references = href.split('../').length - 1;

		//get the path parts and delete the last one (this page or directory)
		var parts = loc.pathname.split('/');
		parts = parts.splice(0, parts.length - 1);

		//for each of the up-references, delete the last part of the path
		for(var i=0; i<references; i++)
		{
			parts = parts.splice(0, parts.length - 1);
		}

		//now rebuild the path
		var path = '';
		for(i=0; i<parts.length; i++)
		{
			if(parts[i] != '')
			{
				path += '/' + parts[i];
			}
		}
		path += '/';

		//and add the last part of the path
		path += lastpath;

		//then add the path and input href to the base URI
		uri += path;
	}

	//otherwise it's a relative path,
	else
	{
		//calculate the path to this directory
		path = '';
		parts = loc.pathname.split('/');
		parts = parts.splice(0, parts.length - 1);
		for(var i=0; i<parts.length; i++)
		{
			if(parts[i] != '')
			{
				path += '/' + parts[i];
			}
		}
		path += '/';

		//then add the path and input href to the base URI
		uri += path + href;
	}

	//return the final uri
	return uri;
};





//remove duplicate items from an array
//by building a new array that rejects non-unique values
DustMeSelectors_tools.arrayUnique = function(ary)
{
	var newary = [];
	for(var i=0; i<ary.length; i++)
	{
		if(!this.arrayContains(newary, ary[i]))
		{
			newary.push(ary[i]);
		}
	}
	return newary;
};

//check whether an array contains a particular value
DustMeSelectors_tools.arrayContains = function(ary, val)
{
	for(var i=0; i<ary.length; i++)
	{
		if(ary[i] == val) { return true; }
	}
	return false;
};




//save the position and size properties of a dialog
DustMeSelectors_tools.saveDialogProps = function(dialog, name)
{
	//if the remember dialog size preference is true
	if(DustMeSelectors_preferences.getPreference('boolean', 'remember'))
	{
		//create a properties string of the dimensions and position
		//in the order: width, height, x, y
		var props = dialog.outerWidth + ','
				 + dialog.outerHeight + ','
				 + dialog.screenX + ','
				 + dialog.screenY;

		//set the data to preferences
		DustMeSelectors_preferences.setPreference('string', 'dialogs.' + name, props);
	}
};

//get and apply stored dialog properties
DustMeSelectors_tools.applyDialogProps = function(dialog, name)
{
	//if the remember dialog size preference is true
	if(DustMeSelectors_preferences.getPreference('boolean', 'remember'))
	{
		//look for saved properties for this dialog
		var props = DustMeSelectors_preferences.getPreference('string', 'dialogs.' + name);

		//if we have data, parse it and apply the values to the dialog
		if(props !== '')
		{
			props = props.split(',');
			dialog.resizeTo(parseInt(props[0], 10), parseInt(props[1], 10));
			dialog.moveTo(parseInt(props[2], 10), parseInt(props[3], 10));
		}
	}
};



//handle a content document mutation event
//for when we have the automation set to run again when DOM changes
DustMeSelectors_tools.handleContentMutation = function(relatednode)
{
	//use a buffer so we don't get swamped if lots all fire at once
	//this will start a timer, and clear and restart it if another fires
	//hence only one will fire at the end of a sequence
	if(typeof DustMeSelectors_tools.buffer == 'undefined')
	{
		DustMeSelectors_tools.buffer = null;
	}
	if(DustMeSelectors_tools.buffer !== null)
	{
		window.clearTimeout(DustMeSelectors_tools.buffer);
		DustMeSelectors_tools.buffer = null;
	}
	DustMeSelectors_tools.buffer = window.setTimeout(function()
	{
		//ignore insertions into the head, which might be our own test stylesheets
		if(relatednode.nodeName.toLowerCase() != 'head')
		{
			//if we have a browser object, stop and re-start using its methods
			if(typeof DustMeSelectors_browser != 'undefined')
			{
				DustMeSelectors_browser.restart();
			}

			//if we have a spider object, stop and re-start using its methods
			else if(typeof DustMeSelectors_spider != 'undefind')
			{
				DustMeSelectors_spider.restart();
			}

			//reset the buffer
			window.clearTimeout(DustMeSelectors_tools.buffer);
			DustMeSelectors_tools.buffer = null;
		}

	}, 300);
};



//JSON functions based on the code at http://www.json.org/json.js
//but edited to only include what's needed by this script (objects and strings)
//and so that it uses regular functions instead of prototypes
//because prototypes of built-ins in the chrome scope pollute the content document scope
//and this can create problems for other scripts (eg. gmail freezes up)
DustMeSelectors_tools.charmatrix = {
	'\b': '\\b',
	'\t': '\\t',
	'\n': '\\n',
	'\f': '\\f',
	'\r': '\\r',
	'"' : '\\"',
	'\\': '\\\\'
	};

DustMeSelectors_tools.Object_toJSONString = function(obj)
{
	var a = [], k, v;
	for(k in obj)
	{
		if(!obj.hasOwnProperty(k)) { continue; }
		v = obj[k];
		if(v)
		{
			if(typeof v == 'object')
			{
				a.push(DustMeSelectors_tools.String_toJSONString(k) + ':' + DustMeSelectors_tools.Object_toJSONString(v));
			}
			else if(typeof v == 'string')
			{
				a.push(DustMeSelectors_tools.String_toJSONString(k) + ':' + DustMeSelectors_tools.String_toJSONString(v));
			}
		}
		else
		{
			a.push(DustMeSelectors_tools.String_toJSONString(k) + ':null');
		}
	}
	return '{' + a.join(',') + '}';
};

DustMeSelectors_tools.String_toJSONString = function(str)
{
	if(/["\\\x00-\x1f]/.test(str))
	{
		return '"' + str.replace(/([\x00-\x1f\\"])/g, function (a, b)
		{
			var c = DustMeSelectors_tools.charmatrix[b];
			if (c)
			{
				return c;
			}
			c = b.charCodeAt();
			return '\\u00' + Math.floor(c / 16).toString(16) + (c % 16).toString(16);
		}) + '"';
	}
	return '"' + str + '"';
};

DustMeSelectors_tools.String_parseJSON = function(str)
{
	var j;
	function walk(k, v)
	{
		var i;
		if (v && typeof v === 'object')
		{
			for (i in v)
			{
				if (v.hasOwnProperty(i))
				{
					v[i] = walk(i, v[i]);
				}
			}
		}
		return filter(k, v);
	}
	if(/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/.test(str.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '')))
	{
		j = eval('(' + str + ')');
		return j;
	}
	throw new SyntaxError(DustMeSelectors_tools.bundle.getString('error.json'));
};


