/*
 * Bind():
 *
 *
 */
Function.prototype.Bind = function() {
  var method = this, args = Array.Clone(arguments), context = args.shift();
	return function() {
		method.apply(context, args);
	}
}



/*
 * BindWithEvent():
 *
 *
 */
Function.prototype.BindWithEvent = function() {
  var method = this, args = Array.Clone(arguments), bubbleEvent = args.shift(), context = args.shift();
	return function(event) {
		var e = event || window.event;
		args.push(e);
		method.apply(context, args);

		// stop event bubbles when asked
		if(!bubbleEvent) {
			e.returnValue = bubbleEvent;
			if(e.cancelable) e.preventDefault();
		}
	}
}


/*
 * Clone():
 *
 *
 */
Array.Clone = function(iterable) {
  if (!iterable) return [];
  if (iterable.toArray) {
    return iterable.toArray();
  } else {
    var results = [];
    for (var i = 0; i < iterable.length; i++)
      results.push(iterable[i]);
    return results;
  }
}


/*
 * Extend():
 * A method to simulate inheritance
 *
 */
Object.Extend = function(destination, source) {
  for (property in source) {
    destination[property] = source[property];
  }
  return destination;
}


/*
 * Class Events:
 * A helper class for doing event hooking/unhooking
 *
 */
var Events = {
	//
	// __events:
	// a list of all event handlers in the current page
	//
	__events: [],

	//
	// AddEventListener():
	// Attach an event handler to listen to a particular event for an object
	//
	AddEventListener: function(source, type, delegate, captures) {
		captures = captures? captures : false;
		if(document.addEventListener) source.addEventListener(type, delegate, captures);
		else if(document.attachEvent) source.attachEvent('on' + type, delegate);
		else source['on' + type] = delegate;

		Events.__events.push({s:source,e:type,d:delegate});
	},

	//
	// RemoveEventListener():
	// Detach an event handler to listen to a particular event from an object
	//
	RemoveEventListener: function(source, type, delegate) {
		if(document.addEventListener) source.removeEventListener(type, delegate, false);
		else if(document.attachEvent) source.detachEvent('on' + type, delegate);
		else source['on' + type] = null;
	},

	//
	// Dispose():
	// Clear up memory by detaching all event handlers this class is handling
	//
	Dispose: function() {
		for(var i=0; i<Events.__events.length; i++) {
			var e = Events.__events[i];
			Events.RemoveEventListener(e.s, e.e, e.d);
		}
	}
}


/*
 * Class Ajax:
 * A wrapper class for handling Ajax calls
 *
 */
var Ajax = {
	//
	// Request:
	// An info object containing ajax call that we are going to make
	//
	Request: function(method, url, body, cache, callback, params) {
		this.Method = method || Ajax.Method.Get;		// request method
		this.Url = url || null;											// location where the data is
		this.Body = body || null;										// request body, used in POST method
		this.CacheInterval = cache || 300000;				// how long to cache request output (in millisecs), default is 5 mins (5 * 60 * 1000ms)
		this.OnRequestComplete = callback || null;	// callback method after the request has completed
		this.Params = params || null;								// callback params, will be passed into the callback method as part of Ajax.RequestCompleteEventArgs
	},


	//
	// RequestCompleteEventArgs:
	// Event arguments object being passed to a method that handles RequestComplete event of Ajax
	//
	RequestCompleteEventArgs: function(xml, params) {
		this.Xml = xml || null;
		this.Params = params || [];
	},


	//
	// [Enum]
	// Method:
	// Request method of the Ajax call
	//
	Method: {
		Get: "GET",
		Post: "POST"
	},


	//
	// [Enum]
	// ReadyState:
	// Ready state of the fetch object
	//
	ReadyState: {
		Uninitialized: 0,
		Loading: 1,
		Loaded:2,
		Interactive:3,
		Complete: 4
	},


	//
	// F:
	// XmlHttpRequest fetch object
	//
	F: null,


	//
	// Cache:
	// Internal cache to hold request output temporarily
	//
	Cache: [],


	//
	// CacheItem:
	// A cache item containing response/output of an Ajax call and its associated expiry timestamp
	//
	CacheItem: function(data, expiry) {
		this.Data = data || null;	// response/output to cache
		this.Expiry = expiry || null;	// expiry in milliseconds (use Date.getTime() for expiry date comparison)
	},


	//
	// Init:
	// Initialise the fetch object
	//
	Init: function() {
		try {
			Ajax.F = new ActiveXObject("Msxml2.XMLHTTP");
		} catch(ex){
			try {
				Ajax.F = new ActiveXObject("Microsoft.XMLHTTP");
			} catch(ex2) {
				Ajax.F = null;
			}
		}
		
		if(!Ajax.F && typeof XMLHttpRequest != "undefined") {
			Ajax.F = new XMLHttpRequest();
		}
	},


	//
	// Send():
	// Let's run an Ajax call!
	//
	// request -> an instance of an Ajax.Request object containing request information
	// params -> params to be passed into callback method if any
	Send: function(request) {
		try {
			if(!Ajax.F) throw("Cannot instantiate XmlHttpRequest object.");
			if(!request || !(request instanceof Ajax.Request)) throw("The method argument 'request' is not a valid type.");
			if(!request.Url) throw("Url is not specified.");

			var c = Ajax.Cache[request.Url];
			if(request.CacheInterval > 0 && c && c.Expiry > (new Date()).getTime()) {
				(request.OnRequestComplete || function(){})(new Ajax.RequestCompleteEventArgs(c.Data, request.Params));
				return;
			}

			//alert("Fetching new data...");
			// allow only one request at a time, please
			if (Ajax.F.readyState != Ajax.ReadyState.Uninitialize && Ajax.F.readyState != Ajax.ReadyState.Complete)
				this.F.abort();

			// force browser not to read from its internal cache and fetch data from server
			var url = request.Url + (request.Url.indexOf("?") == -1? "?" : "&") + encodeURIComponent((new Date()).getTime());
			Ajax.F.open(request.Method, url, true);
			Ajax.F.setRequestHeader("Content-Type","text/xml");
			Ajax.F.onreadystatechange = function() {
				if(Ajax.F.readyState == Ajax.ReadyState.Complete && Ajax.F.status == 200 && Ajax.F.responseXML) {
					var c = new Ajax.CacheItem(Ajax.F.responseXML, (new Date()).getTime() + request.CacheInterval);
					Ajax.Cache[request.Url] = c;
					(request.OnRequestComplete || function(){})(new Ajax.RequestCompleteEventArgs(c.Data, request.Params));
				}
			};
			Ajax.F.send(request.Body);


		} catch(e) {
			throw("Ajax.Send(): " + e);
		}
	},


	//
	// Dispose:
	// Clean up the Ajax object
	//
	Dispose: function() {
		if(Ajax.F) Ajax.F = null;
		if(Ajax.F && typeof(Ajax.F.onreadystatechange) == "function") Ajax.F.onreadystatechange = null;
		Ajax.Cache = null;
	}
}

// Instantiate our Ajax helper object now!
Ajax.Init();



/*
 * Class Animation:
 * 
 *
 */
Animation = {
	//
	// AccelerationFunction():
	// Define the base method for an acceleration function
	//
	AccelerationFunction: function(e,steps) {
		var b=null,a=(steps||63),d=e;

		this.SetSteps = function(d) {
			a = d;
			b = null;
			c();
		};

		this.GetSteps=function() {
			return a;
		};

		this.GetValue=function(e) {
			if(!b) c();
			var d=parseInt(Math.round(e*a));
			if(d<0)d=0;
			if(d>a)d=a;
			return b[d];
		};

		this.GetTotal=function() {
			return this.GetValue(1);
		};

		function c() {
			b=[];
			b[0]=0;
			for(var c=1;c<=a;c++)
				b[c]=b[c-1]+d(c/a);
		}
	}
};


/*
 * Class Animation.AccelerationFunctions:
 * Implementations of different acceleration functions, by supplying different return value for an acceleration function we are able to
 * control how an animation runs
 *
 */
Animation.AccelerationFunctions = {
	Linear: new Animation.AccelerationFunction(function() {
		return 1;
	}),
	
	ExponentialAcc: new Animation.AccelerationFunction(function(b) {
		var a=0,d=1,c=d-a,f=a+b*c,e=Math.pow(f,2);
		return e;
	}),
	
	ExponentialDec: new Animation.AccelerationFunction(function(b) {
		var a=-1,d=0,c=d-a,f=a+b*c,e=Math.pow(f,2);
		return e;
	}),
	
	CosineWave: new Animation.AccelerationFunction(function(b) {
		var a=-Math.PI,d=Math.PI,c=d-a,f=a+b*c,e=Math.cos(f)+1;
		return e;
	}),
	
	CrazyElevator: new Animation.AccelerationFunction(function(b) {
		var a=-5,d=5,c=d-a,f=a+b*c,e=2/(Math.pow(Math.abs(f),3)+1);
		return e;
	}),

	CrazyElevator7: new Animation.AccelerationFunction(function(b) {
		var a=-5,d=5,c=d-a,f=a+b*c,e=2/(Math.pow(Math.abs(f),3)+1);
		return e;
	},7),

	CrazyElevator14: new Animation.AccelerationFunction(function(b) {
		var a=-5,d=5,c=d-a,f=a+b*c,e=2/(Math.pow(Math.abs(f),3)+1);
		return e;
	},14)
};


/*
 * Class Roller:
 *
 *
 * Note:
 * o. The use of css margin for the container is not recommended due to ie6 bug.
 * o. It's highly recommended to set the dimension of the container to prevent flash of unstyled content and better animation effect.
 * o. When calculating the container's dimension, don't forget the take into account the margin/padding/border of it
 *
 */
Animation.Roller = function(container, view) {
	// keep a reference to the this object
	var context = this;

	// variables
	var _objToRoll = container, _w = null, _h = null; // _w = width of container, _h = height of container

	var _firstChild = null, _y = 0;	// _y = margin top of the first child
	var _isExpanded = view, _isRolling = 0;
	var f = Animation.AccelerationFunctions.CrazyElevator7;
	var	i = 10, j = 10, delay = 10;
	var _r = Animation.Roller;

	// events
	this.OnBeforeRollIn;
	this.BeforeRollIn = function(F) {
		this.OnBeforeRollIn = F;
	};

	this.OnBeforeRollOut;
	this.BeforeRollOut = function(F) {
		this.OnBeforeRollOut = F;
	};

	this.OnRoll;
	this.Roll = function(F) {
		this.OnRoll = F;
	}

	this.OnAfterRollIn;
	this.AfterRollIn = function(F) {
		this.OnAfterRollIn = F;
	};

	this.OnAfterRollOut;
	this.AfterRollOut = function(F) {
		this.OnAfterRollOut = F;
	};

	//
	// SetMinWidth:
	// Set the minimum width of the roll object when rolled in
	//
	this.SetMinWidth = function(n) {
		if(typeof n == "number" && n >= 0) i = Math.floor(n,10)
	};

	//
	// SetMinHeight:
	// Set the minimum height of the roll object when rolled in
	//	
	this.SetMinHeight = function(n) {
		if(typeof n == "number" && n >= 0) j = Math.floor(n,10)
	};

	//
	// SetAccelerationFunction:
	// Set the acceleration function for the animation
	//
	this.SetAccelerationFunction = function(F) {
		if(F instanceof Animation.AccelerationFunction) f=F;
	};

	//
	// SetView:
	// Set the view of the roller object, whether it is expanded or collapsed
	//
	this.SetView = function(v) {
		_isExpanded = v;

		var vw = Animation.Roller.View;
		if(v != vw.Expanded && v != vw.Collapsed) v = vw.Expanded;
	}

	//
	// SetDelay:
	// Set animation delay
	//
	this.SetDelay = function(n) {
		if(typeof n == "number" && n > 0) delay = Math.floor(n,10)
	};

	//
	// IsExpanded:
	// Is the roll object currently expanded?
	//
	this.IsExpanded = function() {
		return _isExpanded;
	};

	//
	// IsRolling:
	// Is the roll object currently rolling?
	//
	this.IsRolling = function() {
		return _isRolling;
	};

	//
	// RollIn:
	// Perform a roll-in animation!
	//
	this.RollIn = function(d) {
		if(_isRolling) return;

		(this.OnBeforeRollIn || function(){})();
		m(_r.RollStyle.In,d);
		_isExpanded = false;
	};

	//
	// RollOut:
	// Perform a roll-out animation!
	//	
	this.RollOut = function(d) {
		if(_isRolling) return;

		(this.OnBeforeRollOut || function(){})();
		m(_r.RollStyle.Out,d);
		_isExpanded = true;
	};

	//
	// m:
	// Private methods that renders the animation by using loop and timeout
	//
	function m(t,m) {
		_isRolling = 1;
		var q=0,r=0;
		if(t==_r.RollStyle.Out) {
			q=i;
			r=j;
		}

		var w=_w,u=_h, y=w-i,z=u-j,
				A=y/f.GetTotal(),	B=z/f.GetTotal(), v=f.GetSteps();

		// pre-calculate paths
		var paths = [];
		for(var s=0;s<=v;s++) {
			var x=s/v;

			var l=parseInt(Math.round(f.GetValue(x)*A))+q,
					n=parseInt(Math.round(f.GetValue(x)*B))+r;

			if(m==_r.RollDirection.TopDown)
				paths[s] = { i: x, p: _y + (n-j) + "px" };
			if(m==_r.RollDirection.BottomUp)
				paths[s] = { i: x, p: (_y - n) + "px" };
			if(m==_r.RollDirection.LeftRight)
				throw("NotImplementedException: Animation.Roller.RollDirection.LeftRight is not supported at the moment");
			if(m==_r.RollDirection.RightLeft)
				throw("NotImplementedException: Animation.Roller.RollDirection.RightLeft is not supported at the moment");
		}

		s = 0;
		var interval = setInterval(C.Bind(context), delay);

		//
		// C:
		// Perform animation with pre-calculated paths
		//
		function C() {
			if(m==_r.RollDirection.TopDown)
				_firstChild.style.marginTop = paths[s].p;
			if(m==_r.RollDirection.BottomUp)
				_firstChild.style.marginTop = paths[s].p;
			if(m==_r.RollDirection.LeftRight)
				throw("NotImplementedException: Animation.Roller.RollDirection.LeftRight is not supported at the moment");
			if(m==_r.RollDirection.RightLeft)
				throw("NotImplementedException: Animation.Roller.RollDirection.RightLeft is not supported at the moment");
			
			if(paths[s].i==1) {
				clearInterval(interval);

				_isRolling = 0;
				if(t==_r.RollStyle.In) {
					_y = parseInt(_firstChild.style.marginTop.substring(0, _firstChild.style.marginTop.indexOf("px")));
					(this.OnAfterRollIn || function(){})();
				}
				else {
					_y = 0;
					(this.OnAfterRollOut || function(){})();
				}
			}
			else {
				s++;
			}

			(this.OnRoll || function(){})();
		}
	};

	//
	// Init:
	// Initialise this roller object
	//
	function Init() {
			
		// create a container div for an object we want to animate
		// check also if the object already have a roller-type container, only creates it when there is no container
		var p = _objToRoll.parentNode;
		if(p && p.__type != "roller") {
			// add a <div style="clear:left"></div> to stop Internet Explorer overflow bug
			var clearDiv = document.createElement("div");
			clearDiv.style.clear = "left";
			_objToRoll.parentNode.insertBefore(clearDiv, _objToRoll.nextSibling);
			_firstChild = _objToRoll.parentNode.removeChild(_objToRoll);

			var containerDiv = document.createElement("div");
			containerDiv.__type = "roller";
			containerDiv.style.overflow = "hidden";
			containerDiv.style.styleFloat = "left";
			containerDiv.style.cssFloat = "left";
			containerDiv.appendChild(_firstChild);
			_objToRoll = containerDiv;
			clearDiv.parentNode.insertBefore(_objToRoll, clearDiv);
		}
		else {
			_firstChild = _objToRoll;
		}

		// if the container's width isn't specified, try to get it programmatically
		if (!container.currentStyle) {
			if(window.getComputedStyle)
				_w = parseInt(document.defaultView.getComputedStyle(container, null).getPropertyValue("width"));
		}
		else _w = parseInt(container.currentStyle.width);

		// try to get the container's height also
		if(!_h) _h = _firstChild.offsetHeight;
		if (!container.currentStyle) {
			if(window.getComputedStyle)
				_h = parseInt(document.defaultView.getComputedStyle(container, null).getPropertyValue("height"));
		}
		else _h = parseInt(container.currentStyle.height);

		// do a check if the view is a valid view
		context.SetView(_isExpanded);
		if(!_isExpanded) {
			_y = -(_h);
			_firstChild.style.marginTop = _y + "px";
		}
	}
	Init();
};

Animation.Roller.RollDirection = {
	TopDown:1,
	RightLeft:2,
	BottomUp:4,
	LeftRight:8
};
	
Animation.Roller.RollStyle = {
	In:0,
	Out:1
};

Animation.Roller.View = {
	Collapsed:0,
	Expanded:1
};

// Garbage collection time!
var Util = {
	Dispose: function() {
		Events.Dispose();
		Ajax.Dispose();
	}
}
Events.AddEventListener(window, 'unload', Util.Dispose);