// 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, preferences.xul and spider.xul
however the dialogs are child windows of the main browser
so each of those instances expects a different window heirarchy
*/


//dust-me selectors filesystem object
//much of this is based on the code snippets at
//http://developer.mozilla.org/en/docs/Code_snippets:File_I/O
//http://developer.mozilla.org/en/docs/XUL_Tutorial:Open_and_Save_Dialogs
DustMeSelectors_filesystem = new Object();



//get the dustmeselectors-data folder inside the profile directory
//either creating it, or getting the path if it exists already
//set permissions at 600 to limit read/write access to the current user
DustMeSelectors_filesystem.getDataFolder = function()
{
	//get the datafolder path preference
	var dirpath = DustMeSelectors_preferences.getPreference('string', 'datafolder');

	//if we datafolder preference is empty
	if(dirpath == '')
	{
		//get the profile data directory
		var datafolder = Components.classes["@mozilla.org/file/directory_service;1"]
			.getService(Components.interfaces['nsIProperties'])
			.get('ProfD', Components.interfaces.nsIFile);

		//create the writeable data directory inside it, if it doesn't already exist
		datafolder.append('dustmeselectors-data');
		if(!datafolder.exists() || !datafolder.isDirectory())
		{
			datafolder.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0600);
		}
	}

	//otherwise create the folder reference from that path
	//** what if the folder has subsequently been deleted?
	else
	{
		datafolder = Components.classes["@mozilla.org/file/local;1"]
			.createInstance(Components.interfaces.nsILocalFile);
		datafolder.initWithPath(dirpath);
	}

	//save the datafolder path
	this.datapath = datafolder.path;

	//create the trash folder if it doesn't already exist
	var trashfolder = datafolder.clone();
	trashfolder.append('trash');
	if(!trashfolder.exists() || !trashfolder.isDirectory())
	{
		trashfolder.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0600);
	}

	//create and save a pointer to the trash folder
	this.trashpointer = Components.classes["@mozilla.org/file/local;1"]
		.createInstance(Components.interfaces.nsILocalFile);
	this.trashpointer.initWithPath(trashfolder.path);

	//** why does this alert twice?
	//alert(this.trashpath);

	//create the hosts index file if it doesn't already exist
	datafolder.append('hostsindex');
	if(!datafolder.exists())
	{
		datafolder.create(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0600);
	}
};



//initialize a file pointer with the given name
DustMeSelectors_filesystem.initFilePointer = function(filename)
{
	var filepointer = Components.classes["@mozilla.org/file/local;1"]
		.createInstance(Components.interfaces.nsILocalFile);
	filepointer.initWithPath(this.datapath);
	filepointer.append(filename);

	return filepointer;
};



//dump data to a new or existing file
DustMeSelectors_filesystem.dumpToFile = function(filename, jsondata)
{
	//initialize a file pointer with the given name
	var filepointer = this.initFilePointer(filename);

	//initialize the output stream and write the data
	var filestream = Components.classes["@mozilla.org/network/file-output-stream;1"]
		.createInstance(Components.interfaces.nsIFileOutputStream);
	filestream.init(filepointer, 0x02 | 0x08 | 0x20, 0600, 0);
	filestream.write(jsondata, jsondata.length);
	filestream.close();
};



//read data from a file
//returning an empty string if the file doesn't exist
//or the data from the file if it does
DustMeSelectors_filesystem.getFromFile = function(filename)
{
	//create a variable for storing the read data
	//initially empty, which we'll return if we find no data
	var jsondata = '';

	//initialize a file pointer with the given name
	var filepointer = this.initFilePointer(filename);

	//try to initialize the input stream and read the data
	//** why is this try catch, is because an exception is thrown for non-existent file?
	try
	{
		var filestream = Components.classes["@mozilla.org/network/file-input-stream;1"]
			.createInstance(Components.interfaces.nsIFileInputStream);
		var scriptablestream = Components.classes["@mozilla.org/scriptableinputstream;1"]
			.createInstance(Components.interfaces.nsIScriptableInputStream);

		filestream.init(filepointer, -1, 0, 0);
		scriptablestream.init(filestream);

		var str = scriptablestream.read(4096);
		while (str.length > 0)
		{
			jsondata += str;
			str = scriptablestream.read(4096);
		}

		scriptablestream.close();
		filestream.close();
	}

	//if we fail do nothing, so we end up returning an empty string
	catch(err) {}

	//return the json data
	return jsondata;
};



//move a file with the given name to the trash folder
//** should we worry about overwriting data in the trash?
//** or do something like add a timestamp to deleted files
DustMeSelectors_filesystem.moveToTrash = function(filename)
{
	//do this on a try catch for when the file doesn't exist
	//for eg, when trying to delete log file if site hasn't been spidered
	//could have done some kind of other detection, but this is easier :)
	try
	{
		var filepointer = this.initFilePointer(filename);
		filepointer.moveTo(this.trashpointer, filename);
	}
	catch(err) {}
};



//create a CSV file of selectors data
DustMeSelectors_filesystem.saveAsCSV = function(doc)
{
	//get the selected tab in the tabbrowser
	var tab = doc.getElementById('dms-viewtabs').selectedItem;
	
	//get the selected host
	var host = doc.getElementById('dms-hostsindexmenu').selectedItem.getAttribute('value');
	
	//now get the appropriate data according to the selected tab
	//returning null in each case if there is not data
	//and along the way store the datatype in a convenience form
	switch(tab.id)
	{
		//the selectors tab
		case 'dms-viewtab-selectors' :

			var data = DustMeSelectors_tools.getDataObject(host, 'selectors', null);
			var datatype = 'selectors';
			break;

		//the used selectors tab
		case 'dms-viewtab-used' :

			data = DustMeSelectors_tools.getDataObject(host, 'used', null)
			var datatype = 'used';
			break;

		//the spider log tab
		case 'dms-viewtab-spider' :

			data = DustMeSelectors_tools.getDataObject(host, 'log', null)
			var datatype = 'log';
			break;
	}
	
	//now the data compilation is completely different
	//for log information, or un/used selectors information
	
	//so, if this is log information 
	if(tab.id == 'dms-viewtab-spider')
	{
		//if the data is null, output the not spidered message
		if(data == null)
		{
			var spreadsheet = DustMeSelectors_tools.bundle.getString('spider.nolog');
		}
		
		//otherwise build two spreadsheet columns starting with the baseurl and summary, then an extra linebreak
		//add quote characters around each value in case it contains commas
		else
		{
			spreadsheet = '"' + data.baseurl 
				+ (data.summary == 'incomplete'
					? (' ' + DustMeSelectors_tools.bundle.getString('spider.incomplete'))
					: '')
				+ '","' 
				+ DustMeSelectors_tools.bundle
					.getString('spider.logsummary')
					.replace('%1', data.pages)
					.replace('%2', data.files - data.pages)
					.replace('%P1', (data.pages == 1 ? '' : DustMeSelectors_tools.bundle.getString('view.plural')))
					.replace('%P2', (data.files - data.pages == 1 ? '' : DustMeSelectors_tools.bundle.getString('view.plural')))
				+ '"\r\n,\r\n';
				
			//then add a line for each spidered URL, still adding quotes
			for(var i in data)
			{
				if(!data.hasOwnProperty(i) || /^(pages|files|summary|baseurl)$/.test(i)) { continue; }
				spreadsheet += '"' + i + '",';
				if(data[i].message)
				{
					spreadsheet += '"' + data[i].message + '"';
				}
				else if(data[i].status != null)
				{
					spreadsheet += '"' + DustMeSelectors_tools.bundle.getString('spider.checked') + '"';
				}
				spreadsheet += '\r\n';
			}
		}
	}
	
	//otherwise for un/used selectors information
	else
	{
		//if the data is null, output the no stylesheets message
		if(data == null)
		{
			spreadsheet = DustMeSelectors_tools.bundle.getString('view.none');
		}
		
		//otherwise, to compile the data data into CSV text
		//first define a matrix that represents the amount of data 2D
		//along the way, rekey the nested objects
		//create a single-member nested object out of string members
		//and store the "no unused" or "no used" message as a single-member to empty nested objects
		else
		{
			var matrix = [];
			for(var i in data)
			{
				if(!data.hasOwnProperty(i)) { continue; }
				var n = 0;
				//if this member is a string message
				//(such as "no unused selectors")
				if(typeof data[i] == 'string')
				{
					//convert this member to an object
					//and store the string as its message property
					data[i] = { 'msg' : data[i] };
				}
				//otherwise it's an object of selectors
				else
				{
					//rekey the object, then iterate and remove items marked deleted
					data[i] = DustMeSelectors.rekey(data[i]);
					for(var j in data[i])
					{
						if(!data[i].hasOwnProperty(j)) { continue; }
		
						//delete data marked as deleted
						if(data[i][j].indexOf('{D}') != -1)
						{
							delete data[i][j];
						}
						//otherwise count them
						else
						{
							n++;
						}
					}
		
					//if we have no data store the okay message
					//and add one to the count so it gets output
					if(n == 0)
					{
						data[i] = { 
							'msg' : DustMeSelectors_tools.bundle
								.getString('view.' + (datatype == 'used' ? 'noused' : 'okay')) 
							};
						n++;
					}
		
					//otherwise rekey again to allow for deleted data
					else
					{
						data[i] = DustMeSelectors.rekey(data[i]);
					}
				}
				matrix.push(n);
			}
		
			//now sort the matrix to find the largest one
			matrix = matrix.sort(function(a, b){ return b - a; });
		
			//then iterate that many times (from -3 to include the headers, message and space rows)
			//and internally iterate on data to build the spreadsheet columns
			spreadsheet = '';
			for(i=-3; i<matrix[0]; i++)
			{
				var line = '';
				for(j in data)
				{
					if(!data.hasOwnProperty(j)) { continue; }
					//url headers
					if(i == -3)
					{
						//add quote characters around the value in case it contains commas
						line += '"' + j + '"';
					}
					//summary/message
					else if(i == -2)
					{
						//add error message if applicable
						//add quote characters around every value in case it contains commas
						if(typeof data[j]['msg'] != 'undefined')
						{
							line += '"' + data[j]['msg'] + '"';
						}
						//otherwise add redundent rule count
						else
						{
							var n = 0;
							for(var k in data[j])
							{
								if(!data[j].hasOwnProperty(k)) { continue; }
								n++;
							}
							line += '"'
									+ DustMeSelectors_tools.bundle
										.getString('view.' + (datatype == 'used' ? 'used' : 'warning'))
										.replace('%1', n)
										.replace('%P1', (n == 1 ? '' : DustMeSelectors_tools.bundle.getString('view.plural')))
									+ '"';
						}
					}
					//selector
					else if(i >= 0 && typeof data[j][i.toString()] != 'undefined')
					{
						//parse out special token values
						//don't add quotes here, it would break parsing for things like [foo="bar"]
						//and we know that single data won't contain commas
						line += data[j][i.toString()].replace(/\{[0-9DMU]+\}/g, '');
					}
					line += ',';
				}
				line = line.substring(0, line.length - 1);
				spreadsheet += line + '\r\n';
			}
		}
	}
	
	//now create and show a file picker interface, specify .csv and .txt as type filter
	//http://developer.mozilla.org/en/docs/XUL_Tutorial:Open_and_Save_Dialogs
	//** how do we put a default filename in the dialog?
	var nsIFilePicker = Components.interfaces.nsIFilePicker;
	var filepointer = Components.classes["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
	filepointer.init(window, DustMeSelectors_tools.bundle
		.getString('view.save').replace('%1', tab.getAttribute('label')), nsIFilePicker.modeSave);
	filepointer.appendFilter(DustMeSelectors_tools.bundle.getString('view.csv'), '*.csv; *.txt');
	filepointer.appendFilters(nsIFilePicker.filterAll);
	var result = filepointer.show();

	//if we get a successful result from the dialog
	//(ie, a file has been named or selected)
	//** how can we get back the file name, for when we want more than one saving format?
	if(result == nsIFilePicker.returnOK || result == nsIFilePicker.returnReplace)
	{
		//open the file stream to write the data into it
		//http://developer.mozilla.org/en/docs/Code_snippets:File_I/O
		var stream = Components.classes["@mozilla.org/network/file-output-stream;1"]
						.createInstance(Components.interfaces['nsIFileOutputStream']);

		//initialize the stream (write | create | truncate)
		//set permissions to limit write actions to the current user
		//write the spreadsheet and close
		stream.init(filepointer.file, 0x02 | 0x08 | 0x20, 0600, 0);
		stream.write(spreadsheet, spreadsheet.length);
		stream.close();
	}

	//hurray!
};
