/*
Geocaching Block Collapser
Version 1.0
By Lil Devil
http://www.lildevil.org/greasemonkey/
lildevil@gpxspinner.com

--------------------------------------------------------------------------------

This is a Greasemonkey user script.

Follow the instructions on http://www.lildevil.org/greasemonkey/
to install Greasmonkey and this user script.

--------------------------------------------------------------------------------

Function:
 Adds an icon to the title bar of each of the blocks on the right-hand side of
 the geocaching.com cache pages so the blocks can be collapsed and expanded.

--------------------------------------------------------------------------------

Revision History:

1.0 - 2007-01-10 - Initial release.

--------------------------------------------------------------------------------
*/

// ==UserScript==
// @name           Geocaching Block Collapser
// @namespace      http://www.lildevil.org/greasemonkey/
// @description    Collapse blocks on cache pages. (v1.0)
// @include        http://*.geocaching.com/seek/cache_details.aspx?*
// @exclude        *pf=y*
// ==/UserScript==


(function() {
	if (!GM_getValue || !GM_addStyle) {
		alert('Geocaching Collapse Blocks requires Greasemonkey 0.5 or newer.');
		return;
	}

	var icon_collapse_gray = 'data:image/gif;base64,' +
		'R0lGODlhDAAMAOMMAIqvwoywwo6xxJG0xZO1xpa3x5i4yJq6yZ27y5+9zKG+zaTAzqXBz6XBz6XBz6XB' +
		'zyH5BAEAAA8ALAAAAAAMAAwAAAQt8IFJ6wsv6x3E/o8wgNtAkBpRPEXrtqzxGHRNzweaHYj+IAlfQuEj' +
		'Lo7I5CMCADs=';

	var icon_collapse_hover = 'data:image/gif;base64,' +
		'R0lGODlhDAAMAIQUAEt6qWCPvXWgy4au14mw2I2z2Y+02pO325S425a53Jm73Zu83p6+36LB4KbD4anG' +
		'4q3J5LHL5b7U6f///6XBz6XBz6XBz6XBz6XBz6XBz6XBz6XBz6XBz6XBz6XBz6XBzyH5BAEAAB8ALAAA' +
		'AAAMAAwAAAVB4BeMZClKaJoiQCBF0NMsyWEI7RvP9Z3DMpoN55oYj0aiBIlUOhoMxaFAGDihUqo194xO' +
		'q0SGYEwmAz6AtHr9CQEAOw==';

	var icon_expand_gray = 'data:image/gif;base64,' +
		'R0lGODlhDAAMAOMMAIqvwoywwo6xxJG0xZO1xpa3x5i4yJq6yZ27y5+9zKG+zaTAzqXBz6XBz6XBz6XB' +
		'zyH5BAEAAA8ALAAAAAAMAAwAAAQ58IFJ6wsv6x3EFp4mDNtAagOxEapGFE8hz3JsPEau5/ixHT7NAbFB' +
		'EDWIxCah1CQUm+gDuqhar48IADs=';

	var icon_expand_hover = 'data:image/gif;base64,' +
		'R0lGODlhDAAMAIQSAEt6qWCPvXWgy4au14mw2I2z2Y+02pO325S425a53J6+36LB4KbD4anG4q3J5LHL' +
		'5b7U6f///6XBz6XBz6XBz6XBz6XBz6XBz6XBz6XBz6XBz6XBz6XBz6XBz6XBz6XBzyH5BAEAAB8ALAAA' +
		'AAAMAAwAAAU+4BeMZClCaJoiQAA9ThNFyWEI7RvP9Z3DMpoN55oZjUTI8ZhkLBSzAmHQfEanVWhESm0p' +
		'BOBwGPABmM/oTwgAOw==';

	var icon_hide_gray = 'data:image/gif;base64,' +
		'R0lGODlhDAAMAOMMAIqvwoywwo6xxJG0xZO1xpa3x5i4yJq6yZ27y5+9zKG+zaTAzqXBz6XBz6XBz6XB' +
		'zyH5BAEAAA8ALAAAAAAMAAwAAAQ+8IFJ6wsv6x3EE54GfsMznNlZDkRGvLBbaEU9Z4WhGbyeGYfMYUgU' +
		'Ih6IZCZ5RCQeiacmClVsrg/rYsvtPiIAOw==';

	var icon_hide_hover = 'data:image/gif;base64,' +
		'R0lGODlhDAAMAOMIAIY0Ka1TU9dHR9ppad9+fuZ8fOiamv///6XBz6XBz6XBz6XBz6XBz6XBz6XBz6XB' +
		'zyH5BAEAAAgALAAAAAAMAAwAAAQ2EIVJqzQ45wKCOQchEqDQfaAIHqZHruDQGuo6yGcd4y4cz7bBCni4' +
		'3UqdgmDJZAIQgKh0iogAADs=';

	// Create some needed styles
	GM_addStyle('div.toggle_block   { width:12px; height:12px; float:right;' +
									' position:relative; top:1px; right:1px;' +
									' cursor:pointer; }');

	GM_addStyle('div.collapse_block       { background-image:url(' + icon_collapse_gray  + '); }');
	GM_addStyle('div.collapse_block:hover { background-image:url(' + icon_collapse_hover + '); }');
	GM_addStyle('div.expand_block         { background-image:url(' + icon_expand_gray    + '); }');
	GM_addStyle('div.expand_block:hover   { background-image:url(' + icon_expand_hover   + '); }');
	GM_addStyle('tr.hidden_row            { display:none; }');

	GM_addStyle('div.hide_block        { background-image:url(' + icon_hide_gray    + ');' +
									   ' margin-left:3px; }');
	GM_addStyle('div.hide_block:hover  { background-image:url(' + icon_hide_hover   + '); }');
	GM_addStyle('table.hidden          { display:none; }');

	GM_addStyle('#BC_unhide_link       { color:#999999; float:right; margin-right:3px;' +
									   ' cursor:pointer; font-size:smaller; }');
	GM_addStyle('#BC_unhide_link:hover { text-decoration:underline; }');

	GM_addStyle('#BC_options_link      { cursor:pointer; color:#DDDDDD; font-size:80%;' +
									   ' padding:1px 12px; text-decoration:underline;' +
									   ' float:right; }');
	GM_addStyle('#BC_options_link:hover { color:#FFFF00; }');

	GM_addStyle('.BC_option_rows   { vertical-align:middle; }');
	GM_addStyle('.BC_buttons       { font-family:Verdana,Tahoma,Arial,sans-serif; font-size:80%; }');
	GM_addStyle('#BC_options_table { position:absolute; z-index:3; }');  // width:175px;

	// Define some constants
	// 					   0 = expanded      1 = collapsed     2 - hidden
	var Class_Name     = ['collapse_block', 'expand_block', 'expand_block'];
	var Tooltip_String = ['Collapse', 'Expand', 'Expand'];
	var Collapse_Blocks = ['Navigation',
						   'Inventory',
						   'Attributes',
						   'Bookmark Lists',
						   'My Bookmark Lists',
						   'Cache Logs'];
	var Hidable_Blocks =  ['Inventory',
						   'Attributes',
						   'Bookmark Lists',
						   'My Bookmark Lists'];

	var Prefs	= { AllowHiding:   false,
				    AutoInventory: false };

	// Get logged-in user name.
	var lnkLoginName_obj = document.getElementById('Header1_lnkLoginName');
	if (lnkLoginName_obj) {
		var Login_Name = Clean_String(lnkLoginName_obj.firstChild.firstChild.data);
	}

	Read_BC_Prefs();

	// Execute xpath query to find the header row of the blocks we want to collapse.
	var blocks = document.evaluate(
		'//td[@class="containerHeader"]',
		document,
		null,
		XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
		null);

	var blocksHidden = 0;
	for (var b=0; b < blocks.snapshotLength; b++) {
		var td_obj = blocks.snapshotItem(b);

		// get the block name
		var containerName = Clean_String(td_obj.lastChild.data);

		// only collapse blocks we know of
		if (Collapse_Blocks.indexOf(containerName) < 0) { continue; }

		// count the items inside and add to title
		var quan = Count_Items(td_obj);

		// get saved state
		// 0 = expanded (default)
		// 1 = collapsed
		// 2 - hidden
		var blockState = Read_State(containerName);
		if (containerName == 'Inventory' && Prefs.AutoInventory && (blockState!=2)) {
			blockState = quan ? 0 : 1;
		}

		// collapse or hide the block
		Update_Block_Display_State(td_obj.parentNode, blockState);

		// add button to collapse/expand block
		var collapseButton_div = document.createElement('div');
		collapseButton_div.id        = 'collapse_' + Clean_String(containerName, 1) + '_block';
		collapseButton_div.title     = Tooltip_String[blockState] + ' the ' +
										containerName + ' block';
		collapseButton_div.className = 'toggle_block ' + Class_Name[blockState];
		collapseButton_div.addEventListener('click', Collapse_Block, true);
		td_obj.insertBefore(collapseButton_div, td_obj.firstChild);

		// add button to hide block
		if (Prefs.AllowHiding && Hidable_Blocks.indexOf(containerName) >= 0) {
			Add_Hide_Button(containerName, collapseButton_div);
		}

		// if the block is hidden, set a flag
		if (blockState == 2) {
			blocksHidden++;
		}
	}

	// if any blocks are hidden, add a link to un-hide them
	if (blocksHidden) {
		Add_Unhide_Link();
	}

	// add link to edit options
	Add_Options_Link();



// ---------------------Functions--------------------

function Add_Hide_Button(blockName, collapseButton_obj) {

	// make sure a collapse button was supplied
	if (!collapseButton_obj) { return; }

	var hideButton_div = document.createElement('div');
	hideButton_div.id        = 'hide_' + Clean_String(blockName, 1) + '_block';
	hideButton_div.title     = 'Hide the ' + blockName + ' block';
	hideButton_div.className = 'toggle_block hide_block';
	hideButton_div.addEventListener('click', Hide_Block, true);
	collapseButton_obj.parentNode.insertBefore(hideButton_div, collapseButton_obj);
}


function Update_Block_Display_State(anyRow_obj, state) {
	// 0 = expanded (default)
	// 1 = collapsed
	// 2 - hidden
	var table_obj = anyRow_obj.parentNode.parentNode;
	if (state == 2) {
		// hide the whole table
		table_obj.className += ' hidden';
	}
	else {
		// step through the rows and hide (or show) all but the header
		var rows = anyRow_obj.parentNode.getElementsByTagName('tr');

		// start at index 1 to skip the header row
		for (var r=1; r < rows.length; r++) {

			// we only want *direct* descendants
			if (rows[r].parentNode != anyRow_obj.parentNode) { continue; }

			if (state) {
				rows[r].setAttribute('class', 'hidden_row');
			} else {
				rows[r].removeAttribute('class');
			}
		}

		// un-hide the whole table in case it was hidden before
		table_obj.className = table_obj.className.replace(/(^|\s)hidden(\s|$)/, '');
	}
}

function Update_Block_Buttons(toggle_obj, blockName, state) {
	// update the tooltip
	toggle_obj.title = Tooltip_String[state] + ' the ' + blockName + ' block';

	// change the style of the "toggle" button to reflect state
	toggle_obj.className = toggle_obj.className.replace(/(collapse|expand)_block/, Class_Name[state]);
}

function Collapse_Block() {
	// get the opposite of the current state so we don't have to toggle it
	// 0 = expanded (default)
	// 1 = collapsed
	// 2 - hidden
	var openState = this.className.match(/expand_block/) ? 0 : 1;

	// get the outer body of the block we're collapsing/expanding
	var thisRow = this.parentNode.parentNode;

	// hide or show the rows
	Update_Block_Display_State(thisRow, openState);

	// get the block name
	var blockName = Clean_String(this.parentNode.lastChild.data);

	// update the buttons
	Update_Block_Buttons(this, blockName, openState);

	// store the new state
	Write_State(blockName, openState);
}

function Hide_Block() {
	// get a <tr> row of the block we're collapsing/expanding
	var thisRow = this.parentNode.parentNode;

	// hide or show all the rows
	Update_Block_Display_State(thisRow, 2);

	// get the block name
	var blockName = Clean_String(this.parentNode.lastChild.data);

	// store the new state
	Write_State(blockName, 2);

	// add an un-hide link
	Add_Unhide_Link();
}

function Unhide_All_Blocks() {
	for (var i=0; i < Hidable_Blocks.length; i++) {
		var blockName = Hidable_Blocks[i];
		var cleaName = Clean_String(blockName, 1) + '_block';
		var hideButton = document.getElementById('hide_' + cleaName);
		var collapseButton = document.getElementById('collapse_' + cleaName);

		if (hideButton) {
			var header_row = hideButton.parentNode.parentNode;

			// test if the row's enclosing <table> is hidden
			if (header_row.parentNode.parentNode.className.match(/(^|\s)hidden(\s|$)/)) {
				// unhide it
				Update_Block_Display_State(header_row, 0);

				// update the buttons
				Update_Block_Buttons(collapseButton, blockName, 0);

				// save the new state
				Write_State(blockName, 0);
			}
		}
	}
	Remove_Unhide_Link();
}

function Add_Unhide_Link() {
	if (document.getElementById('unhide_blocks_link')) { return; }

	var unhideSpan = document.createElement('span');
	unhideSpan.id  = 'BC_unhide_link';
	unhideSpan.appendChild(document.createTextNode('Un-hide all blocks'));
	unhideSpan.addEventListener('click', Unhide_All_Blocks, true);

	var insertSpot = document.getElementById('Form1').parentNode.childNodes[4];
	insertSpot.parentNode.insertBefore(unhideSpan, insertSpot);
}

function Remove_Unhide_Link() {
	var unlink = document.getElementById('BC_unhide_link');
	if (unlink) {
		unlink.parentNode.removeChild(unlink);
	}
}

function Count_Items(titleCell) {
	// get the block name
	var blockName = Clean_String(titleCell.lastChild.data);
	var more = 0;
	var quantity = 0;
	var quantityStr = 'No';

	switch (blockName) {
		case 'Attributes':
			var attributes = document.evaluate(
				'count(//td/table/tbody/tr/td/img[contains(@src, "/images/attributes/")])',
				titleCell.parentNode.parentNode,
				null,
				XPathResult.NUMBER_TYPE,
				null);
			if (attributes) {
				quantity = attributes.numberValue;
				quantityStr = quantity;
			}
			break;

		case 'Inventory':
			// could be travel bug links anywhere on page so don't use Xpath
			var a = titleCell.parentNode.parentNode.getElementsByTagName('a');
			for (var i=0; i < a.length; i++ ) {
				if (a[i].href.match(/\/track\/details\.aspx/)) {
					quantity++;
				}
				if (a[i].href.match(/\/track\/search.\aspx/) &&
				    a[i].firstChild.data == 'more...') {
					more++;
				}
			}
			quantityStr = quantity;
			if (more) {
				quantityStr += '+';
			}
			quantityStr += ' in';
			break;

		case 'Bookmark Lists':
		case 'My Bookmark Lists':
			// difficult to tell difference between bookmarks and MY bookmarks
			// using Xpath, so use another method
			a = titleCell.parentNode.parentNode.getElementsByTagName('a');
			for (i=0; i < a.length; i++ ) {
				if (a[i].href.match(/\/bookmarks\/view\.aspx/)) {
					quantity++;
				}
				if (a[i].href.match(/\/bookmarks\/default\.aspx/)) {
					more++;
				}
			}
			quantityStr = quantity;
			if (more) {
				quantityStr += '+';
			}
			break;

		case 'Cache Logs':
			var logs = document.evaluate(
				'count(//td/font/strong/img[contains(@src, "/images/icons/")])',
				titleCell.parentNode.parentNode,
				null,
				XPathResult.NUMBER_TYPE,
				null);
			if (logs) {
				quantity = logs.numberValue;
				quantityStr = quantity;
				var e_MoreLogs = document.getElementById('MoreLogs');
				if (e_MoreLogs) {
					quantityStr += '+';
				}
			}
			break;

		default:
			return;
	}
	if (!quantity) {
		quantityStr = 'No';
	}

	if (blockName == 'Cache Logs') {
		quantityStr += ' ';
	}

	// add the quantity as a <span> so we can still get the block name in the .data
	var newSpan_obj = document.createElement('span');
	newSpan_obj.appendChild(document.createTextNode(' ' + quantityStr));
	titleCell.insertBefore(newSpan_obj, titleCell.lastChild);

	return quantity;
}

function Add_Options_Link() {
		var footerLink = document.evaluate(
			'//td/a[@href="http://www.groundspeak.com"]',
			document,
			null,
			XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
			null);

		if (footerLink.snapshotLength) {
			var footerCell = footerLink.snapshotItem(0).parentNode;

			var editLink = document.createElement('div');
			editLink.id  = 'BC_options_link';
			editLink.addEventListener('click', Edit_Options, true);
			editLink.appendChild(document.createTextNode('Block Collapser Options'));
			footerCell.insertBefore(editLink, footerCell.firstChild);
		}
}

function Edit_Options() {
	// disable Options link so we don't get 2 of these tables
	var link_obj = document.getElementById('BC_options_link');
	link_obj.removeEventListener('click', Edit_Options, true);

	Read_BC_Prefs();

	var tableHtml = '<tbody><tr><td class="containerHeader" colspan="2">';
	tableHtml += 'Block Collapser Options</td></tr>';

	var checkedAH = Prefs.AllowHiding ? ' checked="checked"' : '';
	tableHtml += '<tr><td class="containerRow">' +
				 '<input type="checkbox" id="allow_hiding_checkbox" ' +
				 'class="BC_option_rows"' + checkedAH + '>' +
				 '<label for="allow_hiding_checkbox" class="BC_option_rows">' +
				 'Allow blocks to be hidden completely</label></td></tr>';

	var checkedAI = Prefs.AutoInventory ? ' checked="checked"' : '' ;
	tableHtml += '<tr><td class="containerAltRow">' +
				 '<input type="checkbox" id="auto_inventory_checkbox" ' +
				 'class="BC_option_rows"' + checkedAI + '>' +
				 '<label for="auto_inventory_checkbox" class="BC_option_rows">' +
				 'Automatically collapse the Inventory block</label></td></tr>';

	tableHtml += '<td class="containerFooter" colspan="2" align="center">' +
				 '<button type="button" class="BC_buttons" id="save_BC_options">Save</button> &nbsp;' +
				 '<button type="button" class="BC_buttons" id="cancel_BC_options">Cancel</a>' +
				 '</td></tr></tbody>';

	// create table
	var newTable_obj = document.createElement('table');
	newTable_obj.setAttribute('class', 'container');
	newTable_obj.setAttribute('cellspacing', '0');
	newTable_obj.setAttribute('cellpadding', '0');
	newTable_obj.className  = 'container';
	newTable_obj.id			= 'BC_options_table';
	newTable_obj.innerHTML	= tableHtml;

	// display the table with the top set at zero (way at the top of the document)
	link_obj.parentNode.appendChild(newTable_obj);

	// determine where the right edge of the new table should be and set it
	var logs_obj = document.getElementById('CacheLogs').lastChild;
	var logsDim = Get_Position(logs_obj);
	var newTable_left = logsDim.left + (logsDim.width - newTable_obj.offsetWidth) ;
	newTable_obj.style.left = newTable_left + 'px';

	// determine where the top of the new table should be and set it
	var newTable_top = Get_Position(link_obj).top - newTable_obj.offsetHeight - 5;
	newTable_obj.style.top = newTable_top + 'px';

	Add_Listener('save_BC_options', Save_BC_Options);
	Add_Listener('cancel_BC_options', Cancel_BC_Options);
}

function Cancel_BC_Options() {
	// forget it. destroy the table.
	myTable = document.getElementById('BC_options_table');
	if (!myTable) { return; } // shouldn't happen

	myTable.parentNode.removeChild(myTable);

	// re-activate the edit link
	Add_Listener('BC_options_link', Edit_Options);
}

function Save_BC_Options() {
	Prefs.AllowHiding   = document.getElementById('allow_hiding_checkbox').checked;
	Prefs.AutoInventory = document.getElementById('auto_inventory_checkbox').checked;
	Write_BC_Prefs();

	// if hiding no longer allowed, unhide all the blocks
	if (!Prefs.AllowHiding) {
		Unhide_All_Blocks();
	}

	for (var i=0; i < Hidable_Blocks.length; i++) {
		var blockName = Hidable_Blocks[i];
		var cleName = Clean_String(blockName, 1) + '_block';
		var hide_obj = document.getElementById('hide_' + cleName);
		if (Prefs.AllowHiding) {
			if (!hide_obj) {
				var col_obj = document.getElementById('collapse_' + cleName);
				Add_Hide_Button(blockName, col_obj);
			}
		} else {
			if (hide_obj) {
				hide_obj.parentNode.removeChild(hide_obj);
			}
		}
	}

	Cancel_BC_Options();
}

function Get_Position(obj) {
	var ret = {	left   : 0,
				top    : 0,
				width  : 0,
				height : 0 };

	if (obj.offsetParent) {
		ret.left   = obj.offsetLeft;
		ret.top    = obj.offsetTop;
		ret.width  = obj.offsetWidth;
		ret.height = obj.offsetHeight;

		while (obj = obj.offsetParent) {
			ret.left   += obj.offsetLeft;
			ret.top    += obj.offsetTop;
		}
	}
	return ret;
}

function Add_Listener(theID, theFunc, theEvent) {	// default event = click
	theEvent = theEvent || 'click';

	var theObject = document.getElementById(theID);
	if (theObject) {
		theObject.addEventListener(theEvent, theFunc, true);
		return true;
	}
	return false;
}

function Clean_String(str, underscores) {
	// remove leading and trailing spaces
	str = str.replace(/^(\s|\&nbsp;|\xA0)+/i, '');
	str = str.replace(/(\s|\&nbsp;|\xA0)+$/i, '');

	// replace remaining spaces with an underscore
	if (underscores) {
		str = str.replace(/(\s|\&nbsp;|\xA0)+/ig, '_');
	}
	return str;
}

function Write_State(blockName, state) {
	if (Login_Name) {
		// logged in, so store it permanately
		GM_setValue(encodeURI(Clean_String(Login_Name, 1)) +
				'_' + Clean_String(blockName, 1), state);
	} else {
		// not logged in, so save it for this session only
		document.cookie = Clean_String(blockName, 1) + '=' + state + '; path=/seek/';
	}
}

function Write_BC_Prefs() {
	if (Login_Name) {
		// logged in, so store it permanately
		var cleanName = encodeURI(Clean_String(Login_Name, 1));
		GM_setValue(cleanName + '_Allow_Hiding',   Prefs.AllowHiding);
		GM_setValue(cleanName + '_Auto_Inventory', Prefs.AutoInventory);
	} else {
		// not logged in, so save it for this session only
		document.cookie = 'Allow_Hiding='   + Prefs.AllowHiding   + '; path=/seek/';
		document.cookie = 'Auto_Inventory=' + Prefs.AutoInventory + '; path=/seek/';
	}
}

function Read_BC_Prefs() {
	if (Login_Name) {
		var cleanName = encodeURI(Clean_String(Login_Name, 1));
		Prefs.AllowHiding   = GM_getValue(cleanName + '_Allow_Hiding', false);
		Prefs.AutoInventory = GM_getValue(cleanName + '_Auto_Inventory', true);
	} else {
		var result = (document.cookie + '').match(/Allow_Hiding=(\w+)/);
		Prefs.AllowHiding = (result) ? result[1] : false;

		var result = (document.cookie + '').match(/AutoInventory=(\w+)/);
		Prefs.AutoInventory = (result) ? result[1] : true;
	}
}

function Read_State(blockName) {
	if (Login_Name) {
		return GM_getValue(encodeURI(Clean_String(Login_Name, 1)) +
				       '_' + Clean_String(blockName, 1), 0);
	} else {
		var regex = new RegExp(Clean_String(blockName, 1) + '=([-\d]+)');
		var result = (document.cookie + '').match(regex);
		if (result) {
			return result[1];
		} else {
			return 0;
		}
	}
}

//  Returns a URL parameter.
function UrlParm(seekParm) {
	var pageUrl = document.location + '';
	var parmString = pageUrl.substring(pageUrl.indexOf('?') + 1);
	var regEx = new RegExp('(^|&)' + seekParm + '=(.*?)(&|#|$)');
	var result = regEx.exec(parmString);
	if (result) {
		return result[2];
	}
	return '';
}

})();

