/*
 * -- PieGraph
 * --
 * -- Copyright 2009, A.McBain
 *
 * Redistribution and use, with or without modification, are permitted provided that the following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
 *    2. Modified redistributions of the source must provide or link to a copy of the original source.
 *    3. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * --
 *
 * While this code is released "as is", that doesn't mean the author won't take valid bug reports.
 */

/*
 * A Canvas-based PieGraph. PieGraph can be either "2d" or "3d",
 * and takes a map of the objects it is to display. If the items
 * in the map change, the PieGraph will update. If new items are
 * added to the map, they are won't be monitored.
 *
 * There are public functions for forcing an update of the
 * PieGraph, and suppressing the automatic updates if many
 * changes will be made in a short period of time. Calling the
 * reindex function will update the automatic watches to
 * include any new values added since the last time that
 * function was called or the PieGraph was created (whichever
 * was sooner).
 *
 * All items to be displayed in this PieGraph should add up to
 * 100, or very close to it, as PieGraph doesn't do any
 * adjustments to data it is given.
 */
function PieGraph(parent, type, items, itemColors) {
	var $this = this;

	if(!(parent instanceof Frame || parent instanceof Window)) {
		throw "PieGraph parent element must be a Frame";
	}

	if(type instanceof String) type = type.toLowerCase();
	if(type === null || type === undefined || (type !== "2d" && type !== "3d")) {
		type = "2d";
	}

	if(!(items instanceof Object)) {
		throw "PieGraph requires a map of items";
	}

	if(!(itemColors instanceof Object)) {
		itemColors = {};
	}

	var frame = new Frame();
	parent.appendChild(frame);

	var canvas = new Canvas();
	frame.appendChild(canvas);
	var ctx = canvas.getContext("2d");

	var legendFrame = new Frame();
	var legendCanvas = new Canvas();
	legendFrame.appendChild(legendCanvas);
	var ltx = legendCanvas.getContext("2d");
	var tframe = null;

	var legend = {};
	legend.__defineGetter__("width",   function()      { return legendFrame.width;           });
	legend.__defineSetter__("width",   function(value) { return legendFrame.width   = value; });
	legend.__defineGetter__("height",  function()      { return legendFrame.height;          });
	legend.__defineSetter__("height",  function(value) { return legendFrame.height  = value; });
	legend.__defineGetter__("hOffset", function()      { return legendFrame.hOffset;         });
	legend.__defineSetter__("hOffset", function(value) { return legendFrame.hOffset = value; });
	legend.__defineGetter__("vOffset", function()      { return legendFrame.vOffset;         });
	legend.__defineSetter__("vOffset", function(value) { return legendFrame.vOffset = value; });
	legend.__defineGetter__("visible", function()      { return legendFrame.visible;         });
	legend.__defineSetter__("visible", function(value) { return legendFrame.visible = value; });
	legend.__defineSetter__("parent",  function(value) {
		if(value instanceof Frame || value instanceof Window) {
			value.appendChild(legendFrame);
		}
		return value;
	});
	legend.toString = function() {
		return "[object PieGraphLegend]";
	};

	this.PERCENT = PieGraph.PERCENT;
	this.VALUE = PieGraph.VALUE;
	this.NONE = PieGraph.NONE;

	var suppressUpdates = false;
	var showPercentages = true;
	var borderColor = "#6e6a66"; // Rather "arbitrary" gray color.
	var labelType = this.NONE;
	var style = (new Text()).style;

	var tempItem = "";
	var tempValue = "";

	var total = 0; // internal use only.
	var pieHeight = 20;
	var keySize = 10;

	function updateLegend() {
		if(tframe instanceof Frame) {

			var text = tframe.firstChild;
			for(var i in items) {
				text.data = i;

				if(labelType === $this.PERCENT) {
					text.data += " ( " + Math.floor(items[i] / total * 100) + "% )";
				} else if(labelType === $this.VALUE) {
					text.data += " ( " + items[i] + " )";
				}

				for(var j in style) {
					if(style[j] !== "") {
						text.style[j] = style[j];
					}
				}

				text = text.nextSibling;
			}
		}
	}

	function rgb(h, s, v) {
		h = h / 360;
		s = s / 100;
		v = v / 100;

		var r = v;
		var g = v;
		var b = v;

		if(s !== 0) {
			h = h * 6;
			var i = Math.floor(h);
			var f = h - i;
			var p = v * (1 - s);
			var q = v * (1 - s * f);
			var t = v * (1 - s * (1 - f));

			switch (i) {
				case 0:
					r = v;
					g = t;
					b = p;
					break;
				case 1:
					r = q;
					g = v;
					b = p;
					break;
				case 2:
					r = p;
					g = v;
					b = t;
					break;
				case 3:
					r = p;
					g = q;
					b = v;
					break;
				case 4:
					r = t;
					g = p;
					b = v;
					break;
				default:
					r = v;
					g = p;
					b = q;
					break;
			}
		}

		r = Math.round(255 * r);
		g = Math.round(255 * g);
		b = Math.round(255 * b);

		return "rgb(" + r + ", " +  g + ", " + b + ")";
 	}

	function generateHue(hues, min, difference) {
		var used = true;
		var h = 0;

		while(used) {
			h = Math.max(min + 1, Math.floor(Math.random() * 360));
			used = false;
			var length = hues.length;
			for(var i = 0; i < length && !used; i++) {
				if(Math.abs(hues[i] - h) <= difference) {
					used = true;
				}
			}
		}

		return h;
	}

	this.paintGraph = function() {
		var width = canvas.height - pieHeight;
		var height = canvas.height;

		ctx.clearRect(0, 0, width, height);

		legendCanvas.height = 0;
		legendCanvas.width = style.fontSize.replace("px") || 12;

		var colors = {};
		var hues = [];
		var difference = 25; //(360 - 15) / items.__count__; // bad! ranges are too large, but seems correct in theory.

		total = 0;
		for(var i in items) {
			if(!(typeof itemColors[i] === "string" || itemColors[i] instanceof String)) {
				var hue = generateHue(hues, 15, difference);
				hues.push(hue);
				colors[i] = rgb(hue, 90, 100);
			} else {
				colors[i] = itemColors[i];
			}
			total += items[i];
			legendCanvas.height += legendCanvas.width + 5;
		}

		ctx.save();
		ctx.strokeStyle = borderColor;
		if(type === "3d") {
			ctx.scale(1, .5);

			ctx.save();
			ctx.translate(width / 2, width / 2 + pieHeight);

			var tarc = 0;
			for(var i in items) {
				var arc = 2 * Math.PI * (items[i] / total);

				ctx.fillStyle = colors[i];
				ctx.beginPath();
				ctx.arc(0, 0, (width - 2) / 2, tarc, tarc + arc, false);
				ctx.lineTo(Math.cos(tarc + arc) * (width / 2), Math.sin(tarc + arc) * (width / 2) - pieHeight + 1);
				ctx.lineTo(Math.cos(tarc) * (width / 2), Math.sin(tarc) * (width / 2) - pieHeight + 1);
				ctx.closePath();
				ctx.fill();
				for(var j = 0; j < 3; j++)
					ctx.stroke();

				tarc += arc;
			}
			ctx.restore();
		}
		ctx.translate(width / 2, width / 2);

		for(var i in items) {
			var arc = 2 * Math.PI * (items[i] / total);

			ctx.fillStyle = colors[i];
			ctx.beginPath();
			ctx.moveTo(0, 0);
			ctx.lineTo(width / 2 - 1, 0);
			ctx.arc(0, 0, (width - 2) / 2, 0, arc, false);
			ctx.closePath();
			ctx.fill();
			ctx.stroke();
			ctx.rotate(arc);
		}

		if(type === "3d") {
			ctx.fillStyle = ctx.strokeStyle;
			ctx.fillRect(-width / 2, 2, 1, pieHeight);
		}

		ctx.restore();

		if(tframe instanceof Frame) {
			legendFrame.removeChild(tframe);
		}
		tframe = new Frame();
		legendFrame.appendChild(tframe);

		var vOffset = 5;
		var keySize = legendCanvas.width;
		for(var i in items) {
			ltx.fillStyle = borderColor;
			ltx.fillRect(0, vOffset, keySize, keySize);
			ltx.fillStyle = colors[i];
			ltx.fillRect(1, vOffset + 1, keySize - 2, keySize - 2);

			var text = new Text();
			text.anchorStyle = "topLeft";
			text.data = i;
			text.hOffset = keySize + 5;
			text.vOffset = vOffset - 1;
			tframe.appendChild(text);

			if(labelType === $this.PERCENT) {
				text.data += " ( " + Math.floor(items[i] / total * 100) + "% )";
			} else if(labelType === $this.VALUE) {
				text.data += " ( " + items[i] + " )";
			}

			for(var j in style) {
				if(style[j] !== "") {
					text.style[j] = style[j];
				}
			}

			vOffset += keySize + 5;
		}
	};

	this.reindex = function() {
		for(var i in items) {
			items.watch(i, function(p,o,v) {
				tempItem = p;
				tempValue = v;

				if(!suppressUpdates) {
					$this.paintGraph();
				}

				return v;
			});
		}
	};
	this.reindex();

	this.setSlice = function(key, value) {
		if(items[key] !== undefined) {
			if(items[key] === undefined) {
				items.watch(key, function(p,o,v) {
					tempItem = p;
					tempValue = v;

					if(!suppressUpdates) {
						$this.paintGraph();
					}

					return v;
				});
			}
			items[key] = value;
		}
	};

	this.setColor = function(key, value) {
		if(items[key] !== undefined) {
			itemColors[key] = value;
			this.paintGraph();
		}
	};

	this.getLegend = function() {
		return legend;
	};

	this.__defineGetter__("suppressUpdates", function() { return suppressUpdates; });
	this.__defineSetter__("suppressUpdates", function(value) {
		suppressUpdates = Boolean(value);
		return value;
	});

	this.__defineGetter__("labelType", function() { return labelType; });
	this.__defineSetter__("labelType", function(value) {
		if(value === this.PERCENT || value === this.NONE || value === this.VALUE) {
			labelType = value;
			updateLegend();
		}
		return value;
	});

	this.__defineGetter__("style", function() { return style; });
	this.__defineSetter__("style", function(value) {
		if(value instanceof Style) {
			style = value;
			updateLegend();
		}
		return value;
	});

	this.__defineGetter__("borderColor", function() { return borderColor; });
	this.__defineSetter__("borderColor", function(value) {
		borderColor = value;
		this.paintGraph();
		return value;
	});

	this.__defineGetter__("width", function() { return frame.width; });
	this.__defineSetter__("width", function(value) {
		canvas.width = Math.max(canvas.height + 2, value);
		frame.width = canvas.width;
		this.paintGraph();
		return value;
	});
	this.__defineGetter__("height", function() { return frame.height; });
	this.__defineSetter__("height", function(value) {
		canvas.height = value + pieHeight;
		frame.height = canvas.height;
		this.paintGraph();
		return value;
	});

	this.__defineGetter__("hOffset", function() { return frame.hOffset; });
	this.__defineSetter__("hOffset", function(value) {
		return frame.hOffset = value;
	});

	this.__defineGetter__("vOffset", function() { return frame.vOffset; });
	this.__defineSetter__("vOffset", function(value) {
		return frame.vOffset = value;
	});

	this.__defineGetter__("visible", function() { return frame.visible; });
	this.__defineSetter__("visible", function(value) {
		return frame.visible = value;
	});

	this.toString = function() {
		return "[object PieGraph]";
	};
}

PieGraph.PERCENT = "percent";
PieGraph.VALUE = "value";
PieGraph.NONE = "none";

