var FF_PCTDEC = 0x001;
var FF_PCTTOV = 0x002;
var FF_POINTS = 0x004;
var FF_ROUND  = 0x008;
var FF_STRING = 0x010;
var FF_CSSZRO = 0x020;
var FF_CSSRGB = 0x040;
var FF_CSSHEX = 0x080;
var FF_CSSPCT = 0x100;
var FF_CSSPX = 0x200;

var ANS_ELC_DECREMENT = -1;
var ANS_ELC_INCREMENT = -2;
var ANS_ELC_SYNCHRONIZE = -3;

////////////////////////////////////////////////////////////////////////////////
// This class is used to setup animations on HTML elements.  The HTML         //
// elements that will be affected are accessed either through their ids' or   //
// through class names.  If ids are to be used, then they need to follow a    //
// naming format.  The name has to be valid according to W3C Standards        //
// which means it has to start with a letter and may consists of only         //
// letters and numbers.  It has to use an ascending number system in          //
// decimal or hex which starts at zero.  If the animation is to involve       //
// more than one element, then the number modifier has to be included in      //
// the format parameter.  The 'format' is passed as a string with the '%'     //
// character representing decimals and the '#' character representing hex.    //
// Multiple characters can be inserted to represent if the number should      //
// be zero padded to reach a certain length.                                  //
//                                                                            //
// Format examples:                                                           //
// 'idname%'    (id/class: idname0, idname1, idname2, etc...)                 //
// 'idname%%%a' (id/class: idname000a, idname001a, ..., idname111a, etc...)   //
// 'idname###a' (id/class: idname000a, ..., idnamefeea, idnamefefa, etc...)   //
//                                                                            //
// If a class name is used, it can contain a single identifer without the     //
// numeric system.  If that is the case then the elements are numbered        //
// in according to the order they are found on the page.                      //
//                                                                            //
// Amount indicates how many elements are included in the animation, (only    //
// necessary when the numeric system is used in the names.)                   //
//                                                                            //
// Frames indicates how many frames are in the animation.                     //
//                                                                            //
// Fps is how many frames are displayed per second.                           //
//                                                                            //
// Start and Stop indicate the beginning and ending frames' numeric values.   //
// If the animation is using type 'hexcolor', then the values must be         //
// either a 3 or 6 digit hex number passed as a string.  The number may       //
// be prepended with either '#' or '0x'.                                      //
//                                                                            //
// Type determines the type of animation to perform, or more specifically     //
// how to calculate the frames in between the beginning and ending frame.     //
// 'linear' will give an animation with equal increments between frames.      //
// 'hill' gives an animation where the size of the increments between         //
// frames increases until the middle, where the increments will start to      //
// decline in size following a symmetric curve corresponding to the first     //
// half of the animation.  The resulting curve of the increments in the       //
// animation is similar to that of one half period in a sine wave.            //
// 'ladder' is an animation where the increments between frames rise in       //
// equal amounts until the halfway point of the animation where they start    //
// to decline in equal amounts symmetric to the first half of the             //
// animation.  The resulting slope of the animation is similar to the         //
// look of a standup ladder.                                                  //
// Either string 'dec' or 'hex' needs to be prepended to any of the           //
// types just mentioned to indicate that the output of the animation          //
// frames should be decimal or hexadecimal respectively.                      //
// 'hexcolor' indicates the animation will be a 'linear' representation of    //
// hexadecimal color values.  ('deccolor' is not a valid type.)               //
//                                                                            //
// Modifier is a bitmap field where multiple output modifiers may be OR'd     //
// together.  The possible values are as follows:                             //
// FF_PCTDEC: The result will be a decimal percentage (1.0 == 100%)           //
// FF_PCTTOV: By default, the numerical animations will output percentage     //
//            values between 0-100.  To convert those percentages to real     //
//            numbers useful in the animation, this modifier will multiply    //
//            the percentage values by the range involved in the animation    //
//            to give the actual frame value.                                 //
// FF_POINTS: Makes the frame value contain a certain number of decimal       //
//            places which is given in the 'points' parameter.                //
// FF_ROUND:  Rounds the resulting number to the nearest integer.             //
// FF_STRING: Turns the numeric value into a string if it is not already.     //
// FF_CSSZRO: If the resulting number is equal to 0, then make the frame      //
//            a string equal to '0' not containing anything else to make      //
//            it a valid CSS value.                                           //
// FF_CSSHEX: Prepends '#' to the frame value.                                // 
// FF_CSSPCT: Appends '%' to the frame value.                                 //
// FF_CSSPX:  Appends 'px' to the frame value.                                //
//                                                                            //
// Points is the number of decimal places that should be shown in the frame   //
// results.  Must be used in conjunction with the modifier: FF_POINTS.        //
////////////////////////////////////////////////////////////////////////////////

function AnsAnimate(attr, format, amount, frames, fps, start, stop, type, modifier, points)
{
	// 'Parent' Used to pass the 'this' pointer to the private variables object.
	var t = this;

	// The class 'AnsAnimate' needs to have private variables that can only be
	// accessed by the class and methods of the class including static methods.
	// To do this an instance class needs to be setup that contains a copy of
	// all the private variables which can then be passed to static methods as
	// a single parameter.
	var pv = new function ()
	{
		this.t = t;           // Reference to this instance
		this.attr = attr;         // Attribute used for naming system
		this.format = format;     // The format for the naming system
		this.amount = amount;     // The number of elements used in the animation
		this.frames = frames;     // The number of frames used in the animation
		this.fps = fps;           // The 'Frames Per Second'
		this.start = start;       // The starting value for the animation
		this.stop = stop;         // The stopping value for the animation
		this.type = type;         // The type of animation to use
		this.modifier = modifier; // The format modifier to apply
		this.points = points;     // The number of decimal places to use
		this.running = false;
		// The following variables are set from other functions
		// this.base     // Base of the number system used for the animation
		// this.subtype  // The type of animation without the base prepended
		// this.idbegin  // The beginning portion of the format string
		// this.idbase   // The base of the number system used in the format
		// this.iddigits // The number of digits to zero pad to in name count
		// this.idend    // The end portion of the format string
		// this.delay    // The delay used in calls to setTimeout()
		return true;
	};

	if (!AnsAnimate.verifyParameters(pv))
		return false;
	if (!AnsAnimate.setupValues(pv))
		return false;

	this.frame = new Array(pv.frames);
	this.element = new Array();
	this.active = new Array();
	this.focus = 0;
	AnsAnimate.addElements(pv);
	if (pv.subtype == 'linear')
		AnsAnimate.setLinear(pv);
	else if (pv.subtype == 'hill')
		AnsAnimate.setHill(pv);
	else if (pv.subtype == 'ladder')
		AnsAnimate.setLadder(pv);
	else if (pv.subtype == 'color')
		AnsAnimate.setColor(pv);
	AnsAnimate.applyModifiers(pv);
	this.delay = pv.delay;
	this.frames = pv.frames;
}

////////////////////////////////////////////////////////////////////////////////
// The main purpose of this method is for type checking all of the            //
// parameters that are passed into the class constructor.  It also checks     //
// that the strings contain valid options that the class can recognize.       //
////////////////////////////////////////////////////////////////////////////////

AnsAnimate.verifyParameters = function (p)
{
	var attr   = p.attr;
	var amount = p.amount;
	var frames = p.frames;
	var fps    = p.fps;
	var type   = p.type;
	var start  = p.start;
	var stop   = p.stop;

	var base;
	var subtype;

	var rergb = /rgb\( *(\d{1,3}), *(\d{1,3}), *(\d{1,3}) *\)/;
	var rehex = /(?:#|0x|0X)([0-9A-Fa-f]{3}$|[0-9A-Fa-f]{6}$)/;
	var rgb;

	// Perform type checking
	if (typeof attr != 'string' || typeof p.format != 'string' ||
			typeof amount != 'number' || typeof frames != 'number' ||
			typeof fps != 'number' || typeof type != 'string' ||
			typeof p.modifier != 'number' || typeof p.points != 'number')
		return false;

	// If type of tag attribute is not set to use an ID or a Class
	if (attr != 'id' && attr != 'class')
		return false;

	// If the string for the Type of animation has less than 3 characters,
	// then it is not possible that it is a valid Type.
	if (type.length < 4)
		return false;

	base = type.substr(0, 3);

	// If the number base is not set to a valid base
	if (base != 'dec' && base != 'hex')
		return false;
	else
		base = base == 'dec' ? 10 : 16;

	subtype = type.substr(3, type.length - 3);

	// If the Type is not one of the valid animation types
	if (subtype != 'linear' && subtype != 'hill' &&
			subtype != 'ladder' && subtype != 'color')
		return false;

	// If the type is a color, parse the start and stop values
	if (subtype == 'color')
	{
		if (typeof start == 'string')
		{
			rgb = rergb.exec(start);
			if (rgb != null)
			{
				rgb[1] = parseInt(rgb[1]);
				rgb[2] = parseInt(rgb[2]);
				rgb[3] = parseInt(rgb[3]);
				if (rgb[1] > 255 || rgb[2] > 255 || rgb[3] > 255)
					return false;
				start = (rgb[1] << 16) + (rgb[2] << 8) + rgb[3];
			}
			else
			{
				rgb = rehex.exec(start);
				if (rgb != null)
				{
					if (rgb[1].length == 3)
						start = parseInt(rgb[1].charAt(0) + rgb[1].charAt(0) +
							rgb[1].charAt(1) + rgb[1].charAt(1) +
							rgb[1].charAt(2) + rgb[1].charAt(2), 16);
					else
						start = parseInt(rgb[1], 16);
				}
				else
					return false;
			}
		}
		if (typeof start != 'number')
			return false;

		if (typeof stop == 'string')
		{
			rgb = rergb.exec(stop);
			if (rgb != null)
			{
				rgb[1] = parseInt(rgb[1]);
				rgb[2] = parseInt(rgb[2]);
				rgb[3] = parseInt(rgb[3]);
				if (rgb[1] > 255 || rgb[2] > 255 || rgb[3] > 255)
					return false;
				stop = (rgb[1] << 16) + (rgb[2] << 8) + rgb[3];
			}
			else
			{
				rgb = rehex.exec(stop);
				if (rgb != null)
				{
					if (rgb[1].length == 3)
						stop = parseInt(rgb[1].charAt(0) + rgb[1].charAt(0) +
							rgb[1].charAt(1) + rgb[1].charAt(1) +
							rgb[1].charAt(2) + rgb[1].charAt(2), 16);
					else
						stop = parseInt(rgb[1], 16);
				}
				else
					return false;
			}
		}
		if (typeof stop != 'number')
			return false;
	}
	else
	{
		if (typeof start != 'number')
			start = parseInt(start, base);
		if (typeof stop != 'number')
			stop = parseInt(stop, base);
		if (isNaN(start) || isNaN(stop))
			return false;
	}

	// If the animation doesn't have any elements to use, or there isn't more
	// than one frame, or the fps is not a valid value.
	if (amount < 1 || frames < 2 || fps <= 0)
		return false;

	p.start   = start;
	p.stop    = stop;
	p.base    = base;
	p.subtype = subtype;
	return true;
}

//*****************************************************************************/
//* Sets up other needed variables about the animation which will be          */
//* accessible through the privars object.                                    */
//*****************************************************************************/
AnsAnimate.setupValues = function (p)
{
	var reid = /^([A-Za-z]\w*)(%+|#*)(\w*)$/;
	var id = reid.exec(p.format);
	if (id == null)
		return false;
	p.idbegin = id[1];
	// If a numeric system is found in format, then setup appropriate variables
	if (id[2].length > 0)
	{
		p.idbase = id[2].substr(0, 1) == '%' ? 10 : 16;
		p.iddigits = id[2].length;
	}
	// Else if no numeric system is found but one is needed, then return false
	else if (p.attr == 'id' && p.amount > 1)
		return false;
	// Else it must be either a single id element, or using a class attribute
	else
	{
		p.idbase = 10;
		p.iddigits = 1;
	}
	p.idend = id[3].length > 0 ? id[3] : '';
	if (p.subtype != 'color')
		p.range = p.stop - p.start;
	// Frames need to be incremented by one so that each time the animation plays
	// there are exactly the number of frames played as was passed to the animate
	// object initially. This happens because at the end of an animation, the
	// element essentially sits on the last frame played causing it to not be
	// used when the animation reverses.
	p.frames++;
	p.delay = Math.round(1000 / p.fps);
	if (p.modifier & FF_PCTTOV)
		p.modifier = p.modifier | FF_PCTDEC;
	if (p.type == 'deccolor')
		p.modifier = FF_CSSRGB;
	else if (p.type == 'hexcolor')
		p.modifier = FF_CSSHEX;
	return true;
}

//*****************************************************************************/
//* Adds elements to the public element array object in the animate instance. */
//* It sets the status of each element to the starting position with the      */
//* current frame set to the first frame.                                     */
//*****************************************************************************/
AnsAnimate.addElements = function (p)
{
	var t = p.t, amount = p.amount, idbegin = p.idbegin, idbase = p.idbase,
		iddigits = p.iddigits, idend = p.idend;

	if (p.attr == 'id')
	{
		if (amount == 1)
		{
			t.element.push({ 'id': idbegin, 'hand': document.getElementById(idbegin), 'status': 0, 'current': 0 });
		}
		else
		{
			for (var c = 0; c < amount; c++)
			{
				var id = idbegin;
				for (var d = Math.pow(idbase, iddigits - 1); d > 1; d /= idbase)
					if (c < d)
						id += '0';
				id += c.toString(idbase) + idend;
				t.element.push({ 'id': id, 'hand': document.getElementById(id), 'status': 0, 'current': 0 });
			}
		}
	}
	else
	{
		var reclass = new RegExp('(?:^\s*|^.+\s+)' + p.format + '(?:\s+|$)');
		var allElements = document.getElementsByTagName('*');
		var elementsCount = allElements.length;
		for (var i = 0; i < elementsCount; i++)
		{
			if (reclass.test(allElements[i].className))
				t.element.push({ 'id': allElements[i].id, 'hand': allElements[i], 'status': 0, 'current': 0 });
		}
		p.amount = t.element.length;
	}
	return true;
}

AnsAnimate.setLinear = function (p)
{
	var t = p.t,
		frames = p.frames;
	for (var c = 0; c < frames; c++)
		t.frame[c] = 100 / (frames - 1) * c;
	return true;
}

AnsAnimate.setHill = function (p)
{
	var t = p.t;
	var pframes = p.frames;
	var mid = Math.ceil(pframes / 2);
	var avg = pframes % 2 == 0 ? 100 / pframes : 100 / (pframes - 1);
	var prev = 0.0;

	for (var c = 0; c < pframes; c++)
	{
		if (pframes % 2 == 0)
			t.frame[c] = prev += avg * Math.cos((mid - c) * Math.PI / mid) + avg;
		else
			t.frame[c] = prev += c < mid ? avg * Math.cos((mid - c) * Math.PI / mid) + avg : avg * Math.cos((c - mid + 1) * Math.PI / mid) + avg;
	}
	return true;
}

AnsAnimate.setLadder = function (p)
{
	var t = p.t;
	var pframes = p.frames;
	var mid = Math.ceil(pframes / 2);
	var avg = pframes % 2 == 0 ? 100 / pframes : 100 / (pframes - 1);
	var prev = 0.0;

	for (var c = 0; c < pframes; c++)
	{
		if (pframes % 2 == 0)
		{
			if (c < mid)
				t.frame[c] = prev += avg * (c * 2) / mid;
			else if (c == mid)
				t.frame[c] = prev += 100 / pframes * 2;
			else
				t.frame[c] = prev += avg * ((pframes - c) * 2) / mid;
		}
		else
			t.frame[c] = prev += c < mid ? avg * (c * 2) / mid : avg * ((pframes - c) * 2) / mid;
	}
	return true;
}

AnsAnimate.setColor = function (p)
{
	var t      = p.t;
	var frames = p.frames;
	var start  = p.start;
	var stop   = p.stop;

	var startR, startG, startB, stopR, stopG, stopB;
	var stepR, stepG, stepB, holdR, holdG, holdB;
	var stepSize;

	startR = (start & 0xff0000) >> 16;
	startG = (start & 0x00ff00) >> 8;
	startB = start & 0x0000ff;
	stopR = (stop & 0xff0000) >> 16;
	stopG = (stop & 0x00ff00) >> 8;
	stopB = stop & 0x0000ff;

	stepR = (stopR - startR) / (frames - 1);
	stepG = (stopG - startG) / (frames - 1);
	stepB = (stopB - startB) / (frames - 1);

	for (var i = 0; i < frames; i++)
	{
		holdR = Math.round(startR + stepR * i);
		holdG = Math.round(startG + stepG * i);
		holdB = Math.round(startB + stepB * i);
		holdR = holdR < 16 ? '0' + holdR.toString(16) : holdR.toString(16);
		holdG = holdG < 16 ? '0' + holdG.toString(16) : holdG.toString(16);
		holdB = holdB < 16 ? '0' + holdB.toString(16) : holdB.toString(16);
		t.frame[i] = holdR + holdG + holdB;
	}
	return true;
}

AnsAnimate.applyModifiers = function (p)
{
	var t = p.t;
	var tframelength = t.frame.length;
	for (var c = 0; c < tframelength; c++)
	{
		if (p.modifier & FF_PCTDEC)
			t.frame[c] *= 0.01;
		if (p.modifier & FF_PCTTOV)
			t.frame[c] = p.start + t.frame[c] * p.range;
		if (p.modifier & FF_POINTS)
			t.frame[c] = t.frame[c].toFixed(p.points);
		if (p.modifier & FF_ROUND)
			t.frame[c] = Math.round(+t.frame[c]);
		if (p.modifier & FF_STRING || (p.base > 10 && typeof t.frame[c] != 'string'))
			t.frame[c] = (+t.frame[c]).toString(p.base);
		if (p.modifier & FF_CSSZRO && +t.frame[c] == 0)
			t.frame[c] = '0';
		else
		{
			if (p.modifier & FF_CSSRGB)
				t.frame[c] = 'rgb(' +
					parseInt(t.frame[c].substr(0, 2), 16) + ', ' +
					parseInt(t.frame[c].substr(2, 2), 16) + ', ' +
					parseInt(t.frame[c].substr(4, 2), 16) + ')';
			else if (p.modifier & FF_CSSHEX)
				t.frame[c] = '#' + t.frame[c];
			if (p.modifier & FF_CSSPCT)
				t.frame[c] = typeof t.frame[c] != 'string' ? t.frame[c].toString(p.base) + '%' : t.frame[c] + '%';
			if (p.modifier & FF_CSSPX)
				t.frame[c] = typeof t.frame[c] != 'string' ? t.frame[c].toString(p.base) + 'px' : t.frame[c] + 'px';
		}
	}
	return true;
}

