/*
 * Copyright (c) 2007 	Josh Davis ( http://joshdavis.wordpress.com )
 * 
 * Licensed under the MIT License ( http://www.opensource.org/licenses/mit-license.php ) as follows:
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
*/

/**
 * Binds a function to the given object's scope
 *
 * @param {Object} object The object to bind the function to.
 * @return {Function}	Returns the function bound to the object's scope.
 */
Function.prototype.bind = function (object)
{
	var method = this;
	return function ()
	{
		return method.apply(object, arguments);
	};
};

/**
 * Create a new instance of Event.
 *
 * @classDescription	This class creates a new Event.
 * @return {Object}	Returns a new Event object.
 * @constructor
 */
function Event()
{
	this.events = [];
	this.builtinEvts = [];
}

/**
 * Gets the index of the given action for the element
 *
 * @memberOf Event
 * @param {Object} obj The element attached to the action.
 * @param {String} evt The name of the event.
 * @param {Function} action The action to execute upon the event firing.
 * @param {Object} binding The object to scope the action to.
 * @return {Number} Returns an integer.
 */
Event.prototype.getActionIdx = function(obj,evt,action,binding)
{
	if(obj && evt)
	{

		var curel = this.events[obj][evt];
		if(curel)
		{
			var len = curel.length;
			for(var i = len-1;i >= 0;i--)
			{
				if(curel[i].action == action && curel[i].binding == binding)
				{
					return i;
				}
			}
		}
		else
		{
			return -1;
		}
	}
	return -1;
};

/**
 * Adds a listener
 *
 * @memberOf Event
 * @param {Object} obj The element attached to the action.
 * @param {String} evt The name of the event.
 * @param {Function} action The action to execute upon the event firing.
 * @param {Object} binding The object to scope the action to.
 * @return {null} Returns null.
 */
Event.prototype.addListener = function(obj,evt,action,binding)
{
	if(this.events[obj])
	{
		if(this.events[obj][evt])
		{
			if(this.getActionIdx(obj,evt,action,binding) == -1)
			{
				var curevt = this.events[obj][evt];
				curevt[curevt.length] = {action:action,binding:binding};
			}
		}
		else
		{
			this.events[obj][evt] = [];
			this.events[obj][evt][0] = {action:action,binding:binding};
		}
	}
	else
	{
		this.events[obj] = [];
		this.events[obj][evt] = [];
		this.events[obj][evt][0] = {action:action,binding:binding};
	}
};

/**
 * Removes a listener
 *
 * @memberOf Event
 * @param {Object} obj The element attached to the action.
 * @param {String} evt The name of the event.
 * @param {Function} action The action to execute upon the event firing.
 * @param {Object} binding The object to scope the action to.
 * @return {null} Returns null.
 */
Event.prototype.removeListener = function(obj,evt,action,binding)
{
	if(this.events[obj])
	{
		if(this.events[obj][evt])
		{
			var idx = this.actionExists(obj,evt,action,binding);
			if(idx >= 0)
			{
				this.events[obj][evt].splice(idx,1);
			}
		}
	}
};

/**
 * Fires an event
 *
 * @memberOf Event
 * @param e [(event)] A builtin event passthrough
 * @param {Object} obj The element attached to the action.
 * @param {String} evt The name of the event.
 * @param {Object} args The argument attached to the event.
 * @return {null} Returns null.
 */
Event.prototype.fireEvent = function(e,obj,evt,args)
{
	if(!e){e = window.event;}

	if(obj && this.events)
	{
		var evtel = this.events[obj];
		if(evtel)
		{
			var curel = evtel[evt];
			if(curel)
			{
				for(var act in curel)
				{
					var action = curel[act].action;
					if(curel[act].binding)
					{
						action = action.bind(curel[act].binding);
					}
					action(e,args);
				}
			}
		}
	}
};
/**
 * Checks for the existance of a builtin listener
 *
 * @memberOf Event
 * @param {Object} obj The element attached to the action.
 * @param {String} evt The name of the event.
 * @param {Function} action The action to execute upon the event firing.
 * @param {Object} binding The object to scope the action to.
 * @return {Object} Returns a builtin event object.
 */
Event.prototype.getBuiltinListenerIdx = function(obj,evt,action,binding)
{
	for(var i = this.builtinEvts.length-1;i >= 0;i--)
	{
		var ArrayEl = this.builtinEvts[i];
		if(ArrayEl)
			if(ArrayEl.obj == obj && ArrayEl.evt == evt && ArrayEl.action == action && ArrayEl.binding == binding)
				return i;
	}
	return -1;
};
/**
 * Adds a builtin listener
 *
 * @memberOf Event
 * @param {Object} obj The element attached to the action.
 * @param {String} evt The name of the event.
 * @param {Function} action The action to execute upon the event firing.
 * @param {Object} binding The object to scope the action to.
 * @return {null} Returns null.
 */
Event.prototype.addBuiltinListener = function(obj,evt,action,binding)
{
	if(obj && evt && action)
	{
		if(this.getBuiltinListenerIdx(obj,evt,action,binding) < 0)
		{
			var boundAction = action;
			if(binding)
				boundAction = action.bind(binding);
			if(obj.addEventListener)
				obj.addEventListener(evt,boundAction,false);
			else if(obj.attachEvent)
				obj.attachEvent('on' + evt,boundAction);
			else
				obj['on' + evt] = boundAction;
			this.builtinEvts[this.builtinEvts.length] = {obj:obj,evt:evt,action:action,binding:binding,boundAction:boundAction};
		}
	}
};
/**
 * Removes a builtin listener
 *
 * @memberOf Event
 * @param {Object} obj The element attached to the action.
 * @param {String} evt The name of the event.
 * @param {Function} action The action to execute upon the event firing.
 * @param {Object} binding The object to scope the action to.
 * @return {null} Returns null.
 */
Event.prototype.removeBuiltinListener = function(obj,evt,action,binding)
{
	for(var i = this.builtinEvts.length-1;i >= 0;i--)
	{
		var ArrayEl = this.builtinEvts[i];
		if(ArrayEl)
		{
			if(obj && evt && action && binding)
				if(ArrayEl.obj == obj && ArrayEl.evt == evt && ArrayEl.action == action && ArrayEl.binding == binding)
				{
					this.detachListener(ArrayEl,i);
					break;
				}
			else if(obj && evt && action)
				if(ArrayEl.obj == obj && ArrayEl.evt == evt && ArrayEl.action == action)
					this.detachListener(ArrayEl,i);
			else if(obj && evt)
				if(ArrayEl.obj == obj && ArrayEl.evt == evt)
					this.detachListener(ArrayEl,i);
			else if(obj)
				if(ArrayEl.obj == obj)
					this.detachListener(ArrayEl,i);
			else
				this.detachListener(ArrayEl,i);
		}
	}
};
/**
 * Detaches a builtin listener from a DOM object (prevents memory leaks)
 *
 * @memberOf Event
 * @param {Object} arrayEl The builtin listener object.
 * @param {Number} idx The index of the object in the builtin listener array.
 * @return {null} Returns null.
 */
Event.prototype.detachListener = function(arrayEl,idx)
{
	var obj = arrayEl.obj;
	var evt = arrayEl.evt;
	var boundAction = arrayEl.boundAction;

	if(obj.removeEventListener)
		obj.removeEventListener(evt,boundAction,false);
	evt = 'on' + evt;
	if(obj.detachEvent)
		obj.detachEvent(evt,boundAction);
	obj[evt] = null;

	delete arrayEl.obj;
	delete arrayEl.evt;
	delete arrayEl.action;
	delete arrayEl.binding;
	delete arrayEl.boundAction;

	delete this.builtinEvts[idx];
	this.builtinEvts.splice(idx,1);
};

/**
 * @memberOf CollapsibleBox
 * @param {number} duration Time in milliseconds to open/close
 * @param {boolean} fixbottom Whether to fix the content to the bottom (true) of the container or not (false)
 * @param {boolean} startopen Start open (true) or closed (false)
 * @constructor
 */
function CollapsibleBox(duration,fixbottom,startopen)
{
	this.fixbottom = fixbottom;
	this.open = startopen;
	this.duration = duration;
	this.currentframe = 0;
	this.animationState = 'end';

	this.collapse = document.createElement('div');
	this.collapse.className = 'collapse';
	this.collapse.style.display = startopen?'block':'none';
	this.content = document.createElement('div');
	this.content.className = 'collapseContent';
	this.collapse.appendChild(this.content);
}
/**
 * Returns the main div of the collapsible box
 *
 * @memberOf CollapsibleBox
 * return {DOMNode}
 */
CollapsibleBox.prototype.getContainer = function()
{
	return this.collapse;
};
/**
 * Returns the content div of the collapsible box
 *
 * @memberOf CollapsibleBox
 * return {DOMNode}
 */
CollapsibleBox.prototype.getContent = function()
{
	return this.content;
};
/**
 * Shows the contents
 *
 * @memberOf CollapsibleBox
 */
CollapsibleBox.prototype.showBox = function()
{
	this.collapse.style.height = '1px';
	this.collapse.style.display = 'block';
	this.open = true;
	this.currentframe = 1;
	this.animationState = 'opening';
	gEVENT.fireEvent(null,this,'opening',this);
	window.setTimeout(this.animate.bind(this),1);
};
/**
 * Animates the opening/closing of the contents
 *
 * @memberOf CollapsibleBox
 */
CollapsibleBox.prototype.animate = function()
{
	if(this.animationState != 'end')
	{
		var height = this.collapse.scrollHeight;
		var fps = 30/1000;
		var fpd = this.duration * fps;
		var pxPerFrame = height/fpd;
		if(this.animationState == 'closing')
		{
			var newheight = Math.round(height - (pxPerFrame * this.currentframe));
		}
		else
		{
			var newheight = Math.round(pxPerFrame * this.currentframe);
		}

		this.collapse.style.height = newheight + 'px';
		var st = newheight - height;
		if(this.fixbottom)
		{
			this.content.style.top = st + 'px';
		}
		this.currentframe++;
		if(st < 0 && Math.abs(st) < height)
		{
			window.setTimeout(this.animate.bind(this),fps);
		}
		else
		{
			var state = this.animationState;
			this.animationState = 'end';

			if(state == 'closing')
			{
				this.collapse.style.display = 'none';
				this.collapse.style.height = 'auto';
				gEVENT.fireEvent(null,this,'close',this);
			}
			else if(state == 'opening')
			{
				this.collapse.style.height = 'auto';
				gEVENT.fireEvent(null,this,'open',this);
			}
		}
	}
};
/**
 * Hides the contents
 *
 * @memberOf CollapsibleBox
 */
CollapsibleBox.prototype.hideBox = function()
{
	this.open = false;
	this.currentframe = 1;
	this.animationState = 'closing';
	gEVENT.fireEvent(null,this,'closing',this);
	window.setTimeout(this.animate.bind(this),1);
};
/**
 * Returns whether the box is open or not
 *
 * @memberOf CollapsibleBox
 * @return {boolean}
 */
CollapsibleBox.prototype.isOpen = function()
{
	return this.open;
};

var gEVENT = new Event();

function load()
{
	var testCont = document.getElementById('testCont');
	
	var CB = new CollapsibleBox(1000,true,false);
	var container = CB.getContainer();
	var content = CB.getContent();
	content.appendChild(document.createTextNode('Line 1'));
	content.appendChild(document.createElement('br'));
	content.appendChild(document.createTextNode('Line 2'));
	content.appendChild(document.createElement('br'));
	content.appendChild(document.createTextNode('Line 3'));

	var open = document.createElement('a');
	open.appendChild(document.createTextNode('Open'));
	gEVENT.addBuiltinListener(open,'click',CB.showBox,CB);
	testCont.appendChild(open);
	testCont.appendChild(document.createTextNode(' '));
	var close = document.createElement('a');
	close.appendChild(document.createTextNode('Close'));
	gEVENT.addBuiltinListener(close,'click',CB.hideBox,CB);
	testCont.appendChild(close);
	testCont.appendChild(document.createElement('br'));
	testCont.appendChild(container);
}

load();