// Online number base converter tool
// Allows bases up to 36, negatives, radix points, and unique user interaction
//
// Built exlusively for www.digitconvert.com
// Other use prohibited unless approved by the creators
// Copyright Dane Iracleous and Matt Curry
// Created on January 12, 2009
// Last updated on January 5, 2011
//
// Version 2.0
// 
//
//
// January 5, 2011: Dane Iracleous
//		-Recreated the tool as a set of Mootools classes
//		-Transition effects for adding/removing boxes
//		-Modular design for easy integration with web site




//the class for the entire converter tool containing a list of converter boxes and a button to add more boxes
var BaseConverter = new Class(
{
	Implements: [Options],
	
	options:
	{
		controls: true, //whether or not the user can add/remove boxes
		english: true, //whether or not to show english names of bases
		transition: true, //whether or not to provide aesthetic transition effects for adding/removing boxes
		transitionTime: 500, //the time in milliseconds that a transition takes
		transitionType: Fx.Transitions.Quad.easeInOut, //the type of transition
		defaultBase: 10, //the default base for newly added boxes
		precision: 100 //number of places after radix point to round off
	},
	
	initialize: function(container, options)
	{
		var self = this;
		
		//set the options
		self.setOptions(options);
		//create the main container
		self.container = new Element("div",
		{
			id: "BaseConverter"
		});
		//create the box container
		self.boxContainer = new Element("div",
		{
			id: "boxContainer"	
		});
		//put the box container into the main container
		self.boxContainer.inject(self.container);
		//create the array of boxes
		self.box = Array();
		if(self.options.controls==true)
		{
			//create the link to add a new box
			self.addLink = new Element("a",
			{
				text: "+ Add a Box",
				"class": "addLink",
				title: "click to add another base converter box",
				events:
				{
					click: function()
					{
						self.addBox(self.options.defaultBase);	
					}
				},
				styles:
				{
					cursor: "pointer"	
				}
			});
			//put the new box link into the main container
			self.addLink.inject(self.container, "bottom");
			self.container.inject(container);
		}
	},
	
	//global convert function to convert all visible boxes to the right value according to their selected basees
	convert: function(obj, add)
	{
		var self = this;
		
		if(add)
		{
			if(self.box[0]!=undefined)
			{
				var base = self.box[0].menu.getProperty("value");
				var value = self.box[0].input.getProperty("value");
				obj.convert(base, value);
			}
		}
		else
		{
			var base = obj.menu.getProperty("value");
			var value = obj.input.getProperty("value");
			for(var i in self.box)
			{
				if(self.box[i] != obj && self.box[i].container!=undefined)
					self.box[i].convert(base, value);	
			}
		}
	},
	
	//adds a new converter box of a specified base to the tool, appended at the bottom
	addBox: function(base)
	{
		var self = this;
		var z = self.box.push(new ConverterBox(self, base));
		z--;
		//get the HTML of this box
		var e = self.box[z].get();
			
		if(self.options.transition==true)
		{
			//set the styles to prepare for morph transition
			e.setStyle("opacity", "0")
			e.setStyle("height", "0px");
			e.setStyle("margin-bottom", "0px");
			e.inject(self.boxContainer);
			//set the morph transition
			var effect = new Fx.Morph(e,
			{
				duration: self.options.transitionTime,
				transition: self.options.transitionType
			});
			//commence the morph transition
 			effect.start({
				'opacity': [0, 1],
				'height': [0, 30],
				'padding-top': [0, 10],
				'padding-bottom': [0, 10]
			});
		}
		else
		{
			e.inject(self.boxContainer);
		}
		//return the number of this box relative to its position on the page
		return z;
	},
	
	//removes a box of a certain array index - can be called externally
	removeBox: function(z)
	{
		var self = this;
		self.box[z].remove();
	},
	
	//removes all boxes
	removeAll: function()
	{
		var self = this;
		for(z in self.box)
		{
			if(self.box[z].remove != undefined)
				self.box[z].remove();
		}
	},
	
	//destroy this converter
	destroy: function()
	{
		var self = this;
		$clear(self.box);
		self.container.empty();
		self.container.destroy();
		$clear(self);
	},
	
	get: function()
	{
		var self = this;
		return self.container;
	}
});

//the class for a single converter box containing a drop-down base selector, an input box for a number, and a button to remove the box itself from the tool
var ConverterBox = new Class(
{
	initialize: function(converter, def)
	{
		var self = this;
		var defaultBase = def;
		self.converter = converter;
		
		//the list of all bases and their various infos: english name + RegEx allowable characters
		self.base = Array();
		self.base[2] = Array("Binary", "0-1");
		self.base[3] = Array("Ternary", "0-2");
		self.base[4] = Array("Quaternary", "0-3");
		self.base[5] = Array("Quinary", "0-4");
		self.base[6] = Array("Senary", "0-5");
		self.base[7] = Array("Septenary", "0-6");
		self.base[8] = Array("Octal", "0-7");
		self.base[9] = Array("Nonary", "0-8");
		self.base[10] = Array("Decimal", "0-9");
		self.base[11] = Array("Undecimal", "0-9Aa");
		self.base[12] = Array("Duodecimal", "0-AaBb");
		self.base[13] = Array("Tridecimal", "0-9AaBbCc");
		self.base[14] = Array("Tetradecimal", "0-9AaBbCcDd");
		self.base[15] = Array("Pentadecimal", "0-9AaBbCcDdEe");
		self.base[16] = Array("Hexadecimal", "0-9AaBbCcDdEeFf");
		self.base[17] = Array("Septendecimal", "0-9AaBbCcDdEeFfGg");
		self.base[18] = Array("Octodecimal", "0-9AaBbCcDdEeFfGgHh");
		self.base[19] = Array("Nonadecimal", "0-9AaBbCcDdEeFfGgHhIi");
		self.base[20] = Array("Vigesimal", "0-9AaBbCcDdEeFfGgHhIiJj");
		self.base[21] = Array("Unovigesimal", "0-9AaBbCcDdEeFfGgHhIiJjKk");
		self.base[22] = Array("Duovigesimal", "0-9AaBbCcDdEeFfGgHhIiJjKkLl");
		self.base[23] = Array("Triovigesimal", "0-9AaBbCcDdEeFfGgHhIiJjKkLlMm");
		self.base[24] = Array("Quadrovigesimal", "0-9AaBbCcDdEeFfGgHhIiJjKkLlMmNn");
		self.base[25] = Array("Pentavigesimal", "0-9AaBbCcDdEeFfGgHhIiJjKkLlMmNnOo");
		self.base[26] = Array("Hexavigesimal", "0-9AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPp");
		self.base[27] = Array("Heptovigesimal", "0-9AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQq");
		self.base[28] = Array("Octovigesimal", "0-9AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRr");
		self.base[29] = Array("Novovigesimal", "0-9AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSs");
		self.base[30] = Array("Trigesimal", "0-9AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTt");
		self.base[31] = Array("Unotrigesimal", "0-9AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUu");
		self.base[32] = Array("Duotrigesimal", "0-9AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVv");
		self.base[33] = Array("Triotrigesimal", "0-9AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWw");
		self.base[34] = Array("Quadrotrigesimal", "0-9AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXx");
		self.base[35] = Array("Pentatrigesimal", "0-9AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYy");
		self.base[36] = Array("Hexatrigesimal", "0-9AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz");
		
		self.container = new Element("div",
		{
			"class": "box"
		});
		self.menu = new Element("select",
		{
			"class": "menu",
			id: "mav_"+(converter.box.length),
			events:
			{
				change: function()
				{
					self.convert(-1, self.input.getProperty("value"));
				}
			}
		});
		//base selector generation
		for(var i=2; i<=36; i++)
		{
			var label = "Base "+i;
			if(self.converter.options.english==true)
				label += " ("+self.base[i][0]+")";
			var option = new Element("option",
			{
				value: i,
				text: label
			});
			option.inject(self.menu);
			if(i==defaultBase)
				option.setProperty("selected", "selected");
		}
		
		//input box generation
		self.input = new Element("input",
		{
			"class": "entry",
			type: "text",
			events:
			{
				keyup: function()
				{
					self.fullConvert();
				},
				input: function()
				{
					self.fullConvert();
				},
				blur: function()
				{
					self.fullConvert();
				}
			}
		});
		
		self.menu.inject(self.container);
		self.input.inject(self.container);
		
		if(self.converter.options.controls==true)
		{
			self.removeLink = new Element("a",
			{
				"class": "removeLink",
				title: "click to remove this base converter box",
				events:
				{
					click: function()
					{
						self.remove();	
					},
					mousedown: function()
					{
						this.setStyle("opacity", "0.25");
						this.setStyle("cursor", "default");	
					}
				}
			});
			self.removeLink.inject(self.container);
			var removeImg = new Element("img",
			{
				src: "remove1.png",
				styles:
				{
					position: "relative",
					top: "5px"
				},
				events:
				{
					mouseenter: function()
					{
						this.set("src", "remove.png");	
					},
					mouseleave: function()
					{
						this.set("src", "remove1.png");	
					}
				}
			});
			removeImg.inject(self.removeLink);
		}
		
		self.converter.convert(self, true);
	},
	
	//portal to the global convert function to convert all boxes
	fullConvert: function()
	{
		var self = this;
		self.filterValue();
		self.converter.convert(self);
	},
	
	//portal to the global convert function to convert all boxes
	convert: function(base, value)
	{
		var self = this;
		
		if(self.currentBase==undefined)
			self.currentBase = self.menu.getProperty("value");
		
		if(base==-1)
		{
			var decValue = self.baseToDecimal(self.input.getProperty("value"), self.currentBase);
			var thisValue = self.decimalToBase(decValue.toString(), self.menu.getProperty("value"));
		}
		else
		{
			var decValue = self.baseToDecimal(value, base);
			var thisValue = self.decimalToBase(decValue.toString(), self.currentBase);
		}
		self.input.set("value", thisValue);
		self.currentBase = self.menu.getProperty("value");
	},
	
	//converts a decimal number to a specified base
	decimalToBase: function(decimal, base)
	{
		var self = this;
		if(decimal == "0")
			return "0";
		if(decimal == "" || base == "1")
			return "";
		var arr = [];
		var x = 0;
		var z = 0;
		var ret = true;
		while(x < decimal.length)
		{
			if(decimal.charAt(x) != "0")
				ret = false;
			arr[x] = decimal.charAt(x);
			x++;
		}
		if(ret == true)
			return "0";
		x = 0;
		z = 0;
		while(arr[z] != "." && z < decimal.length)
		{
			x++;
			z++;
		}
		var leftDigits = new Number(x);
		var rightDigits = new Number(decimal.length - x - 1);
		z = 0;
		var hasLeft = false;
		var hasRight = false;
		if(leftDigits > 0)
			hasLeft = true;
		if(rightDigits > 0)
			hasRight = true;

		var leftValue = new String;
		var rightValue = new String;
		x = 0;
		while(x < leftDigits)
		{
			leftValue += new String(arr[x]);
			x++;
		}
		x = 0;
		while (x < rightDigits)
		{
			rightValue += new String(arr[x + leftDigits + 1]);
			x++;
		}
		x = 0;
		var negative = false;
		if(leftValue.charAt(0) == "-")
		{
			leftValue = leftValue.substr(1, leftValue.length);
			negative = true;
		}
		x = 0;
		var leftInt = new Number(leftValue);
		var rightInt = new Number("0." + rightValue);
		var leftBin = new String;
		var rightBin = new String;
		var value = 0;
		while(value < leftInt)
		{
			z = 1;
			while(value < leftInt && (z < base))
			{
				value = z * Math.pow(base, x);
				z++;
			}
			x++;
		}
		x--;
		z--;
		put = false;
		while(leftInt > 0 && x >= 0)
		{
			z = base - 1;
			while(z >= 0)
			{
				if(leftInt - z * Math.pow(base, x) >= 0)
				{
					leftBin += self.numericToAlpha(z);
					leftInt -= z * Math.pow(base, x);
					put = true;
					break;
				}
				z--;
			}
			if(put == false)
				leftBin += "0";
			x--;
			z = x - 1;
			put = false;
		}
		while(x >= 0)
		{
			leftBin += "0";
			x--;
		}
		x = -1;
		while(rightInt > 0 && x >= -100)
		{
			z = base - 1;
			while(z >= 0)
			{
				if(rightInt - z * Math.pow(base, x) >= 0)
				{
					rightBin += self.numericToAlpha(z);
					rightInt -= z * Math.pow(base, x);
					put = true;
					break;
				}
				z--;
			}
			if(put == false)
				rightBin += "0";
			x--;
			z = x - 1;
			put = false;
		}
		var leftBinString = new String(leftBin);
		x = 0;
		while(leftBinString.charAt(x) == "0")
			x++;
		leftBinString = leftBinString.substring(x);
		if(leftBinString == "0" || leftBinString == "")
			hasLeft = false;
		var leftBinFinal = leftBinString;
		var rightBinFinal = rightBin;
		var finalString;
		if(hasRight == true && hasLeft == true)
			finalString = leftBinFinal + "." + rightBinFinal;
		else if(hasLeft == true && hasRight == false)
			finalString = leftBinFinal;
		else if(hasLeft == false && hasRight == true)
			finalString = "0." + rightBinFinal;
		else
			finalString = "0";
		already = false;
		if(negative == true)
			return "-" + finalString;
		else 
			return finalString;
	},
	
	//converts a number of a specified base to decimal
	baseToDecimal: function(basevalue, base)
	{
		var self = this;

		if(base == "1")
			return "";

		var binary = new String(basevalue);
		var temporary = "";
		var negative = false;
		if(binary.charAt(0) == "-")
		{
			binary = binary.substr(1, binary.length);
			negative = true;
		}
		if(binary == "0")
			return "0";
		if(binary == "")
			return "";
		var arr = [];
		var binaryConverted = new Number(0);
		var x = new Number(0);
		while(x < binary.length)
		{
			arr[x] = self.alphaToNumeric(binary.charAt(x));
			x++;
		}
		x = 0;
		while(arr[x] != "." && x < binary.length)
			x++;
		var leftDigits = new Number(x);
		var rightDigits = new Number(binary.length - x - 1);
		var left = [];
		var right = [];
		var leftValue = [];
		var rightValue = [];
		x = 0;
		while(x < leftDigits)
		{
			left[x] = arr[x];
			x++;
		}
		x = 0;
		while (x < rightDigits)
		{
			right[x] = arr[x + leftDigits + 1];
			x++;
		}
		x = 0;
		var leftPower = new Number(leftDigits - 1);
		var rightPower = new Number(rightDigits);
		while(x <= leftPower)
		{
			leftValue[x] = left[x] * Math.pow(base, leftPower - x);
			x++;
		}
		x = 0;
		while(x < rightPower)
		{
			rightValue[x] = right[x] * Math.pow(base, -1 * (x + 1));
			x++;
		}
		x = 0;
		while(x <= leftPower)
		{
			binaryConverted += leftValue[x];
			x++;
		}
		x = 0;
		var binaryConvertedRight = new Number(0);
		while(x < rightPower)
		{
			binaryConvertedRight += rightValue[x];
			x++;
		}
		x = 0;
		var binConv = binaryConverted.toString();
		var binConvRight = binaryConvertedRight.toString();
		arr = arr.slice(0, 0);
		while(x < binConv.length)
		{
			arr[x] = binConv.charAt(x);
			x++;
		}
		var y = 0;
		var z = 0;
		var light = new String("");
		var noDec = false;
		while(z < binConv.length)
		{
			if(arr[z] == "e")
			{
				noDec = true;
				var t = binConv.length - 1;
				var u = 0;
				while(arr[t] != "+")
				{
					light += arr[t];
					u++;
					t--;
				}
				var real = new Number(light.split("").reverse().join(""));
				while(y < (real - (binConv.length - 3 - u)) + 2)
				{
					arr[z + y] = "0";
					y++;
				}
				break;
			}
			z++;
		}
		z = 0;
		var final = new String;
		if(noDec == true)
		{
			while(z < arr.length - 1)
			{
				if(arr[z] != ".")
					final += arr[z];
				z++;
			}
			binaryConverted = final;
		}
		z = 0;
		var normal = false;
		while(z < binaryConverted.length)
		{
			if(binaryConverted.charAt(z) != "0")
				normal = true;
			z++;
		}
		var finalString;
		if(leftDigits > 0 && rightDigits > 0)
			finalString = binaryConverted + binConvRight.substr(1);
		else if(rightDigits > 0 && !normal)
			finalString = binConvRight;
		else if(leftDigits > 0)
			finalString = binaryConverted;
		else
			finalString = "0";

		if(negative == true)
			return "-" + finalString;
		else
			return finalString;
	},
	
	//formats a number to a corresponding letter
	numericToAlpha: function(z)
	{
		if(z == "10")
			return "A";
		else if(z == "11")
			return "B";
		else if(z == "12")
			return "C";
		else if(z == "13")
			return "D";
		else if(z == "14")
			return "E";
		else if(z == "15")
			return "F";
		else if(z == "16")
			return "G";
		else if(z == "17")
			return "H";
		else if(z == "18")
			return "I";
		else if(z == "19")
			return "J";
		else if(z == "20")
			return "K";
		else if(z == "21")
			return "L";
		else if(z == "22")
			return "M";
		else if(z == "23")
			return "N";
		else if(z == "24")
			return "O";
		else if(z == "25")
			return "P";
		else if(z == "26")
			return "Q";
		else if(z == "27")
			return "R";
		else if(z == "28")
			return "S";
		else if(z == "29")
			return "T";
		else if(z == "30")
			return "U";
		else if(z == "31")
			return "V";
		else if(z == "32")
			return "W";
		else if(z == "33")
			return "X";
		else if(z == "34")
			return "Y";
		else if(z == "35")
			return "Z";
		else
			return z;
	},
	
	//formats a letter to a corresponding number
	alphaToNumeric: function(z)
	{
		if(z == "A")
			return "10";
		else if(z == "B")
			return "11";
		else if(z == "C")
			return "12";
		else if(z == "D")
			return "13";
		else if(z == "E")
			return "14";
		else if(z == "F")
			return "15";
		else if(z == "G")
			return "16";
		else if(z == "H")
			return "17";
		else if(z == "I")
			return "18";
		else if(z == "J")
			return "19";
		else if(z == "K")
			return "20";
		else if(z == "L")
			return "21";
		else if(z == "M")
			return "22";
		else if(z == "N")
			return "23";
		else if(z == "O")
			return "24";
		else if(z == "P")
			return "25";
		else if(z == "Q")
			return "26";
		else if(z == "R")
			return "27";
		else if(z == "S")
			return "28";
		else if(z == "T")
			return "29";
		else if(z == "U")
			return "30";
		else if(z == "V")
			return "31";
		else if(z == "W")
			return "32";
		else if(z == "X")
			return "33";
		else if(z == "Y")
			return "34";
		else if(z == "Z")
			return "35";
		else
			return z;
    },
	
	//function to make sure the inputted value is correctly formatted before processing
	filterValue: function()
	{
		var self = this;
		
		var temp = self.input.getProperty("value").toUpperCase();
		self.input.setProperty("value", temp);
		
		var decimalPlaces = self.converter.options.precision;
		var allowNegative = true;
		var base = parseInt(self.menu.getProperty("value"));
		var which = self.base[base][1];
		
		var reg0Str = "[" + which + "]*";
		reg0Str += "\\.?[" + which + "]{0," + decimalPlaces + "}";
		reg0Str = allowNegative ? "^-?" + reg0Str : "^" + reg0Str;
		reg0Str = reg0Str + "$";
		var reg0 = new RegExp(reg0Str);
		if(reg0.test(temp))
			return true;
		var reg1Str = "[^" + which + (decimalPlaces != 0 ? "." : "") + (allowNegative ? "-" : "") + "]";
		var reg1 = new RegExp(reg1Str, "g");
		temp = temp.replace(reg1, "");
		var hasNegative = temp.length > 0 && temp.charAt(0) == "-";
		var reg2 = /-/g;
		temp = temp.replace(reg2, "");
		if(hasNegative)
			temp = "-" + temp;
		if(decimalPlaces != 0)
		{
			var reg3 = /\./g;
			var reg3Array = reg3.exec(temp);
			if(reg3Array != null)
			{
				var reg3Right = temp.substring(reg3Array.index + reg3Array[0].length);
				reg3Right = reg3Right.replace(reg3, "");
				reg3Right = decimalPlaces > 0 ? reg3Right.substring(0, decimalPlaces) : reg3Right;
				temp = temp.substring(0, reg3Array.index) + "." + reg3Right;
			}
		}
		var value = temp.toUpperCase();
		
		self.input.setProperty("value", value);
	},
	
	//removes a converter box permanently - can be called externally
	remove: function()
	{
		var self = this;
		if(self.converter.options.transition==true)
		{
			//set the morph transition
			var effect = new Fx.Morph(self.container,
			{
				duration: self.converter.options.transitionTime,
				transition: self.converter.options.transitionType,
				onComplete: function()
				{
					self.container.destroy();
					//self.converter.box.splice(z, 1);
				}
			});
			//commence the morph transition
 			effect.start(
			{
				'opacity': [1, 0],
				'height': [30, 0],
				'padding-top': [10, 0],
				'padding-bottom': [10, 0]
			});
		}
		else
		{
			self.container.destroy();
			//self.converter.box.splice(z, 1);
		}
	},
	
	//gets the container HTML of this box
	get: function()
	{
		var self = this;
		return self.container;
	}
});
