/**
 * CertainTrust SDK
 * 
 * Implements the computational trust model "CertainTrust"
 * in JavaScript.
 * See <http://www.tk.informatik.tu-darmstadt.de/de/research/smart-security-and-trust/> for further details.
 * 
 * 
 * Telecooperation Department, Technische Universität Darmstadt
 * <http://www.tk.informatik.tu-darmstadt.de/>
 * 
 * Prof. Dr. Max Mühlhäuser <max@informatik.tu-darmstadt.de>
 * Florian Volk <florian.volk@cased.de>
 * 
 * 
 * @author	David Kalnischkies
 * @author	Florian Volk
 * @version	1.0
 */
 
 /* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 

/** Certain Trust Widget
 *
 * Creates a color-coded graph created from an initial trust value
 * in which the user can pick certainty (y-axis) and trust (x-axis)
 * with the mouse in the graph.
 *
 * The values are displayed and can be modified inside a form
 * while the graph in the canvas will be updated from those values.
 *
 * Options can be given in an object as parameter to the constructor
 * The following keys are understood:
 *  id is the id the top-div of the widget will get. Its used as basename for all element ids in it, too
 *  label is an object for various messages which are shown to the user
 *   lang is the language to use. Available are 'de' and 'en' (default)
 *   Alternatively f, t, c and e can be used to set the text explicitly
 *  canvas is an object containing subkeys 'width' and 'height' defining dimensions of the canvas element
 *  line is an object defining the style of the lines building the cross to mark the chosen trust/certainty
 *   cap is the canvas.lineCap defining the style used to smooth the end of the lines
 *   height is the length of the line from start to the crosspoint
 *   width is the thickness of the line
 *   style is the color of the line
 *  readonly can be used to disable input in the HTI as a whole,
 *   Alternatively f, t, c, e, inputs for all of the previous and mouse can be used to disable specific elements.
 *  show can be used to disable display of certain inputs refered to as
 *   f, t, c, e, inputs for all of the previous, title and axes.
 *
 * One of the following keys can be used to specify the position of the widget in the DOM,
 * the first defined will be used. If none is provided domAfter will be set to the <script>-tag calling
 * the creation of the widget (= the last <script>-tag currently in the DOM)
 * Either the DOM element itself can be given or the ID of the element as a string.
 *  domReturn is set to true to get the DOM subtree as return value of the constructor instead [update() is NOT called]
 *  domParent is (the id of) the parent for the widget
 *  domBefore is (the id of) the element which the widget will be in front of
 *  domAfter is (the id of) the element after which the widget will be inserted
 */



var CertainTrustHTI = function(certainlogic, config) {
	if (certainlogic === undefined)
		this.certainTrust = new CertainTrust(5);
	else
		this.certainTrust = certainlogic;

	this.NR = CertainTrustHTIElement.length();
	CertainTrustHTIElement.push(this);
	this.certainTrust.addObserver(this);

	// set sane defaults for config if nothing is set
	if (config === undefined) config = {};
	if (config.id === undefined) config.id = 'certaintrust-hti-' + this.NR;
	// design your widget
	if (config.canvas === undefined) config.canvas = {};
	if (config.canvas.height === undefined) config.canvas.height = 100;
	if (config.canvas.width === undefined) config.canvas.width = 120;
	if (config.line === undefined) config.line = {};
	if (config.line.cap === undefined) config.line.cap = 'round';
	if (config.line.height === undefined) config.line.height = 5;
	if (config.line.width === undefined) config.line.width = 1;
	if (config.line.style === undefined) config.line.style = 'black';
	if (config.line.baserad === undefined) config.line.baserad = 45;
	else config.line.baserad %= 90;
	if (config.confidence === undefined) config.confidence = {};
	if (config.confidence.cap === undefined) config.confidence.cap = 'round';
	if (config.confidence.height === undefined) config.confidence.height = 5;
	if (config.confidence.width === undefined) config.confidence.width = 0;
	if (config.confidence.style === undefined) config.confidence.style = 'gray';
	if (config.confidence.quantil === undefined) config.confidence.quantil = 1.96;

	// language settings
	if (config.label === undefined) config.label = {};
	if (config.label.lang === undefined) config.label.lang = 'en';
	if (config.label.lang === 'de') {
		if (config.label.f === undefined) config.label.f = 'Initialwert';
		if (config.label.t === undefined) config.label.t = 'Vertrauen';
		if (config.label.c === undefined) config.label.c = 'Sicherheit';
		if (config.label.e === undefined) config.label.e = 'Erwartung';
	} else {
		if (config.label.f === undefined) config.label.f = 'Init. value';
		if (config.label.t === undefined) config.label.t = 'Trust';
		if (config.label.c === undefined) config.label.c = 'Certainty';
		if (config.label.e === undefined) config.label.e = 'Expectation';
	}
	// readonly forms maybe?
	var readonlyflag = false;
	if (config.readonly === undefined) {
		config.readonly = {};
		config.readonly.e = true;
	} else {
		readonlyflag = config.readonly;
		config.readonly = {};
	}
	if (config.readonly.inputs === undefined) config.readonly.inputs = readonlyflag;
	if (config.readonly.f === undefined) config.readonly.f = config.readonly.inputs;
	if (config.readonly.t === undefined) config.readonly.t = config.readonly.inputs;
	if (config.readonly.c === undefined) config.readonly.c = config.readonly.inputs;
	if (config.readonly.e === undefined) config.readonly.e = config.readonly.inputs;
	if (config.readonly.mouse === undefined) config.readonly.mouse = readonlyflag;

	// show/disable elements
	var showflag = true;
	if (config.show === undefined) config.show = {};
	else {
		showflag = config.show;
		config.show = {};
	}
	if (config.show.inputs === undefined) config.show.inputs = showflag;
	if (config.show.f === undefined) config.show.f = config.show.inputs;
	if (config.show.t === undefined) config.show.t = config.show.inputs;
	if (config.show.c === undefined) config.show.c = config.show.inputs;
	if (config.show.e === undefined) config.show.e = config.show.inputs;
	if (config.show.title === undefined) config.show.title = showflag;
	if (config.show.axes === undefined) config.show.axes = showflag;

	this.ID = config.id;
	this.config = config;
	var element = document.createElement('div');
	element.setAttribute('id', this.ID);
	element.setAttribute('class', 'certaintrust-hti');
	if (config.show.title === true && this.certainTrust.getName().length !== 0)
	{
		var title = document.createElement('h1');
		var msg = document.createTextNode(this.certainTrust.getName());
		title.appendChild(msg);
		element.appendChild(title);
	}
	var form = document.createElement('form');
	form.setAttribute('id', this.ID + '-form');
	var appendInput = function (form, id, nr, type, value, text, readonly, show) {
		if (show === false)
			return;
		var div = document.createElement('div');
		div.setAttribute('class', 'certaintrust-hti-' + type);
		var label = document.createElement('label');
		label.setAttribute('for', id + '-' + type);
		div.appendChild(label);
		var labeltext = document.createTextNode(text);
		label.appendChild(labeltext);
		var input = document.createElement('input');
		input.setAttribute('type', 'text');
		input.setAttribute('id', id + '-' + type);
		var cte = CertainTrustHTIElement.ByNr(nr);
		input.setAttribute('value', value);
		if (readonly === true) {
			input.setAttribute('readonly', 'readonly');
			input.setAttribute('tabindex', '-1');
		} else {
			input.addEventListener('keypress', cte._onKeyPress, false);
			input.addEventListener('blur', cte._onBlur, false);
		}
		div.appendChild(input);
		form.appendChild(div);
	};
	appendInput(form, this.ID, this.NR, 'f', this.certainTrust.getF(), config.label.f, config.readonly.f, config.show.f);
	appendInput(form, this.ID, this.NR, 't', this.certainTrust.getT(), config.label.t, config.readonly.t, config.show.t);
	appendInput(form, this.ID, this.NR, 'c', this.certainTrust.getC(), config.label.c, config.readonly.c, config.show.c);
	appendInput(form, this.ID, this.NR, 'e', this.certainTrust.getExpectation(), config.label.e, config.readonly.e, config.show.e);
	if (form.hasChildNodes())
		element.appendChild(form);

	var hti = document.createElement('div');
	hti.style.cssFloat=hti.style.styleFloat='left';
	if (config.show.axes === true)
	{
		var yaxis = document.createElement('span');
		yaxis.setAttribute('class', 'certaintrust-hti-yaxis');
		yaxis.style.cssFloat=yaxis.style.styleFloat='left';
		// width still defines the width of the box even if the
		// element in it is rotated, so we need to set this to 1em
		// even if the text will overflow it to have the canvas close
		yaxis.style.width='1em';
		// transform in theory:
		yaxis.style.transform='rotate(270deg) translate(-4em,0em)';
		yaxis.style.transformOrigin='100% 100%';
		// transform in practice:
		yaxis.style.MozTransform='rotate(270deg) translate(-4em,0em)';
		yaxis.style.MozTransformOrigin='100% 100%';
		yaxis.style.webkitTransform='rotate(270deg) translate(-4em,0em)';
		yaxis.style.webkitTransformOrigin='100% 100%';
		yaxis.style.msTransform='rotate(270deg) translate(-4em,0em)';
		yaxis.style.msTransformOrigin='100% 100%';
		yaxis.style.OTransform='rotate(270deg) translate(-4em,0em)';
		yaxis.style.OTransformOrigin='100% 100%';
		// \u00a0 is a non-breaking space, \u2192 is a right arrow
		var yaxislabel = document.createTextNode(config.label.c + '\u00a0\u2192');
		yaxis.appendChild(yaxislabel);
		hti.appendChild(yaxis);
	}
	this.canvas = document.createElement('canvas');
	this.canvas.style.cssFloat=this.canvas.style.styleFloat='left';
	this.canvas.setAttribute('id', this.ID + '-canvas');
	this.canvas.setAttribute('width', config.canvas.width);
	this.canvas.setAttribute('height', config.canvas.height);
//	this.canvas.setAttribute('title', this.ID); // useful to identify which widget is which
	this.canvas.addEventListener("mousedown", this._onClick, false);
	this.canvas.addEventListener("mousemove", this._onMove, false);
	hti.appendChild(this.canvas);
	if (config.show.axes === true)
	{
		var origin = document.createElement('span');
		origin.style.textAlign='center';
		origin.style.width='1em';
		origin.style.clear='both';
		origin.style.cssFloat=origin.style.styleFloat='left';
		var originlabel = document.createTextNode('0');
		origin.appendChild(originlabel);
		hti.appendChild(origin);
		var xaxis = document.createElement('span');
		xaxis.style.cssFloat=xaxis.style.styleFloat='left';
		var xaxislabel = document.createTextNode(config.label.t + '\u00a0\u2192');
		xaxis.appendChild(xaxislabel);
		hti.appendChild(xaxis);
	}
	element.appendChild(hti);

	var dom = this.certainTrust._insertElement(config, element);
	if (dom !== undefined)
		return dom;

	this.update();
};

/** (re)draws the canvas
 *  The widget must be in the DOM tree to be able to be drawn */
CertainTrustHTI.prototype.update = function() {
	var ctx = this.canvas.getContext('2d');
	var width = parseInt(this.canvas.getAttribute('width'), 10);
	var height = parseInt(this.canvas.getAttribute('height'), 10);

	var initf = this.certainTrust.getF();
	var imageData = ctx.createImageData(width, height);
	var d = 0;
	for (var y = 0; y < height; ++y) {
		var certainty = 1 - (y / height);
		for (var x = 0; x < width; ++x) {
			var trust = x / width;
			var color = this.certainTrust._getColor(certainty, trust, initf);
			// each pixel consists of four numbers: red, green, blue and alpha ranging from 0 to 255
			imageData.data[d++] = color[0];
			imageData.data[d++] = color[1];
			imageData.data[d++] = color[2];
			imageData.data[d++] = 255; // set no alpha-transparency
		}
	}
	ctx.putImageData(imageData, 0, 0);

	// put a 'cross' on the 'pixel' representing certainty/trust
	var dotmiddle = Math.floor(this.config.line.width / 2);
	var doty = Math.round(((1 - this.certainTrust.getC()) * height) - dotmiddle);
	var dotx = Math.round((this.certainTrust.getT() * width) - dotmiddle);

	// confidence interval
	if (this.config.confidence.width !== 0) {
		// if the line has an odd-size we have to place it on half-pixels or it looks odd
		var middle = (this.config.confidence.width % 2 === 0) ? 0 : 0.5;
		// calculate upper/lower bound of Wilson confidence interval
		var K = this.config.confidence.quantil;
		var x = this.certainTrust.getR();
		var n = this.certainTrust.getR() + this.certainTrust.getS();
		var p = this.certainTrust.getT();
		var wilson = this.intervalCertainty(K, x, n, p, width);

		ctx.beginPath();
		ctx.moveTo(Math.floor(wilson.upper - dotmiddle), doty + middle - this.config.confidence.height);
		ctx.lineTo(Math.floor(wilson.upper - dotmiddle), doty + middle + this.config.confidence.height);
		ctx.moveTo(Math.floor(wilson.upper - dotmiddle), doty + middle);
		ctx.lineTo(Math.floor(wilson.lower - dotmiddle), doty + middle);
		ctx.moveTo(Math.floor(wilson.lower - dotmiddle), doty + middle - this.config.confidence.height);
		ctx.lineTo(Math.floor(wilson.lower - dotmiddle), doty + middle + this.config.confidence.height);
		ctx.lineWidth = this.config.confidence.width;
		ctx.lineCap = this.config.confidence.cap;
		ctx.strokeStyle = this.config.confidence.style;
		ctx.stroke();
		ctx.closePath();
	}

	// if the line has an odd-size we have to place it on half-pixels or it looks odd
	var middle = (this.config.line.width % 2 === 0) ? 0 : 0.5;
	var line1 = new Array(
		this.certainTrust._pointOnCircle(dotx, doty + middle, (this.config.line.baserad + 0), this.config.line.height),
		this.certainTrust._pointOnCircle(dotx, doty + middle, (this.config.line.baserad + 180), this.config.line.height)
	);
	var line2 = new Array(
		this.certainTrust._pointOnCircle(dotx, doty + middle, (this.config.line.baserad + 90), this.config.line.height),
		this.certainTrust._pointOnCircle(dotx, doty + middle, (this.config.line.baserad + 270), this.config.line.height)
	);

	ctx.beginPath();
	ctx.moveTo(Math.round(line1[0][0]) + middle, Math.round(line1[0][1]) + middle);
	ctx.lineTo(Math.round(line1[1][0]) + middle, Math.round(line1[1][1]) + middle);
	ctx.moveTo(Math.round(line2[0][0]) + middle, Math.round(line2[0][1]) + middle);
	ctx.lineTo(Math.round(line2[1][0]) + middle, Math.round(line2[1][1]) + middle);
	ctx.lineWidth = this.config.line.width;
	ctx.lineCap = this.config.line.cap;
	ctx.strokeStyle = this.config.line.style;
	ctx.stroke();
	ctx.closePath();

	// display the certainTrust values in a user-friendly way without modifying their internal state
	this._setElementValue(this.ID + '-f', this.certainTrust.getF());
	this._setElementValue(this.ID + '-c', this.certainTrust.getC());
	this._setElementValue(this.ID + '-t', this.certainTrust.getT());
	this._setElementValue(this.ID + '-e', this.certainTrust.getExpectation());
};
CertainTrustHTI.prototype._setElementValue = function(id, value) {
	var element = document.getElementById(id);
	if (element === null)
		return;
	element.value = this._formatNumber(value);
};

/** calculating Wilson/Goldman interval boundaries */
CertainTrustHTI.prototype.intervalCertainty = function(K, x, n, p, width) {
	var K2 = Math.pow(K, 2);
	var nK2 = n + K2;
	var part1 = (x + (K2 / 2)) / nK2;
	var part2 = (K * Math.sqrt(n)) / nK2;
	var part3 = Math.sqrt(p * (1 - p) + (K2 / (4 * n)));
	var uwx = (part1 + part2 * part3) * width;
	var lwx = (part1 - part2 * part3) * width;
	return { upper: uwx, lower: lwx };
};

CertainTrustHTI.prototype._onMove = function(e) {
	if (CertainTrustHTIElement._isMouseDown !== true)
		return;
	var cte = CertainTrustHTIElement.ByCanvas(this);
	if (cte.config.readonly.mouse === true)
		return;
	cte._onClick(e, this);
};

CertainTrustHTI.prototype._onClick = function(e, clkcanvas) {
	// if it's called by onMove this is not the clicked canvas but the element, so we have to pass it on
	if (clkcanvas === undefined)
		clkcanvas = this;

	var cte = CertainTrustHTIElement.ByCanvas(clkcanvas);
	if (cte.config.readonly.mouse === true)
		return;
	// this could be the start of a drag across the canvas
	CertainTrustHTIElement._isMouseDown = true;

	// convert screen-relative coordinates to canvas-relatives to trust/certainty and limit these values to [0;1]
	var x = e.clientX;
	var y = e.clientY;
	// https://developer.mozilla.org/en-US/docs/Web/API/element.getBoundingClientRect
	var ctBounding = clkcanvas.getBoundingClientRect();
	var cx = ctBounding.left;
	var cy = ctBounding.top;
	var newT = Math.max(0, Math.min(1, (x - cx) / clkcanvas.width));
	var newC = Math.max(0, Math.min(1, 1 - ((y - cy) / clkcanvas.height)));
	cte.certainTrust.setTC(newT, newC);
};

CertainTrustHTI.prototype._onKeyPress = function(e) {
	if (e.keyCode != 13)
		return;

	// update values only in case the user pressed the return key
	var cid = this.id.substring(0, this.id.lastIndexOf('-'));
	var cte = CertainTrustHTIElement.ById(cid);
	cte._updateInput(this.id);
};

CertainTrustHTI.prototype._onBlur = function(e) {
	// update values if focus left the input field
	var cid = this.id.substring(0, this.id.lastIndexOf('-'));
	var cte = CertainTrustHTIElement.ById(cid);
	cte._updateInput(this.id);
};

CertainTrustHTI.prototype._updateInput = function(id) {
	// this is the input-element the key was pressed in - thankfully the id for
	// these input fields are autogenerated from the id of the widget
	var cid = id.substring(0, id.lastIndexOf('-'));
	if (id.substring(id.lastIndexOf('-')) === '-f') {
		var newF = this._normalizeInput(document.getElementById(cid + '-f').value);
		this.certainTrust.setF((isNaN(newF)) ? this.certainTrust.getF() : newF);
	} else { // if ( == '-c' || == '-t')
		var newT = this._normalizeInput(document.getElementById(cid + '-t').value);
		var newC = this._normalizeInput(document.getElementById(cid + '-c').value);
		this.certainTrust.setTC(
			(isNaN(newT)) ? this.certainTrust.getT() : newT,
			(isNaN(newC)) ? this.certainTrust.getC() : newC
		);
	}
};

// parse the user-input to a number (best attempt approach)
CertainTrustHTI.prototype._normalizeInput = function(input) {
	// first, replace the first "," with "." to enable German-language style floating point input
	var rawInput = (input+"").replace(/,/, ".");
	// now, strip out all leading 0s to prevent parseFloat from treating the input as octal
	rawInput = rawInput.replace(/^0+\./, ".");

	// convert to a number
	var floatInput = parseFloat(rawInput); // attention, this may be NaN -> _updateInput handles this
	if ((1 < floatInput) || (0 > floatInput)) {
		return NaN;
	} else {
		return floatInput;
	}
};

// rounds numbers to at most 3 decimal places
CertainTrustHTI.prototype._formatNumber = function(number) {
	// return number.toFixed(3);
	return Math.round(number * 1000) / 1000;
};

// global var for storing and accessing all the widgets
var CertainTrustHTIElement = { _elements: [],
	_isMouseDown: false,
	ByCanvas: function(canvas) {
		for (var i = 0; i < CertainTrustHTIElement._elements.length; ++i) {
			if (CertainTrustHTIElement._elements[i].canvas === canvas)
				return CertainTrustHTIElement._elements[i];
		}
		return null;
	},
	ById: function(id) {
		for (var i = 0; i < CertainTrustHTIElement._elements.length; ++i) {
			if (CertainTrustHTIElement._elements[i].ID === id)
				return CertainTrustHTIElement._elements[i];
		}
		return null;
	},
	ByNr: function(nr) { return CertainTrustHTIElement._elements[nr]; },
	push: function(cte) { CertainTrustHTIElement._elements.push(cte); },
	length: function() { return CertainTrustHTIElement._elements.length; }

};

// react on every mouseup - even outside of a canvas
document.addEventListener("mouseup", function() { CertainTrustHTIElement._isMouseDown = false; }, false);
