/*
 * Create a snake object. The argument is document.location.search or any string with the same format.
 */
Snake = function(search) {
	var strings = search.substring(1).split("&");

	for ( var i = 0; i < strings.length; i++) {
		var string = strings[i];
		var k = string.indexOf("=");
		if (k != -1)
			this[string.substring(0, k)] = decodeURIComponent(string.substring(k + 1));
	}

	this.s = (this.s == undefined) ? "" : this.s;
	this.colored = (this.colored != "false");
	this.orientation = (this.orientation == undefined) ? -1 : eval(this.orientation);
	this.size = (this.size == undefined) ? 0 : eval(this.size);
	this.smooth = (this.smooth == undefined) ? 0 : eval(this.smooth);
	this.check = (this.check == "true");
}

/*
 * Convert the number x (0 <= x <= 255) to a two-digit hex string.
 */
Snake.prototype.hex2 = function(x) {
	x = Math.round(x);
	return (x < 16 ? "0" : "") + x.toString(16);
}

/*
 * Color gradient. For 0 <= t <= 1, return a vector of three numbers r,g,b with
 * 0 <= r,g,b <= 255.
 */
Snake.prototype.color = function(t) {
	if (t <= 0.5)
		return [ (1 - 4 * t * t) * 255, 0, (4 * t * t) * 255 ]; // red -> blue
	else
		return [ 0, (2 * t - 1) * 255, (2 - 2 * t) * 255 ]; // blue -> green
}

/*
 * Remove linefeeds, replace whitespace and "+" by ",".
 */
Snake.normalise = function(s) {
	return s.replace(/[\n\r]/g, "").replace(/[\s]*$/, "").replace(/[\+\s]+/g, ",");
}

/*
 * Is s given in alpha format?
 */
Snake.isAlpha = function(s) {
	return s.search(/[A-Za-z]/) != -1 || s.search(/^[-]?[0-9]{1,2}[+,][-]?0/) != -1;
}

/*
 * Switch between alphabetical and numerical format.
 */
Snake.switchFormat = function(s) {
	s = Snake.normalise(s);

	var codeOf0 = "0".charCodeAt(0);
	var codeOfA = "A".charCodeAt(0);
	var codeOfZ = "Z".charCodeAt(0);
	var codeOfa = "a".charCodeAt(0);
	var codeOfz = "z".charCodeAt(0);

	var N = eval(s.substring(0, s.indexOf(",")));
	var N2 = N/2;
	var N2floor = Math.floor(N2);
	var N2ceil = Math.ceil(N2);
	var s1 = "" + N;

	if (Snake.isAlpha(s)) {
		for ( var i = s.indexOf(",") + 1; i < s.length; i++) {
			var c = s.charCodeAt(i);

			if (c == codeOf0)
				s1 += "," + N2;
			else if (c >= codeOfa && c <= codeOfz)
				s1 += "," + (N2ceil - 1 - (c - codeOfa));
			else if (c >= codeOfA && c <= codeOfZ)
				s1 += "," + (N2floor + 1 + (c - codeOfA));
		}
	} else {
		s1 += ",";

		var numbers = eval("[" + s + "]");

		for ( var i = 1; i < numbers.length; i++) {
			var x = numbers[i];

			if (2*x == N)
				s1 += "0";
			else if (2*x < N)
				s1 += String.fromCharCode(codeOfa - 1 + N2ceil - x);
			else
				s1 += String.fromCharCode(codeOfA - 1 + x - N2floor);
		}
	}

	return s1;
}

/*
 * Mirror all angles.
 */
Snake.mirror = function(s) {
	var wasAlpha = Snake.isAlpha(s);

	if (wasAlpha)
		s = Snake.switchFormat(s);
	else
		s = Snake.normalise(s);

	var numbers = eval("[" + s + "]");
	var N = numbers[0];

	for ( var i = 1; i < numbers.length; i++)
		numbers[i] = N - numbers[i];

	var s = "" + N;

	for ( var i = 1; i < numbers.length; i++)
		s += "," + numbers[i];

	if (wasAlpha)
		s = Snake.switchFormat(s);

	return s;
}

/*
 * Revert begin and end.
 */
Snake.revert = function(s) {
	var wasAlpha = Snake.isAlpha(s);

	if (wasAlpha)
		s = Snake.switchFormat(s);
	else
		s = Snake.normalise(s);

	var numbers = eval("[" + s + "]");
	var N = numbers.shift();

	numbers.push(N);
	numbers.reverse();

	s = "" + N;

	for ( var i = 1; i < numbers.length; i++)
		s += "," + numbers[i];

	if (wasAlpha)
		s = Snake.switchFormat(s);

	return s;
}

Snake.prototype.click = function(canvas, x, y) {
	this.makeData();

	var points = this.points;
	if (points.length == 0)
		return this.s;

	var numbers = this.numbers;
	var s = this.s
	var wasAlpha = Snake.isAlpha(s);

	if (wasAlpha)
		s = Snake.switchFormat(s);
	else
		s = Snake.normalise(s);

	var g = canvas.getContext("2d");
	var point = new Geometry.Point(x, y);
	var minI, minDist2;

	for (var i = 0; i < points.length - 1; i++) {
		var x1 = g.convertWorldX(points[i].x);
		var y1 = g.convertWorldY(points[i].y);
		var x2 = g.convertWorldX(points[i+1].x);
		var y2 = g.convertWorldY(points[i+1].y);
		var p1 = new Geometry.Point(x1, y1);
		var p2 = new Geometry.Point(x2, y2);
		var segment = new Geometry.Segment(p1, p2);
		var dist2 = point.distanceToSegment2(segment);

		if (i == 0 || dist2 < minDist2) {
			minI = i;
			minDist2 = dist2;
		}
	}

	numbers[minI] *= -1;

	s = "" + numbers[0];

	for ( var i = 1; i < numbers.length; i++)
		s += "," + numbers[i];

	if (wasAlpha)
		s = Snake.switchFormat(s);

	return s;
}

/*
 * If this.s defines a snake, then calculate its data and return true. Else
 * return false.
 */
Snake.prototype.makeData = function() {
	var s = this.s;

	if (Snake.isAlpha(s))
		s = Snake.switchFormat(s);
	else
		s = Snake.normalise(s);

	if (s.search(/^[-]?[0-9]{1,2},.*/) == -1)
		return false;

	var N = Math.abs(eval(s.substring(0, s.indexOf(","))));
	if (N == 0)
		return false;

	if (s.search(/^([-]?[0-9]{1,2},)*[-]?[0-9]{1,2}$/) != 0)
		return false;

	var numbers = eval("[" + s + "]");
	var points = new Array(numbers.length + 1);
	var dir = 0;
	var dirCount = new Array(2 * N);

	for ( var i = 0; i < 2 * N; i++)
		dirCount[i] = 0;

	points[0] = new Geometry.Point(0, 0);
	points[1] = new Geometry.Point(1, 0);

	for ( var i = 1; i < numbers.length; i++) {
		dir = (dir + 2 * Math.abs(numbers[i]) + N) % (2 * N);
		var x = points[i].x + Math.cos(Math.PI / N * dir);
		var y = points[i].y + Math.sin(Math.PI / N * dir);
		points[i + 1] = new Geometry.Point(x, y);
		dirCount[dir]++;
	}

	var ori = this.orientation;
	if (ori == -1) {
		ori = 0;
		for ( var d = 1; d < 2 * N; d++)
			if (dirCount[d] > dirCount[ori])
				ori = d;
	}

	for ( var i = 0; i < points.length; i++) {
		var x = points[i].x;
		var y = points[i].y;
		points[i].x = x * Math.cos(Math.PI / N * ori) + y * Math.sin(Math.PI / N * ori);
		points[i].y = y * Math.cos(Math.PI / N * ori) - x * Math.sin(Math.PI / N * ori);
	}

	var isClosed = points[0].equals(points[points.length - 1]);
	var isSymmetric = true;
	for ( var i = 1, j = numbers.length - 1; i <= j && isSymmetric; i++, j--)
		if (Math.abs(numbers[i]) != Math.abs(N - numbers[j]))
			isSymmetric = false;

	var circle = Geometry.Circle.minidisk(points);

	// the Rosenthal smoothing
	for ( var k = 0; k < this.smooth; k++) {
		for ( var i = 0; i < points.length - 1; i++) {
			points[i].x = 0.5 * (points[i].x + points[i + 1].x);
			points[i].y = 0.5 * (points[i].y + points[i + 1].y);
		}

		if (isClosed) {
			points[points.length - 1] = new Geometry.Point(points[0].x, points[0].y);
		} else
			points.length--;
	}

	this.N = N;
	this.numbers = numbers;
	this.points = points;
	this.isClosed = isClosed;
	this.isSymmetric = isSymmetric;
	this.circle = circle;
	this.radius = Math.sqrt(circle.radius2);

	return true;
}

/*
 * The smallest distance of segments. 0.0 if there are self-intersections.
 */
Snake.prototype.smallestDistance = function() {
	var minDist2 = 1.0e99;
	var points = this.points;

	for ( var i = 0; i < points.length; i++) {
		var seg1 = new Geometry.Segment(points[i], points[i + 1]);
		for ( var j = i + 2; j < points.length - 1; j++) {
			if (!this.isClosed || i != 0 || j != points.length - 2) {
				var seg2 = new Geometry.Segment(points[j], points[j + 1]);
				var dist2 = seg1.distanceToSegment2(seg2);
				minDist2 = Math.min(minDist2, dist2);
			}
		}
	}

	return Math.sqrt(minDist2);
}

/*
 * Return the text for the data element.
 */
Snake.prototype.dataText = function() {
	var nSegments = this.points.length - 1;
	if (!this.isClosed)
		nSegments += this.smooth;

	return "N = " + this.N + ", segments = " + nSegments + ", d = " + (2 * this.radius).toFixed(10) + ".";
}

/*
 * Return the text for the info element.
 */
Snake.prototype.infoText = function() {
	var text = "";

	if (this.check || this.isClosed || this.isSymmetric)
		text = "This snake is:";

	var minDist = this.smallestDistance();

	if (this.check) {
	    if (minDist > 0.0)
		text += " valid (min.dist. = " + minDist + ")";
	    else
		text += " <span style = \"color:red\">invalid</span>";
	}

	if (this.isClosed)
		text += " closed";

	if (this.isSymmetric)
		text += " symmetric";

	return text;
}

/*
 * Does this snake have the same points as the given snake?
 */
Snake.prototype.equals = function(snake) {
	return this.s == snake.s && this.orientation == snake.orientation && this.smooth == snake.smooth;
}

/*
 * Draw to the given canvas, data and info elements.
 */
Snake.prototype.drawTo = function(canvas, data, info) {
	var previous = Snake.previous;

	if (previous != undefined && this.equals(previous)) {
		this.N = previous.N;
		this.numbers = previous.numbers;
		this.points = previous.points;
		this.isClosed = previous.isClosed;
		this.isSymmetric = previous.isSymmetric;
		this.circle = previous.circle;
		this.radius = previous.radius;
	} else {
		if (!this.makeData()) {
			data.innerHTML = "The input is not a snake definition.";
			info.innerHTML = "";
			return;
		}
		Snake.previous = this;
	}

	data.innerHTML = this.dataText();
	info.innerHTML = this.infoText();

	var numbers = this.numbers;
	var points = this.points;
	var circle = this.circle;
	var radius = this.radius;
	var d = radius * 1.01;
	var g = canvas.getContext("2d");

	g.setWorld(circle.center.x - d, circle.center.y - d, circle.center.x + d, circle.center.y + d);

	g.strokeStyle = "grey";
	g.beginPath();
	g.moveTo(0, 0);
	g.lineTo(canvas.width - 1, 0);
	g.lineTo(canvas.width - 1, canvas.height - 1);
	g.lineTo(0, canvas.height - 1);
	g.lineTo(0, 0);
	g.stroke();

	for ( var i = 1; i < points.length; i++) {
		var col = (this.colored) ? this.color((i - 1) / (points.length - 2)) : [ 0, 0, 0 ];

		g.beginPath();

		if (numbers[i-1] < 0)
			g.strokeStyle = "#dddddd";
		else
			g.strokeStyle = "#" + this.hex2(col[0]) + this.hex2(col[1]) + this.hex2(col[2]);

		var x1 = points[i - 1].x;
		var y1 = points[i - 1].y;
		var x2 = points[i].x;
		var y2 = points[i].y;

		g.moveToWorld(x1, y1);
		g.lineToWorld(x2, y2);
		g.stroke();

		if (this.smooth != "0") {
			g.strokeStyle = "grey";
			g.beginPath();
			g.arcWorld(x1, y1, radius / 120, 0, 2 * Math.PI, false);
			g.stroke();
		}
	}

	g.strokeStyle = "grey";
	g.beginPath();
	g.moveToWorld(circle.center.x, circle.center.y - radius / 25);
	g.lineToWorld(circle.center.x, circle.center.y + radius / 25);
	g.stroke();

	g.strokeStyle = "grey";
	g.beginPath();
	g.moveToWorld(circle.center.x - radius / 25, circle.center.y);
	g.lineToWorld(circle.center.x + radius / 25, circle.center.y);
	g.stroke();

	g.strokeStyle = "blue";
	g.beginPath();
	g.arcWorld(circle.center.x, circle.center.y, radius, 0, 2 * Math.PI, false);
	g.stroke();

	g.strokeStyle = "grey";
	g.fillStyle = "blue";

	for ( var i = 0; i < points.length; i++) {
		var x = points[i].x;
		var y = points[i].y;

		if (i == 0 || i == points.length - 1) {
			g.beginPath();
			g.arcWorld(x, y, radius / 120, 0, 2 * Math.PI, false);
			g.stroke();
		} else if (((x - circle.center.x).squared() + (y - circle.center.y).squared())
				.equals(circle.radius2)) {
			g.beginPath();
			g.arcWorld(x, y, radius / 120, 0, 2 * Math.PI, false);
			g.fill();
		}
	}
}
