/**
 * Import Util.js first
 * Need JavaScript1.2 for function literal support
 * 
 * Usage: 
 * <A id="aboutA"><SPAN id="aboutSpan">About</SPAN></A>
 * 
 * Place a script like this at the end of your document: 
 * <SCRIPT type="text/javascript" language="JavaScript1.2">
 * <!--
 * 	EffectHandler.bind("aboutA", "aboutSpan", "AbstractEffect(100)");
 * //-->
 * </SCRIPT>
 * (c) 2005 Mat Gessel
 */

/**
 * CommandQueue
 */
CommandQueue.INTERVAL = 5;

CommandQueue.s_instance = new CommandQueue();

function CommandQueue()
{
	this.m_commands = new Array();
	this.m_filters = new Array();
	this.m_isRunning = false;
	this.m_timeoutID = null;
}

CommandQueue.post = function(command)
{
	CommandQueue.s_instance.post(command);
};

CommandQueue.addFilter = function(filter)
{
	CommandQueue.s_instance.addFilter(filter);
};

CommandQueue.removeFilter = function(filter)
{
	CommandQueue.s_instance.removeFilter(filter);
};

CommandQueue.prototype.addFilter = function(filter)
{
	this.m_filters[this.m_filters.length] = filter;
};

CommandQueue.prototype.removeFilter = function(filter)
{
	var found = Arrays.removeFromArray(this.m_filters, filter);
	
	if (! found)
	{
		throw "CommandQueue.remove(): command '" + command + "' not found";
	}
};

CommandQueue.prototype.post = function(command)
{
	this.add(command);
	this.start();
};

CommandQueue.prototype.add = function(command)
{
	this.m_commands[this.m_commands.length] = command;
};

CommandQueue.prototype.remove = function(command)
{
	var found = Arrays.removeFromArray(this.m_commands, command);
	
	if (! found)
	{
		throw "CommandQueue.remove(): command '" + command + "' not found";
	}
};

CommandQueue.prototype.pop = function()
{
	var result = this.m_commands[0];
	this.remove(result);
	return result;
};

CommandQueue.prototype.asArray = function()
{
	var result = new Array();
	for (var i = 0; i < this.m_commands.length; i++)
	{
		result[i] = this.m_commands[i];
	}
	return result;
};

CommandQueue.prototype.runLoop = function()
{
	for (var i = 0; i < this.m_filters.length; i++)
	{
		this.m_filters[i].filter(this);
	}
	
	if (this.m_commands.length == 0)
	{
		this.stop();
		return;
	}
	
	var command = this.pop();
//	Debug.writeln("Executing command: \"" + command.getName() + "\"");
	command.execute();
	
	this.m_timeoutID = setTimeout("CommandQueue.s_instance.runLoop()", CommandQueue.INTERVAL);
};

CommandQueue.prototype.start = function()
{
	if (! this.m_isRunning)
	{
		this.m_isRunning = true;
		
		// delay execution a moment incase another command is posted (i.e. for coalescing/filtering)
		this.m_timeoutID = setTimeout("CommandQueue.s_instance.runLoop()", CommandQueue.INTERVAL);
	}
};

CommandQueue.prototype.stop = function()
{
	if (this.m_isRunning)
	{
		clearTimeout(this.m_timeoutID);
		this.m_timeoutID = null;
		this.m_isRunning = false;
	}
};


/**
 * Command
 */
function Command(name)
{
	this.initCommand(name);
}

Command.prototype.initCommand = function(name)
{
	this.m_name = name;
};

Command.prototype.execute = function()
{
	throw "Error: Command.execute() not implemented for '" + this.m_name + "'";
};

Command.prototype.toString = function()
{
	return "Command[Name: " + this.getName() + "]";
};

Command.prototype.getName = function()
{
	return this.m_name;
};


/**
 * CommandFilter
 */
function CommandFilter(name)
{
	this.initCommandFilter(name);
}

CommandFilter.prototype.initCommandFilter = function(name)
{
	this.m_name = name;
};

CommandFilter.prototype.filter = function(commandQueue)
{
	throw "Error: CommandFilter.filter() not implemented for '" + this.m_name + "'";
};


/**
 * Loop
 */
Loop.s_nextID = 0;
Loop.s_instances = new Object();

function Loop(runnable)
{
	this.m_id = Loop.s_nextID++;
	this.m_runnable = runnable;
	this.m_interval = null;
	this.m_timeoutID = null;
	this.m_running = false;
	Loop.s_instances[String(this.m_id)] = this;
}

Loop.prototype.isRunning = function()
{
	return this.m_running;
}

Loop.prototype.start = function(interval)
{
	if (this.m_running)
		return;
		// happens in FFox due to lost MOut event
		//throw "Loop.start(): loop is already running";
		
	this.m_interval = interval;
	this.m_running = true;
	this.loop();
};

Loop.prototype.loop = function()
{
	if (this.m_running)
	{
		this.m_runnable.run();
		this.m_timeoutID = setTimeout("Loop.s_instances[" + String(this.m_id) + "].loop()", this.m_interval);
	}
};

Loop.prototype.stop = function()
{
	clearTimeout(this.m_timeoutID);
	this.m_running = false;
	this.m_interval = null;
	this.m_timeoutID = null;
};


/**
 * AbstractEffect
 */

/**
 * Constructor
 * @param interval the animation delay in ms
 */
function AbstractEffect(interval)
{
	this.initAbstractEffect(interval);
}

AbstractEffect.prototype.initAbstractEffect = function(interval)
{
	this.m_interval = (interval != null) ? interval : -1;
	this.m_target = null;
	this.m_loop = null;
};

AbstractEffect.prototype.toString = function()
{
	return String(this.constructor) + 
		"[interval: " + this.m_interval + "]";
};

AbstractEffect.prototype.getInterval = function()
{
	return this.m_interval;
};

AbstractEffect.prototype.setInterval = function(interval)
{
	this.m_interval = interval;
};

AbstractEffect.prototype.getTarget = function()
{
	return this.m_target;
};

AbstractEffect.prototype.setTarget = function(target)
{
	this.m_target = target;
};

AbstractEffect.prototype.startLoop = function()
{
	if (this.m_loop == null)
	{
		this.m_loop = new Loop(this);
	}
	this.m_loop.start(this.m_interval);
};

AbstractEffect.prototype.stopLoop = function()
{
	this.m_loop.stop();
};

AbstractEffect.prototype.isRunning = function()
{
	return this.m_loop != null && this.m_loop.isRunning();
};

AbstractEffect.prototype.start = function()
{
	this.doStart(this.getTarget());
//	this.step();
};

/**
 * Called by Loop
 */
AbstractEffect.prototype.run = function()
{
	this.doStep(this.getTarget());
};

AbstractEffect.prototype.finish = function()
{
	this.doFinish(this.getTarget());
};

/**
 * Override this method in subclasses to change text effect
 */
AbstractEffect.prototype.doStart = function(target) {};

/**
 * Override this method in subclasses to change text effect
 */
AbstractEffect.prototype.doStep = function(target) {};

/**
 * Override this method in subclasses to restore normal text
 */
AbstractEffect.prototype.doFinish = function(target) {};


/**
 * AbstractTextEffect
 */
AbstractTextEffect.prototype = new AbstractEffect();
AbstractTextEffect.prototype.constructor = AbstractTextEffect;

/**
 * Constructor
 * @param interval the animation delay in ms
 */
function AbstractTextEffect(interval)
{
	this.initAbstractTextEffect(interval);
}

AbstractTextEffect.prototype.initAbstractTextEffect = function(interval)
{
	this.initAbstractEffect(interval);
	this.m_parts = null;
};

/**
 * Prepares the text element in the specified SPAN tag for manipulation of individual letters. 
 * Encapsulates each letter of the text in it's own SPAN tag. 
 * @param target, must be a span containing a single text element
 */
AbstractTextEffect.prototype.setTarget = function(target)
{
	this.m_target = target;
	this.m_parts = new Array();
	
	if (! target.AbstractTextEffect_hasBeenSplit) // dont split twice if bound to 2 anchors
	{
		var chars = target.firstChild.data.split("");
		target.removeChild(target.firstChild);
		var subSpan;
		var text;
		for (var i = 0; i < chars.length; i++)
		{
			subSpan = document.createElement("SPAN");
			text = document.createTextNode(chars[i]);
			subSpan.appendChild(text);
			target.appendChild(subSpan);
			this.m_parts[i] = subSpan;
		}
		target.AbstractTextEffect_hasBeenSplit = true;
	}
	
	for (var i = 0; i < target.childNodes.length; i++)
	{
		this.m_parts[i] = target.childNodes.item(i);
	}
};


/**
 * StartEffectCommand
 */
StartEffectCommand.prototype = new Command();
StartEffectCommand.prototype.constructor = StartEffectCommand;

function StartEffectCommand(effect)
{
	this.initCommand("Start Effect Command");
	this.m_effect = effect;
}

StartEffectCommand.prototype.getEffect = function()
{
	return this.m_effect;
};

StartEffectCommand.prototype.execute = function()
{
	this.m_effect.start();
};


/**
 * StopEffectCommand
 */
StopEffectCommand.prototype = new Command();
StopEffectCommand.prototype.constructor = StopEffectCommand;

function StopEffectCommand(effect)
{
	this.initCommand("Stop Effect Command");
	this.m_effect = effect;
}

StopEffectCommand.prototype.getEffect = function()
{
	return this.m_effect;
};

StopEffectCommand.prototype.execute = function()
{
	this.m_effect.finish();
};


/**
 * StopStartFilter
 * Removes stop/start commands which are posted in immediate succession 
 */
StopStartFilter.prototype = new CommandFilter();
StopStartFilter.prototype.constructor = StopStartFilter;

function StopStartFilter()
{
	this.initCommandFilter("StopStartFilter");
}

StopStartFilter.prototype.filter = function(commandQueue)
{
	var commands = commandQueue.asArray();
	
	// maps the stop effect command to it's effect
	var stopCommands = new Map();
	
	for (var i = 0; i < commands.length; i++)
	{
		var effect = commands[i].getEffect();
		if (commands[i].constructor == StartEffectCommand)
		{
			var previousCmd = stopCommands.remove(effect);
			if (previousCmd != null)
			{
				commandQueue.remove(previousCmd);
				commandQueue.remove(commands[i]);
				continue;
			}
		}
		if (commands[i].constructor == StopEffectCommand)
		{
			stopCommands.put(effect, commands[i]);
		}
	}
};


/**
 * EffectHandler
 */
EffectHandler.EVENT_MOUSEOVER = 0;
EffectHandler.EVENT_MOUSEDOWN = 1;
EffectHandler.EVENT_MOUSEUP = 2;
EffectHandler.EVENT_MOUSEOUT = 3;

CommandQueue.addFilter(new StopStartFilter());

function EffectHandler(trigger, target, effect)
{
	this.m_trigger = trigger;
	this.m_target = target;
	this.m_effect = (effect != null) ? effect : new AbstractEffect();
	this.m_lastEvent = null;
}

EffectHandler.prototype.doMouseOver = function(event)
{
//	Debug.prettyPrint(event);
//	Debug.writeln("EffectHandler.doMouseOver()");
	if (this.m_lastEvent == EffectHandler.EVENT_MOUSEOVER)
	{
		this.doMouseOut(event);
	}
	
	CommandQueue.post(new StartEffectCommand(this.m_effect));
	this.m_lastEvent = EffectHandler.EVENT_MOUSEOVER;
};

EffectHandler.prototype.doMouseDown = function(event)
{
	this.m_lastEvent = EffectHandler.EVENT_MOUSEDOWN;
};

EffectHandler.prototype.doMouseUp = function(event)
{
	this.m_lastEvent = EffectHandler.EVENT_MOUSEUP;
};

EffectHandler.prototype.doMouseOut = function(event)
{
//	Debug.writeln("EffectHandler.doMouseOut()");
	
	CommandQueue.post(new StopEffectCommand(this.m_effect));
	this.m_lastEvent = EffectHandler.EVENT_MOUSEOUT;
};

EffectHandler.prototype.toString = function()
{
	return "EffectHandler [triggerID: \"" + this.m_trigger.id + "\", targetID: \"" + this.m_target.id + "\"]";
};

/**
 * Binds a handler to the trigger element with the specified id. 
 * An effect is specified which acts on the target element when the 
 * mouse interacts with the trigger. 
 * 
 * @param triggerID the element which we will add dynamic behavior to
 * @param targetID the id of the element which will be manipulated by the effect
 * @param effect effect object to apply
 */
EffectHandler.bind = function(triggerID, targetID, effect)
{
	var trigger = document.getElementById(triggerID);
	if (trigger == null)
		throw "Invalid triggerID: '" + triggerID + "'";
	
	var target = document.getElementById(targetID);
	if (target == null)
		throw "Invalid targetID: '" + targetID + "'";
		
	EffectHandler.bindImpl(trigger, target, effect);
};

EffectHandler.bindImpl = function(trigger, target, effect)
{
	effect.setTarget(target);
	var handler = new EffectHandler(trigger, target, effect);
	trigger.onmouseover = MouseEventMulticaster.create(trigger.onmouseover, function(event) { handler.doMouseOver(event); });
	trigger.onmousedown = MouseEventMulticaster.create(trigger.onmousedown, function(event) { handler.doMouseDown(event); });
	trigger.onmouseup = MouseEventMulticaster.create(trigger.onmouseup, function(event) { handler.doMouseUp(event); });
	trigger.onmouseout = MouseEventMulticaster.create(trigger.onmouseout, function(event) { handler.doMouseOut(event); });
};

/**
 * Binds a handler to the any element with the specified class name. 
 * If specified, the element with triggerID will be used as the trigger, 
 * otherwise the target element itself will trigger the effect. 
 * 
 * @param triggerID the id of an element, or null
 * @param className the class to apply the effect to
 * @param prototypeEffect effect object to apply
 */
EffectHandler.bindToClass = function(triggerID, className, prototypeEffect)
{
	var trigger = null;
	var elements = document.getElementsByTagName("*");
	var classNameExpr = new RegExp('\\b'+className+'\\b');
	
	if (triggerID != null)
	{
		trigger = document.getElementById(triggerID);
		
		if (trigger == null)
			throw "Invalid triggerID: '" + triggerID + "'";
	}
	
	if (elements == null && document.all) // IE 5 [Win] hack
	{
		elements = document.all;
	}
	
	var foundInstance = false; // just for assertions
	for (var i = 0; i < elements.length; i++)
	{
		if (elements[i].className && elements[i].className.match(classNameExpr) != null)
		{
			EffectHandler.bindImpl((trigger != null) ? trigger : elements[i], elements[i], EffectHandler.clone(prototypeEffect));
			foundInstance = true;
		}
	}
	
	if (! foundInstance)
		throw "No elements found with class name: '" + className + "'";
};

EffectHandler.clone = function(effect)
{
	var result = new Object();
	
	for (member in effect)
	{
		result[member] = effect[member];
	}
	return result;
};


/**
 * MouseEventMulticaster
 */
function MouseEventMulticaster(handlerF1, handlerF2)
{
	this.m_handlerF1 = handlerF1;
	this.m_handlerF2 = handlerF2;
}

MouseEventMulticaster.prototype.onMouseEvent = function(event)
{
	this.m_handlerF1(event);
	this.m_handlerF2(event);
};

MouseEventMulticaster.create = function(handlerF1, handlerF2)
{
	if (handlerF1 == null && handlerF2 == null)
		throw "MouseEventMulticaster.create(), handlerF1 & handlerF2 cannot both be null";
	
	if (handlerF2 == null)
	{
		return handlerF1;
	}
	else if (handlerF1 == null)
	{
		return handlerF2;
	}
	else
	{
		return function(event) { new MouseEventMulticaster(handlerF1, handlerF2).onMouseEvent(event); }
	}
};


/**
 * DemoEffect
 */
DemoEffect.prototype = new AbstractEffect();
DemoEffect.prototype.constructor = DemoEffect;

/**
 * Constructor
 * @param interval the animation delay in ms
 */
function DemoEffect(interval)
{
	this.initDemoEffect(interval);
}

DemoEffect.prototype.initDemoEffect = function(interval)
{
	this.initAbstractEffect(interval);
	this.m_origColor = null;
	this.m_origBGColor = null;
	this.m_on = false;
}

DemoEffect.prototype.doStart = function(target)
{
	this.m_origColor = target.style.color;
	this.m_origBGColor = target.style.backgroundColor;
	this.startLoop();
};

DemoEffect.prototype.doStep = function(target)
{
	this.m_on = ! this.m_on;
	if (this.m_on)
	{
		target.style.color = "white";
		target.style.backgroundColor = "#0000B0";
	}
	else
	{
		target.style.color = this.m_origColor;
		target.style.backgroundColor = this.m_origBGColor;
	}
};

DemoEffect.prototype.doFinish = function(target)
{
	this.stopLoop();
	target.style.color = this.m_origColor;
	target.style.backgroundColor = this.m_origBGColor;
};

/**
 * DemoTextEffect
 */
DemoTextEffect.prototype = new AbstractTextEffect();
DemoTextEffect.prototype.constructor = DemoTextEffect;

/**
 * Constructor
 * @param interval the animation delay in ms
 */
function DemoTextEffect(interval)
{
	this.initDemoTextEffect(interval);
}

DemoTextEffect.prototype.initDemoTextEffect = function(interval)
{
	this.initAbstractTextEffect(interval);
};

DemoTextEffect.prototype.doStart = function(target)
{
	this.startLoop();
};

DemoTextEffect.prototype.doStep = function(target)
{
	for (var i = 0; i < this.m_parts.length; i++)
	{
		this.m_parts[i].style.color = "rgb(" + new String(Math.floor(Math.random() * 255)) + "," + new String(Math.floor(Math.random() * 255)) + "," + new String(Math.floor(Math.random() * 255)) + ")";
	}
};

DemoTextEffect.prototype.doFinish = function(target)
{
	this.stopLoop();
	for (var i = 0; i < this.m_parts.length; i++)
	{
		this.m_parts[i].style.color = "";
	}
};

