/**
 * Colour Converter Dashboard widget
 *
 * Copyright (c) 2006
 * FilmLight Ltd,
 * 14-15 Manette Street,
 * London W1D 4AP
 * All rights reserved.
 */


/* White point for L*a*b* default 16 ft-lamberts D65 */
var whiteXYZ = new Array(15.2028, 16.0, 17.3962);
var lastXYZ = new Array(0.0, 0.0, 0.0);
var lastUnits = "ft-l";

var precision = -6;


var lastUpdatedFunction = function() {};
var autoUpdate = true;

/**
 * Initialisation funtion
 */
function setup(update) {
	setXYZ([15.2028, 16.0, 17.3962], "units");     // fill in default values
	autoUpdate = update;
}

function updateFields() {
	lastUpdatedFunction.call();
	return false;
}

function changeUnits(elem) {
	var units = elem.options[elem.selectedIndex].value;
	var scale = 1.0;

	if (units == lastUnits)
		return;

	if (lastUnits == "ft-l" && units == "cd/m2")
		scale = 3.4262;
	if (lastUnits == "cd/m2" && units == "ft-l")
		scale = 1.0 / 3.4262;
	var xyz = new Array();
	for (var n = 0; n < 3; ++n) {
		whiteXYZ[n] *= scale;
		xyz.push(lastXYZ[n] * scale);
	}

	setXYZ(xyz, "units");
	lastUnits = units;
}


/**
 * Convert input to XYZ and update other form fields
 *
 * @param value the value that will be converted
 * @param input the units of the value that will be converted
 */
function convertValue(value, input) {
	if (value == "")   // empty, don't change anything
		return;

	var L, xyz, x, y, z;

    // ui elements
	var white_status = document.getElementById("whiteXYZStatus");
	var xyz_status = document.getElementById("XYZStatus");
	var luminance_status = document.getElementById("LuminanceStatus");
	var xy_status = document.getElementById("xyStatus");
	var uv_status = document.getElementById("uvStatus");
	var degreesK_status = document.getElementById("degreesKStatus");
	var daylightD_status = document.getElementById("daylightDStatus");
	var lab_status = document.getElementById("LabStatus");

	var updateFunction;

	switch (input) {
		case "XYZ":
			updateFunction = function() {
				setXYZ([getValue("XYZ_XValue"), getValue("XYZ_YValue"), getValue("XYZ_ZValue")], "XYZ");
			}

			if(autoUpdate) {
				updateFunction.call();
			} else {
				lastUpdatedFunction = updateFunction;
			}

			break;
		case "Luminance":
			L = getValue("LuminanceValue");
			xyz = new Array();
			if (lastXYZ.length == 3 && lastXYZ[1] > 0.0) {
				xyz.push(lastXYZ[0] * L / lastXYZ[1]);
				xyz.push(L);
				xyz.push(lastXYZ[2] * L / lastXYZ[1]);
			} else if (whiteXYZ.length == 3 && whiteXYZ[1] > 0.0) {
				xyz.push(whiteXYZ[0] * L / whiteXYZ[1]);
				xyz.push(L);
				xyz.push(whiteXYZ[2] * L / whiteXYZ[1]);
			} else {
				xyz.push(L);
				xyz.push(L);
				xyz.push(L);
			}

			updateFunction = function() {
				setXYZ(xyz, input);
			}

			if(autoUpdate) {
				updateFunction.call();
			} else {
				lastUpdatedFunction = updateFunction;
			}

			break;
		case "xy":
			x = getValue("xy_xValue");
			y = getValue("xy_yValue");
			L = lastXYZ[1];

			updateFunction = function() {
				setXYZ([L * x / y, L, L * (1.0 - x - y) / y], input);
			}

			if(autoUpdate) {
				updateFunction.call();
			} else {
				lastUpdatedFunction = updateFunction;
			}

			break;
		case "uv":
			updateFunction = function() {
				setuv([getValue("uv_uValue"), getValue("uv_vValue")], input);
			}

			if(autoUpdate) {
				updateFunction.call();
			} else {
				lastUpdatedFunction = updateFunction;
			}

			break;
		case "degreesK":
			var k = getValue("degreesKValue");
			if (k < 500.0) {
				degreesK_status.innerHTML = "< 500";
				break;
			} else {
				degreesK_status.innerHTML = "";
			}
			degreesK_status.innerHTML = "";
			var uv = convertDegKtoUV(k);

			updateFunction = function() {
				setuv(uv, input);
			}

			if(autoUpdate) {
				updateFunction.call();
			} else {
				lastUpdatedFunction = updateFunction;
			}

			break;
		case "daylightD":
			var d = getValue("daylightDValue");
			if (d < 35.0) {
				daylightD_status.innerHTML = "< 35.0";
				break;
			}
			if (d > 250.0) {
				daylightD_status.innerHTML = "> 250.0";
				break;
			}
			daylightD_status.innerHTML = "";

			uv = convertDtoUV(d);

			updateFunction = function() {
				setuv(uv, input);
			}

			if(autoUpdate) {
				updateFunction.call();
			} else {
				lastUpdatedFunction = updateFunction;
			}

			break;
		case "whiteXYX":
			whiteXYZ = [getValue("whiteXYZ_XValue"), getValue("whiteXYZ_YValue"), getValue("whiteXYZ_ZValue")];

			updateFunction = function() {
				setXYZ(lastXYZ, input);
			}

			if(autoUpdate) {
				updateFunction.call();
			} else {
				lastUpdatedFunction = updateFunction;
			}

			break;
		case "L*a*b*":
			var Lab = [getValue("Lab_LValue"), getValue("Lab_aValue"), getValue("Lab_bValue")];
			var a = 16.0 / 116.0; // 0.1379
			var b = 24.0 / 116.0; // 0.2069
			var c = b * b * b;	  // 0.008564
			var d = (b - a) / c;	// 7.787
			var f = new Array(3);
			xyz = new Array(3);
			f[1] = (Lab[0] + 16.0) / 116.0;
			f[0] = f[1] + Lab[1] / 500.0;
			f[2] = f[1] - Lab[2] / 200.0;
			for (var n = 0; n < 3; ++n) {
				var v;

				if (f[n] > b) {
					v = f[n] * f[n] * f[n];
				} else {
					v = (f[n] - a) / d;
				}
				xyz[n] = v * whiteXYZ[n];
			}

			updateFunction = function() {
				setXYZ(xyz, input);
			}

			if(autoUpdate) {
				updateFunction.call();
			} else {
				lastUpdatedFunction = updateFunction;
			}

			break;
	}
}

function getValue(control) {
	return eval(document.getElementById(control).value);
}


/**
 * Convert uv and previous luminance value to XYZ and update all values
 */
function setuv(uv, input) {
	var L = lastXYZ[1];
	setXYZ([L * 9.0 * uv[0] / (4.0 * uv[1]),
			L,
			L * (3.0 * (4.0 - uv[0]) / (4.0 * uv[1]) - 5.0)], input);
}


/**
 * Update all values from the current XYZ values, excluding 'input'
 *
 * @param XYZ the XYZ values that will be used to update the input fields
 * @param input the type of input that triggered the conversion
 */
function setXYZ(XYZ, input) {
	var s;

    // ui elements
	var white_Xvalue = document.getElementById("whiteXYZ_XValue");
	var white_Yvalue = document.getElementById("whiteXYZ_YValue");
	var white_Zvalue = document.getElementById("whiteXYZ_ZValue");
	var white_status = document.getElementById("whiteXYZStatus");
	var x_value = document.getElementById("XYZ_XValue");
	var y_value = document.getElementById("XYZ_YValue");
	var z_value = document.getElementById("XYZ_ZValue");
	var xyz_status = document.getElementById("XYZStatus");
	var luminance_value = document.getElementById("LuminanceValue");
	var luminance_status = document.getElementById("LuminanceStatus");
	var xy_xvalue = document.getElementById("xy_xValue");
	var xy_yvalue = document.getElementById("xy_yValue");
	var xy_status = document.getElementById("xyStatus");
	var uv_uvalue = document.getElementById("uv_uValue");
	var uv_vvalue = document.getElementById("uv_vValue");
	var uv_status = document.getElementById("uvStatus");
	var degreesK_value = document.getElementById("degreesKValue");
	var degreesK_status = document.getElementById("degreesKStatus");
	var daylightD_value = document.getElementById("daylightDValue");
	var daylightD_status = document.getElementById("daylightDStatus");
	var lab_lvalue = document.getElementById("Lab_LValue");
	var lab_avalue = document.getElementById("Lab_aValue");
	var lab_bvalue = document.getElementById("Lab_bValue");
	var lab_status = document.getElementById("LabStatus");

	if (whiteXYZ.length == 3) {
		white_status.innerHTML = "";
		white_Xvalue.value = string_from_number(whiteXYZ[0], 4);
		white_Yvalue.value = string_from_number(whiteXYZ[1], 4);
		white_Zvalue.value = string_from_number(whiteXYZ[2], 4);
		if (input != "units") {
		}
	} else {
		white_status.innerHTML = "(error)";
	}

	if (!lastXYZ) {
		lastXYZ = XYZ;
	}

	if (XYZ[0] > lastXYZ[0] * 0.99999 &&
		XYZ[1] > lastXYZ[1] * 0.99999 &&
		XYZ[2] > lastXYZ[2] * 0.99999 &&
		XYZ[0] < lastXYZ[0] * 1.00001 &&
		XYZ[1] < lastXYZ[1] * 1.00001 &&
		XYZ[2] < lastXYZ[2] * 1.00001)
		return;

	if (input != "XYZ") {
		x_value.value = string_from_number(XYZ[0], 4);
		y_value.value = string_from_number(XYZ[1], 4);
		z_value.value = string_from_number(XYZ[2], 4);
		xyz_status.innerHTML = "";
	}

	if (input != "Luminance") {
		luminance_value.value = string_from_number(XYZ[1], 4);
		luminance_status.innerHTML = "";
	}

	if (input != "xy") {
		s = XYZ[0] + XYZ[1] + XYZ[2];
		if (s == 0) s = 1.0;
		xy_xvalue.value = string_from_number(XYZ[0] / s, 4);
		xy_yvalue.value = string_from_number(XYZ[1] / s, 4);
		xy_status.innerHTML = "";
	}

	if (input != "uv") {
		s = XYZ[0] + 15.0 * XYZ[1] + 3.0 * XYZ[2];
		if (s == 0) {
			s = 1.0;
		}
		uv_uvalue.value = string_from_number(4.0 * XYZ[0] / s, 4);
		uv_vvalue.value = string_from_number(9.0 * XYZ[1] / s, 4);
		uv_status.innerHTML = "";
	}

	if (input != "degreesK") {
		degreesK_value.value = convertXYZtoDegK(XYZ);
		degreesK_status.innerHTML = "";
	}

	if (input != "daylightD") {
		daylightD_value.value = convertXYZtoD(XYZ);
		daylightD_status.innerHTML = "";
	}

	if (input != "L*a*b*") {
		var f = new Array();
		var a, b, c, d, n, v;
		a = 16.0 / 116.0; // 0.1379
		b = 24.0 / 116.0; // 0.2069
		c = Math.pow(b, 3);      // 0.008564
		d = (b - a) / c;    // 7.787

		for (n = 0; n < 3; ++n) {
			v = XYZ[n] / whiteXYZ[n];
			if (v > c) {
				v = Math.pow(v, 1.0 / 3.0);
			} else {
				v = d * v + a;
			}
			f.push(v);
		}

		lab_lvalue.value = string_from_number(116.0 * f[1] - 16.0, 2);
		lab_avalue.value = string_from_number(500.0 * (f[0] - f[1]), 2);
		lab_bvalue.value = string_from_number(200.0 * (f[1] - f[2]), 2);
		lab_status.innerHTML = "";
	}

	lastXYZ = XYZ;

	var colour_value = document.getElementById("Colour_Display");
	var rgb = convertXYZtoRGB([XYZ[0], XYZ[1], XYZ[2], whiteXYZ[0], whiteXYZ[1], whiteXYZ[2]]);
	colour_value.style.backgroundColor = "rgb(" + rgb[0] + ", " + rgb[1] + ", " + rgb[2] + ")";
}


/**
 * Make a string for display from the given number limiting the number of decimal places shown
 *
 * @param number the number that will be presented as string
 * @param places the number of decimal places that will be returned in the string
 * @return the string representation of the given number
 */
function string_from_number(number, places) {
	var numstring;

	numstring = number.toFixed(places);

	/* tidy up the number string: trim trailing 0's and ensure there's a digit before the decimal point */
	while (numstring.charAt(numstring.length - 1) == "0") {  // trailing 0's
		numstring = numstring.substring(0, numstring.length - 1);
	}

	// trailing .
	if (numstring.charAt(numstring.length - 1) == ".") {
		numstring = numstring.substring(0, numstring.length - 1);
	}

	// starting .
	if (numstring.charAt(0) == ".") {
		numstring = "0" + numstring;
	} else if (numstring.substring(0, 2) == "-.") {	// starting -.
		numstring = "-0." + numstring.substring(2, numstring.length);
	}

	return numstring;
}


/**
 * Convert degrees K to u'v'
 *
 * Blackbody formula. XYZ weights as corrected by Vos, so may not agree exactly with 1931 calculations.
 *
 * @param degK the value in Kelvin degrees
 * @return array with the u'v' values
 */
function convertDegKtoUV(degK) {
	var u, v;
	var l = 380.0;               // Wavelength in nm
	var X = 0.0;
	var Y = 0.0;
	var Z = 0.0;
	var S;
	var p;

	var nm2xyz = [
			[ 0.000251, 0.000019, 0.001167 ],
			[ 0.001007, 0.000074, 0.004689 ],
			[ 0.003549, 0.000261, 0.016576 ],
			[ 0.009340, 0.000688, 0.043851 ],
			[ 0.021445, 0.001628, 0.101474 ],
			[ 0.029059, 0.002540, 0.139704 ],
			[ 0.031153, 0.003526, 0.153929 ],
			[ 0.026991, 0.004354, 0.140132 ],
			[ 0.021752, 0.005582, 0.122993 ],
			[ 0.016332, 0.008465, 0.106054 ],
			[ 0.008592, 0.012934, 0.071981 ],
			[ 0.002965, 0.019354, 0.042533 ],
			[ 0.000453, 0.030052, 0.025173 ],
			[ 0.000868, 0.046799, 0.014706 ],
			[ 0.005961, 0.066058, 0.007292 ],
			[ 0.015599, 0.080200, 0.003939 ],
			[ 0.027352, 0.088760, 0.001908 ],
			[ 0.040777, 0.092570, 0.000836 ],
			[ 0.055835, 0.092574, 0.000386 ],
			[ 0.071420, 0.088574, 0.000217 ],
			[ 0.085634, 0.080944, 0.000172 ],
			[ 0.095601, 0.070431, 0.000118 ],
			[ 0.098591, 0.058708, 0.000086 ],
			[ 0.092740, 0.046799, 0.000041 ],
			[ 0.078822, 0.035448, 0.000024 ],
			[ 0.059144, 0.024655, 0.000009 ],
			[ 0.041176, 0.016282, 0.000005 ],
			[ 0.026037, 0.009955, 0.000002 ],
			[ 0.015103, 0.005675, 0.000001 ],
			[ 0.008014, 0.002977, 0.000001 ],
			[ 0.004283, 0.001582, 0.000000 ],
			[ 0.002073, 0.000764, 0.000000 ],
			[ 0.001037, 0.000382, 0.000000 ],
			[ 0.000528, 0.000195, 0.000000 ],
			[ 0.000264, 0.000097, 0.000000 ],
			[ 0.000131, 0.000048, 0.000000 ],
			[ 0.000062, 0.000023, 0.000000 ],
			[ 0.000030, 0.000011, 0.000000 ],
			[ 0.000015, 0.000006, 0.000000 ],
			[ 0.000007, 0.000003, 0.000000 ],
			[ 0.000004, 0.000001, 0.000000 ]
			];

	for (var counter = 0; counter < nm2xyz.length; counter++) {
		var tempV = nm2xyz[counter];
		p = 1 / (Math.pow(l, 5) * (Math.exp(14388000.0 / (l * degK)) - 1.0));
		l += 10;
		X += tempV[0] * p;
		Y += tempV[1] * p;
		Z += tempV[2] * p;
	}

	S = X + (15 * Y) + (3 * Z)

	u = 4 * X / S;
	v = 9 * Y / S;

	return [u, v]; // There was a third null element
}


/**
 * Converts XYZ to Kelvin degrees
 *
 * @param xyz array of three values
 * @return the converted Kelvin degree value
 */
function convertXYZtoDegK(xyz) {
	var x = xyz[0];
	var y = xyz[1];
	var z = xyz[2];

	var s = x + 15 * y + 3 * z;
	var u = 4 * x / s;
	var v = 9 * y / s;
	var k0 = 0;
	var k1 = 0;
	var d0 = 10; // Dummy large value
	var d1 = 0;
	var del = Math.pow(10, precision);

	var v1_a = new Array();

	var v0 = new Array();
	v0[2] = [0, 0];

	var v1 = new Array();
	v1[2] = [0, 0];

	var threshold = Math.pow(10, -6);

	for (var m = 1 / 700; m > threshold; m -= del) {
		k1 = 1 / m;
		v1_a = convertDegKtoUV(k1)
		v1[0] = v1_a[0];
		v1[1] = v1_a[1];

		d1 = (u - v1[0]) * (u - v1[0]) + (v - v1[1]) * (v - v1[1]);

		if (d1 > d0) {
			del /= 2;

			if (del < threshold) {
				break;
			}

			m += 3 * del;
		} else {
			d0 = d1;
			v0[0] = v1[0];
			v0[1] = v1[1];
			k0 = k1;
		}
	}

	if (v1[0] != v0[0] && v1[1] != v0[1]) {
		k1 = k0 + (k1 - k0) * ((u - v0[0]) * (v1[0] - v0[0]) + (v - v0[1]) * (v1[1] - v0[1])) / ((v1[0] - v0[0]) * (v1[0] - v0[0]) + (v1[1] - v0[1]) * (v1[1] - v0[1]));
	}

	k1 = Math.floor(k1 + 0.5);

	return k1;
}


/**
 * Convert daylight D to U'V'
 *
 * Formula from Wyszecki & Stiles pp145-146. This does not agree exactly with tables in other books.
 *
 * @param d the daylight value
 * @return the converted u'v' values in an array
 */
function convertDtoUV(d) {
	var s, t, x, y, u, v;

	t = 10 / d;

	if (d < 7) {
		if (d < 7.0) x = 0.244063 + t * (0.09911 + t * (2.9678 - t * 4.6070));

	} else {
		x = 0.237040 + t * (0.24748 + t * (1.9018 - t * 2.0064));
	}
	y = -0.275 + x * (2.87 - 3 * x);
	s = (12.0 * y - 2.0 * x + 3.0);
	u = 4.0 * x / s;
	v = 9.0 * y / s;


	return [u, v];
}


/**
 * Converts XYZ to daylight D
 *
 * @param xyz array of three values
 * @return the converted daylight value
 */
function convertXYZtoD(xyz) {
	var x = xyz[0];
	var y = xyz[1];
	var z = xyz[2];

	var s = x + 15 * y + 3 * z;
	var u = 4 * x / s;
	var v = 9 * y / s;
	var k0 = 0;
	var k1 = 0;
	var d0 = 10; // Dummy large value
	var d1 = 0;
	var del = 0.0001;

	var v1_a = new Array();

	var v0 = new Array();
	v0[2] = [0, 0];

	var v1 = new Array();
	v1[2] = [0, 0];

	var threshold = 0.004;

	for (var m = 0.025; m > threshold; m -= del) {
		k1 = 1 / m;
		v1_a = convertDtoUV(k1);
		v1[0] = v1_a[0];
		v1[1] = v1_a[1];

		d1 = (u - v1[0]) * (u - v1[0]) + (v - v1[1]) * (v - v1[1]);

		if (d1 > d0) {
			del /= 2;

			if (del < 1.0e-8) {
				break;
			}

			m += 3 * del;
		} else {
			d0 = d1;
			v0[0] = v1[0];
			v0[1] = v1[1];
			k0 = k1;
		}
	}

	if (v1[0] != v0[0] && v1[1] != v0[1]) {
		k1 = k0 + (k1 - k0) * ((u - v0[0]) * (v1[0] - v0[0]) + (v - v0[1]) * (v1[1] - v0[1])) / ((v1[0] - v0[0]) * (v1[0] - v0[0]) + (v1[1] - v0[1]) * (v1[1] - v0[1]));
	}

	k1 = Math.floor(k1 + 0.5);

	return k1;
}


/**
 * Convert XYZ to an array of RGB values
 *
 * The xyz array can include white XYZ that will be used for the calculation
 *
 * @param xyz an array of XYZ values that will be converted to RGB
 * @return an array with RGB values
 */
function convertXYZtoRGB(xyz) {
	var R, G, B, w, h, m, mx;
	var x = xyz[0];
	var y = xyz[1];
	var z = xyz[2];

    // default white point 16 ft-lamberts D65
	var whiteX = 15.2028;
	var whiteY = 16.0;
	var whiteZ = 17.3962;

	if (xyz.length >= 6) {
		whiteX = xyz[3];
		whiteY = xyz[4];
		whiteZ = xyz[5];
	}

	if (x < 0.0) x = 0.0;
	if (y < 0.0) x = 0.0;
	if (z < 0.0) x = 0.0;

	/* Convert to R-709 primaries */
	w = +3.080014 * whiteX - 1.537119 * whiteY - 0.542894 * whiteZ;
	R = (+3.080014 * x - 1.537119 * y - 0.542894 * z) / w;
	w = -0.921307 * whiteX + 1.876051 * whiteY + 0.045256 * whiteZ;
	G = (-0.921307 * x + 1.876051 * y + 0.045256 * z) / w;
	w = 0.0528790 * whiteX - 0.203989 * whiteY + 1.151110 * whiteZ;
	B = (0.0528790 * x - 0.203989 * y + 1.151110 * z) / w;

	/* Convert to probe stops */
	h = Math.log(2.0);
	if (R < 0.000001) R = 0.000001;
	if (G < 0.000001) G = 0.000001;
	if (B < 0.000001) B = 0.000001;

	/* Scale to make white text still visible against this background. */
	mx = 1.3;
	if (mx < R) mx = R;
	if (mx < G) mx = G;
	if (mx < B) mx = B;
	R /= mx;
	G /= mx;
	B /= mx;

	R = 4.0 / (3.2 - Math.log(R) / h) - 0.25;
	if (R < 0.0) R = 0.0;
	G = 4.0 / (3.2 - Math.log(G) / h) - 0.25;
	if (G < 0.0) G = 0.0;
	B = 4.0 / (3.2 - Math.log(B) / h) - 0.25;
	if (B < 0.0) B = 0.0;

	m = R;
	if (m < G) m = G;
	if (m < B) m = B;
	if (m <= 1.0) m = 1.0;

	R = Math.round((R / m) * 255);
	G = Math.round((G / m) * 255);
	B = Math.round((B / m) * 255);

	/* extra range check */
	if (R > 255) R = 255;
	if (R < 0) R = 0;
	if (G > 255) G = 255;
	if (G < 0) G = 0;
	if (B > 255) B = 255;
	if (B < 0) B = 0;

	return [R, G, B];
}


