// Bug Cafe Rearrangeable Rack
// Version 1.0.1
// Feb 28, 2006
// Copyright (c) 2006, Pete Hanson
// Released under the GPL license
// http://www.gnu.org/copyleft/gpl.html
//
// Replaces the rack in Bug Cafe's "Bug Words" game with one that allows
// rearranging the tiles (not just shuffling!).  Currently, this has only been
// tested with the so-called "Indigo" board.  
//
// NOTE:  This script requires that you enable the Shuffle Option on the Bug 
// Words Preferences screen.  The script does not currently work with the new
// Beta board.
//
// TODO:  Add drag-and-drop capability in addition to click-and-click.
// TODO:  Add drag-and-drop or click-and-click play.
// TODO:  Make double-rack Indigo board work with both racks, but each rack
//        should be treated differently.
// TODO:  Doesn't work with the new "beta" board.
// The TODO list should be treated as a wish list, not a list of promises for
// future functionality.
//
// --------------------------------------------------------------------
// This is a Greasemonkey user script.  To install it, you need
// Greasemonkey 0.6.4 or later:
//
//     http://greasemonkey.mozdev.org/
//
// You also need Firefox 1.5 or later:
//
//     http://www.mozilla.com/
// --------------------------------------------------------------------
//
// ==UserScript==
// @name          Bug Cafe Rearrangeable Rack
// @namespace     http://www.well.com/user/wolfy/
// @description	  Replaces the rack in Bug Cafe's "Bug Words" game with one that allows rearranging the tiles
// @include       http*://*.bugcafe.net/
// @include       http*://*.bugcafe.net/enhanced/
// @include       http*://*.bugcafe.net/indigo/
// ==/UserScript==
//
// Changelog:
// - 20060203: work started
// - 20060205: v0.1 BETA: first working version
// - 20060206: v0.1.1 BETA:
//      + Added slightly better error handling for beta period
// - 20060206: v0.1.2 BETA:
//      + Now works with non-Indigo board (both Enhanced and not Enhanced)
//      + Now works with double-rack Indigo board, but only the first rack is
//        modified.
//      + Reworked error handling again
// - 20060209: v1.0
//      + Initial release
// - 20060228: v1.0.1
//      + Added code to detect when user does not have "Shuffle" option enabled
//
// Use tabsize=4

// Color constants

var COLOR_ACTIVE   = 'red' ;
var COLOR_BORDER   = 'black' ;
var COLOR_RACK     = '#fff6dd' ;
var COLOR_SELECTED = 'green' ;

// Global variables

var gSelectedTile  = -1 ;		// Currently selected tile number
var gTiles ;					// The tiles portion of the rack (a <tr>)
	
//-----------------------------------------------------------------------------
// Retrieves the tile (a <td> element) in position "pos".  This assumes that
// the spacing gaps have already been inserted in the rack.
	
function GetTile(pos)
{
	return gTiles[2 * pos + 1] ;
}
	
//-----------------------------------------------------------------------------
// Deselect a tile.
	
function TileUnclick()
{
	if (gSelectedTile != -1)
	{
		var tile = GetTile(gSelectedTile) ;
		tile.setAttribute('class', 'bcrr_tile') ;
		gSelectedTile = -1 ;
	}
}
	
//-----------------------------------------------------------------------------
// Handles clicks on the gaps in the rack

function GapClicked(pos, images)
{
	if (gSelectedTile >= 0 && gSelectedTile != pos)
	{
		// Extract the source locations for all of the images.
		var src = new Array(images.length) ;
		for (var i = 0 ; i < images.length ; ++i)
		{
			src[i] = images[i].src ;
		}

		// Stick the source locations back in the letter rack in the
		// desired order.
		
		if (pos < gSelectedTile)
		{
			images[pos].src = src[gSelectedTile] ;
			for (var i = pos + 1 ; i <= gSelectedTile ; ++i)
			{
				images[i].src = src[i - 1] ;
			}
		}
		else if (pos > gSelectedTile + 1)
		{
			if (pos > images.length)
			{
				pos = images.length ;
			}
				
			images[pos - 1].src = src[gSelectedTile] ;
			for (var i = gSelectedTile + 1 ; i < pos ; ++i)
			{
				images[i - 1].src = src[i] ;
			}
		}
	}

	TileUnclick() ;
}
	
//-----------------------------------------------------------------------------
// Handles clicks on the tiles in the rack
	
function TileClicked(pos)
{
	TileUnclick() ;
	gSelectedTile = pos ;
	GetTile(pos).setAttribute('class', 'bcrr_tile_selected') ;
}
	
//-----------------------------------------------------------------------------
// Adds styles required by this script.
	
function AddStyles()
{
	GM_addStyle(
		// Used for small text to describe how to use the rack
		'.bcrr_small {'										+
		'  font-size: 85% ;'								+
		'  font-weight: lighter ;'							+
		'  color: #555555 ;'								+
		'}'													+
		
		// Modified image style to make the tiles look more natural
		'.bcrr_tile_img {'									+
		'  border: 1px solid ' + COLOR_BORDER + ' ;'		+
		'}'													+
		
		// The space occupied by a tile, other than the actual image
		'.bcrr_tile {'										+
		'  border-bottom: 1px solid ' + COLOR_BORDER + ' ;'	+
		'  padding: 2px ;'									+
		'  background-color: ' + COLOR_RACK + ' ;'			+
		'}'													+
		
		// If the mouse is over a tile, it should highlight itself
		'.bcrr_tile_active {'								+
		'  border-bottom: 1px solid ' + COLOR_BORDER + ' ;'	+
		'  padding: 2px ;'									+
		'  background-color: ' + COLOR_ACTIVE + ' ;'		+
		'}'													+
		
		// Currently selected titles should be specially highlighted
		'.bcrr_tile_selected {'								+
		'  border-bottom: 1px solid ' + COLOR_BORDER + ' ;'	+
		'  padding: 2px ;'									+
		'  background-color: ' + COLOR_SELECTED + ' ;'		+
		'}'													+
		
		// Gaps (inactive)
		'.bcrr_gap_first {'									+
		'  background-color: ' + COLOR_RACK + ' ;'			+
		'  border-left: 1px solid ' + COLOR_BORDER + ' ;'	+
		'  border-bottom: 1px solid ' + COLOR_BORDER + ' ;'	+
		'}'													+
		'.bcrr_gap {'										+
		'  background-color: ' + COLOR_RACK + ' ;'			+
		'  border-bottom: 1px solid ' + COLOR_BORDER + ' ;'	+
		'}'													+
		'.bcrr_gap_last {'									+
		'  background-color: ' + COLOR_RACK + ' ;'			+
		'  border-right: 1px solid ' + COLOR_BORDER + ' ;'	+
		'  border-bottom: 1px solid ' + COLOR_BORDER + ' ;'	+
		'}'													+
		
		// Gaps (active)
		'.bcrr_gap_first_active {'							+
		'  background-color: ' + COLOR_ACTIVE + ' ;'		+
		'  border-left: 1px solid ' + COLOR_BORDER + ' ;'	+
		'  border-bottom: 1px solid ' + COLOR_BORDER + ' ;'	+
		'}'													+
		'.bcrr_gap_active {'								+
		'  background-color: ' + COLOR_ACTIVE + ' ;'		+
		'  border-bottom: 1px solid ' + COLOR_BORDER + ' ;'	+
		'}'													+
		'.bcrr_gap_last_active {'							+
		'  background-color: ' + COLOR_ACTIVE + ' ;'		+
		'  border-right: 1px solid ' + COLOR_BORDER + ' ;'	+
		'  border-bottom: 1px solid ' + COLOR_BORDER + ' ;'	+
		'}'													+
		
		'' // So we don't have to worry about the trailing + chars
	) ;
}
	
//-----------------------------------------------------------------------------
// Locate and return the letter rack (the entire table for the rack).
// Throws an error if the letter rack cannot be found.
	
function LocateRack()
{
	var tables = document.getElementsByTagName('table') ;
	for (var i = 0 ; i < tables.length ; ++i)
	{
		var inner = tables[i].innerHTML ;
		if (inner.match(/^\s*<tbody>\s*<tr>\s*<td[^>]*>\s*<font[^>]*>\s*<b>\s*Letter Rack<\/b>/))
		{
			return tables[i] ;
		}
	}

	throw new Error("Failed to find letter rack in this page") ;
}

//-----------------------------------------------------------------------------
// Returns HTML suitable for insertion as a gap in the rack at position
// "pos" (0-7).
	
function Gap(pos, images)
{
	// function objects for use as event listener.
	var gapClicked = function()
		{
			GapClicked(pos, images) ;
		} ;

	var gapMouseIn = function()
		{
			var cls = this.getAttribute('class') ;
			this.setAttribute('class', cls + '_active') ;
		} ;
		
	var gapMouseOut = function()
		{
			var cls = this.getAttribute('class') ;
			this.setAttribute('class', cls.replace(/_active/, '')) ;
		} ;
		
	// Note that the class has to change for the rightmost gap - this is
	// done to make sure we get a border on the right.
	var gap = document.createElement('td') ;
	var gapClass = 'bcrr_gap' ;
	if (pos == 0)
	{
		gapClass = 'bcrr_gap_first' ;
	}
	else if (pos == 7)
	{
		gapClass = 'bcrr_gap_last' ;
	}
	
	gap.setAttribute('class', gapClass) ;
	gap.setAttribute('width', '15') ;
	gap.addEventListener('click', gapClicked, true) ;
	gap.addEventListener('mouseover', gapMouseIn, true) ;
	gap.addEventListener('mouseout', gapMouseOut, true) ;
	gap.innerHTML = '&nbsp;' ;
	return gap ;
}

//-----------------------------------------------------------------------------
// Make a tile clickable.
	
function MakeClickable(tiles, pos)
{
	var tileClicked = function()
		{
			TileClicked(pos) ;
		} ;
	
	var tileMouseIn = function()
		{
			var cls = this.getAttribute('class') ;
			if (! cls.match(/_selected/))
			{
				this.setAttribute('class', 'bcrr_tile_active') ;
			}
		} ;
		
	var tileMouseOut = function()
		{
			var cls = this.getAttribute('class') ;
			var newcls = cls.replace(/_active/, '') ;
			if (cls != newcls)
			{
				this.setAttribute('class', newcls) ;
			}
		} ;

	tiles[pos].addEventListener('click', tileClicked, true) ;
	tiles[pos].addEventListener('mouseover', tileMouseIn, true) ;
	tiles[pos].addEventListener('mouseout', tileMouseOut, true) ;
}

//-----------------------------------------------------------------------------
// Customize the title bar of the letter rack.  Throws an error if any
// errors are detected.

function CustomizeTitleBar(title_bar)
{
	// Update the title bar with some explanatory text.
	title_bar.innerHTML = title_bar.innerHTML.replace(
		/Letter Rack/,
		"Letter Rack<br />"										+
		'<span class="bcrr_small">'								+
		'To rearrange tiles, click a tile, then click a gap'	+
		'</span>'
	) ;

	// The title bar has to span not just tiles now, but 8 gaps as well.
	var title_data = title_bar.getElementsByTagName('td') ;
	if (title_data.length == 0 || ! title_data[0].hasAttribute('colspan'))
	{
		throw new Error("Could not locate title bar data for the rack") ;
	}
		
	title_data[0].setAttribute('colspan', '15') ;
}

//-----------------------------------------------------------------------------
// Customize the tiles on the letter rack.  Throws an error if any errors
// are detected.

function CustomizeTiles(letters)
{
	// Parse the tile row into the 7 tiles (or fewer).
	var tiles = letters.getElementsByTagName('td') ;
	if (tiles.length != 7)
	{
		throw new Error("Tile row has " + tiles.length + " cells, not 7") ;
	}

	// Set an onclick handler for each tile.  At the same time, we can also
	// extract the image information so the onclick handler for gaps has
	// something to work with.
	var images = new Array(tiles.length) ;
	for (var pos = 0 ; pos < tiles.length ; ++pos)
	{
		var imgs = tiles[pos].getElementsByTagName('img') ;
		if (imgs.length != 1)
		{
			images.length = pos ;
			break ;
		}
		
		MakeClickable(tiles, pos) ;
		tiles[pos].removeAttribute('class') ;
		
		imgs[0].setAttribute('class', 'bcrr_tile_img') ;
		images[pos] = imgs[0] ;
	}

	// Insert gaps in the tile rack.  We do this from right to left to
	// simplify the code.
	
	var gap = Gap(tiles.length, images) ;
	tiles[tiles.length - 1].setAttribute('class', 'bcrr_tile') ;
	tiles[tiles.length - 1].removeAttribute('bgcolor') ;
	tiles[tiles.length - 1].parentNode.appendChild(gap) ;
	for (var pos = tiles.length - 2 ; pos >= 0 ; --pos)
	{
		gap = Gap(pos, images) ;
		tiles[pos].setAttribute('class', 'bcrr_tile') ;
		tiles[pos].removeAttribute('bgcolor') ;
		tiles[pos].parentNode.insertBefore(gap, tiles[pos]) ;
	}
	
	// Save the tiles for global use
	gTiles = tiles ;
}
	
//-----------------------------------------------------------------------------
// Customize the rack for our needs.  Throws an error if any portion of
// the rack cannot be found.

function CustomizeRack(rack)
{
	// Retrieve the table rows.
	var rows = rack.getElementsByTagName('tr') ;
	if (rows.length < 2)
	{
		throw new Error("Could not parse rows from rack") ;
	}

	// Update the title bar and the tiles
	CustomizeTitleBar(rows[0], rows[1]) ;
	CustomizeTiles(rows[1]) ;
} ;

//-----------------------------------------------------------------------------
// Determine if the shuffle option is enabled.  We do this by looking for an
// image with the name 'rack0'

function ShuffleEnabled()
{
	var imgs =  document.evaluate(
		'//IMG[@name="rack0"]',
		document,
		null,
		XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
		null
	) ;
	
	return imgs.snapshotLength != 0 ;
}

//-----------------------------------------------------------------------------
// Main program begins here.

// Make sure user has the Shuffle option enabled.  The Shuffle option causes
// Bug Words to use image-based tiles that can be easily manipulated by this
// script.  The text-based tiles used when the Shuffle option is disabled are
// very difficult - maybe impossible - to work with.
if (ShuffleEnabled())
{
	// Add styles required by this script
	AddStyles() ;
	
	// Locate and modify the rack
	var rack = LocateRack() ;

	// Perform the page update (but only after the page has loaded).
	window.addEventListener(
		"load",
		function()
		{
			CustomizeRack(rack) ; 
		},
		true
	) ;
}
else
{
	alert(
		"The 'Bug Cafe Rearrangeable Rack' Greasemonkey script requires "	+
		"that you enable the Shuffle Option in the Bug Words Preferences. "	+
		"To enable this option, go to Preferences, Bug Words and set "		+
		"'Enable Shuffle Option' to 'Yes'."									+
		""
	) ;
}

