/******************************************************
*
* Utils
* requires jQuery, Three.js
*
******************************************************/

var D3D = {
	"version": "0.1", 
	"website": "http://www.doodle3d.com/", 
	"contact": "develop@doodle3d.com"
};

function sendAPI (url, data, callback) {
	"use strict";

	$.ajax({
		url: url, 
		type: "POST", 
		data: data, 
		dataType: "json", 
		timeout: 10000, 
		success: function (response) {
			if (response.status === "success") {
				if (callback !== undefined) {
					callback(response.data);
				}
			}
			else {
				console.warn(response.msg);
			}
		}
	}).fail(function () {
		console.warn("Failed connecting to " + url);
		sendAPI(url, data, callback);
	});
}

function getAPI (url, callback) {
	"use strict";

	$.ajax({
		url: url, 
		dataType: "json", 
		timeout: 5000, 
		success: function (response) {
			if (response.status === "success") {
				if (callback !== undefined) {
					callback(response.data);
				}
			}
			else {
				console.warn(response.msg);
			}
		}
	}).fail(function () {
		console.warn("Failed connecting to " + url);
		getAPI(url, callback);
	});
}

function loadSettings (url, callback) {
	"use strict";

	$.ajax({
		url: url, 
		dataType: "json", 
		success: function (response) {
			if (callback !== undefined) {
				callback(response);
			}
		}
	});
}

function downloadFile (file, data) {
	"use strict";

	var blob = new Blob([data], {type:'text/plain'});

	var button = document.createElement("a");
	button.download = file;
	button.href = window.URL.createObjectURL(blob);
	button.click();
}

Array.prototype.clone = function () {
	"use strict";
	var array = [];

	for (var i = 0; i < this.length; i ++) {
		array[i] = this[i];
	}

	return array;
}
/******************************************************
*
* Box
* Representation of de Doodle3DBox
* Handles all communication with the doodle box
* JavaScript shell for api communication
* Check http://www.doodle3d.com/help/api-documentation
*
******************************************************/

//TODO
//Als meerdere clients met box zouden verbinden zal de api te veel requests krijgen waardoor hij crasht
//implimentatie van het veranderen van onder andere naam, netwerkverbinding etc

D3D.Box = function (localIp) {
	"use strict";
	var self = this;

	this.batchSize = 512;
	this.maxBufferedLines = 4096;

	this.localIp = localIp;
	this.api = "http://" + localIp + "/d3dapi/";

	this.config = {};
	this.status = {};

	this.printBatches = [];
	this.currentBatch = 0;

	this.loaded = false;

	this.getConfigAll(function (data) {
		self.updateConfig(data);

		self.update();

		self.loaded = true;
		if (self.onload !== undefined) {
			self.onload();
		}
	});
};
D3D.Box.prototype.updateConfig = function (config) {
	"use strict";

	for (var i in config) {
		this.config[i] = config[i];
	}

	return this;
};
D3D.Box.prototype.update = function () {
	"use strict";
	//TODO
	//Code is zo op gezet dat maar api call te gelijk is
	//Bij error wordt gelijk zelfde data opnieuw gestuurd
	//Als DoodleBox ontkoppeld wordt komt er een error in de loop waardoor pagina breekt en ververst moet worden

	if (this.printBatches.length > 0 && (this.status["buffered_lines"] + this.batchSize) <= this.maxBufferedLines) {
	//if (this.printBatches.length > 0 ) {
		this.printBatch();
	}
	else {
		this.updateState();
	}
};
D3D.Box.prototype.updateState = function () {
	//que api calls so they don't overload the d3d box
	"use strict";
	var self = this;

	this.getInfoStatus(function (data) {
		self.status = data;

		if (self.onupdate !== undefined) {
			self.onupdate(data);
		}

		self.update();
	});
};
D3D.Box.prototype.print = function (gcode) {
	"use strict";

	this.currentBatch = 0;

	//clone gcode to remove array links
	gcode = gcode.clone();

	//gcode split in batches
	while (gcode.length > 0) {
		var gcodeBatch = gcode.splice(0, Math.min(this.batchSize, gcode.length));
		this.printBatches.push(gcodeBatch);
	}

	return this;
};
D3D.Box.prototype.printBatch = function () {
	"use strict";
	var self = this;

	var gcode = this.printBatches.shift();

	this.setPrinterPrint({
		"start": ((this.currentBatch === 0) ? true : false), 
		"first": ((this.currentBatch === 0) ? true : false), 
		"gcode": gcode.join("\n")
	}, function (data) {
		console.log("batch sent: " + self.currentBatch, data);

		if (self.printBatches.length > 0) {
			//sent new batch
			self.currentBatch ++;
		}
		else {
			//finish sending
		}

		self.updateState();
	});
};
D3D.Box.prototype.stopPrint = function () {
	"use strict";

	this.printBatches = [];
	this.currentBatch = 0;

	var finishMove = [
		"M107 ;fan off", 
		"G91 ;relative positioning", 
		"G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure", 
		"G1 Z+0.5 E-5 X-20 Y-20 F9000 ;move Z up a bit and retract filament even more", 
		"G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way", 
		"M84 ;disable axes / steppers", 
		"G90 ;absolute positioning", 
		"M104 S180", 
		";M140 S70", 
		"M117 Done                 ;display message (20 characters to clear whole screen)"
	];

	this.setPrinterStop({
		//"gcode": {}
		"gcode": finishMove.join("\n")
	}, function (data) {
		console.log("Printer stop command sent");
	});

	return this;
};

//COMMUNICATION SHELL
//see http://www.doodle3d.com/help/api-documentation
D3D.Box.prototype.getConfig = function (keys, callback) {
	"use strict";

	getAPI(this.api + "config/?" + keys.join("=&") + "=", callback);

	return this;
};
D3D.Box.prototype.getConfigAll = function (callback) {
	"use strict";

	getAPI(this.api + "config/all", callback);

	return this;
};
D3D.Box.prototype.setConfig = function (data, callback) {
	"use strict";
	var self = this;

	sendAPI(this.api + "config", data, function (response) {
		for (var i in response.validation) {
			if (response.validation[i] === "ok") {
				self[i] = data[i];
			}
		}

		if (callback !== undefined) {
			callback(response);
		}
	});

	return this;
};
D3D.Box.prototype.getInfo = function (callback) {
	"use strict";

	getAPI(this.api + "info", callback);

	return this;
};
D3D.Box.prototype.getInfoStatus = function (callback) {
	"use strict";

	getAPI(this.api + "info/status", callback);

	return this;
};
D3D.Box.prototype.downloadInfoLogFiles = function () {
	//works in google chrome... not tested in other browsers
	//some browsers may redirect using this code
	"use strict";

	window.location = this.api + "info/logfiles";
};
D3D.Box.prototype.getInfoAcces = function (callback) {
	"use strict";

	getAPI(this.api + "info/access", callback);

	return this;
};
D3D.Box.prototype.getNetworkScan = function (callback) {
	"use strict";

	getAPI(this.api + "network/scan", callback);

	return this;
};
D3D.Box.prototype.getNetworkKnown = function (callback) {
	"use strict";

	getAPI(this.api + "network/known", callback);

	return this;
};
D3D.Box.prototype.getNetworkStatus = function (callback) {
	"use strict";

	getAPI(this.api + "network/status", callback);

	return this;
};
D3D.Box.prototype.setNetworkAssosiate = function (data, callback) {
	"use strict";

	sendAPI(this.api + "network/associate", data, callback);	

	return this;
};
D3D.Box.prototype.setNetworkDisassosiate = function (callback) {
	//not tested
	"use strict";

	sendAPI(this.api + "network/disassociate", {}, callback);

	return this;	
};
D3D.Box.prototype.setNetworkOpenAP = function (callback) {
	//not tested
	"use strict";

	sendAPI(this.api + "network/openap", {}, callback);

	return this;	
};
D3D.Box.prototype.setNetworkRemove = function (ssid, callback) {
	"use strict";

	sendAPI(this.api + "network/remove", {
		"ssid": ssid
	}, callback);

	return this;	
};
D3D.Box.prototype.getNetworkSignin = function (callback) {
	"use strict";

	getAPI(this.api + "network/signin", callback);

	return this;
};
D3D.Box.prototype.getNetworkAlive = function (callback) {
	"use strict";

	getAPI(this.api + "network/alive", callback);

	return this;
};
D3D.Box.prototype.getPrinterTemperature = function (callback) {
	"use strict";

	getAPI(this.api + "printer/temperature", callback);

	return this;
};
D3D.Box.prototype.getPrinterProgress = function (callback) {
	"use strict";

	getAPI(this.api + "printer/progress", callback);

	return this;
};
D3D.Box.prototype.getPrinterState = function (callback) {
	"use strict";

	getAPI(this.api + "printer/state", callback);

	return this;
};
D3D.Box.prototype.getPrinterListAll = function (callback) {
	"use strict";

	getAPI(this.api + "printer/listall", callback);

	return this;
};
D3D.Box.prototype.setPrinterHeatup = function (callback) {
	"use strict";

	sendAPI(this.api + "printer/heatup", {}, callback);

	return this;
};
D3D.Box.prototype.setPrinterPrint = function (data, callback) {
	"use strict";

	sendAPI(this.api + "printer/print", data, callback);

	return this;
};
D3D.Box.prototype.setPrinterStop = function (data, callback) {
	"use strict";

	sendAPI(this.api + "printer/stop", data, callback);

	return this;
};
D3D.Box.prototype.getSketch = function (id, callback) {
	"use strict";

	getAPI(this.api + "sketch/?id=" + id, callback);
	
	return this;
};
D3D.Box.prototype.setSketch = function (data, callback) {
	"use strict";

	sendAPI(this.api + "sketch", {
		"data": data
	}, callback);
	
	return this;
};
D3D.Box.prototype.getSketchStatus = function (callback) {
	"use strict";

	getAPI(this.api + "sketch/status", callback);
	
	return this;
};
D3D.Box.prototype.setSketchClear = function (callback) {
	"use strict";

	sendAPI(this.api + "sketch/clear", callback);
	
	return this;
};
D3D.Box.prototype.getSystemVersions = function (callback) {
	"use strict";

	getAPI(this.api + "system/fwversions", callback);
	
	return this;
};
D3D.Box.prototype.getUpdateStatus = function (callback) {
	"use strict";

	getAPI(this.api + "update/status", callback);
	
	return this;
};
D3D.Box.prototype.setUpdateDownload = function (callback) {
	//not tested
	"use strict";

	sendAPI(this.api + "update/download", {}, callback);
	
	return this;
};
D3D.Box.prototype.setUpdateInstall = function (callback) {
	//not tested
	"use strict";

	sendAPI(this.api + "update/install", {}, callback);
	
	return this;
};
D3D.Box.prototype.setUpdateClear = function (callback) {
	//not tested
	"use strict";

	sendAPI(this.api + "update/clear", {}, callback);
	
	return this;
};
/******************************************************
*
* Printer
* Representation of the connected printer
*
******************************************************/

D3D.Printer = function (printerSettings, userSettings) {
	"use strict";

	this.config = {};

	this.updateConfig(printerSettings);	
	this.updateConfig(userSettings);
};
D3D.Printer.prototype.updateConfig = function (config) {
	"use strict";

	for (var i in config) {
		if (i.indexOf("printer") === 0) {
			this.config[i] = config[i];
		}
	}

	return this;
};
D3D.Printer.prototype.getStartCode = function () {
	"use strict";
	
	var gcode = this.config["printer.startcode"];
	gcode = this.subsituteVariables(gcode);

	return gcode.split("\n");
};
D3D.Printer.prototype.getEndCode = function () {
	"use strict";
	
	var gcode = this.config["printer.endcode"];
	gcode = this.subsituteVariables(gcode);

	return gcode.split("\n");
};
D3D.Printer.prototype.subsituteVariables = function (gcode) {
	"use strict";

	var temperature = this.config["printer.temperature"];
	var bedTemperature = this.config["printer.bed.temperature"];
	var preheatTemperature = this.config["printer.heatup.temperature"];
	var preheatBedTemperature = this.config["printer.heatup.bed.temperature"];
	var printerType = this.config["printer.type"];
	var heatedbed = this.config["printer.heatedbed"];

	switch (printerType) {
		case "makerbot_replicator2": printerType = "r2"; break; 
		case "makerbot_replicator2x": printerType = "r2x"; break;
		case "makerbot_thingomatic": printerType = "t6"; break;
		case "makerbot_generic": printerType = "r2"; break;
		case "_3Dison_plus": printerType = "r2"; break;
	}
	var heatedBedReplacement = heatedbed ? "" : ";";

	gcode = gcode.replace(/{printingTemp}/gi, temperature);
	gcode = gcode.replace(/{printingBedTemp}/gi, bedTemperature);
	gcode = gcode.replace(/{preheatTemp}/gi, preheatTemperature);
	gcode = gcode.replace(/{preheatBedTemp}/gi, preheatBedTemperature);
	gcode = gcode.replace(/{printerType}/gi, printerType);
	gcode = gcode.replace(/{if heatedBed}/gi, heatedBedReplacement);

	return gcode;
};
/******************************************************
*
* Path
*
* Abstraction layer for annoying clipper js
*
******************************************************/

D3D.Paths = function (paths, closed) {
	"use strict";

	Array.call(this);

	this.setPaths(paths || []);

	this.closed = (closed !== undefined) ? closed : true;
};
D3D.Paths.prototype = Object.create(Array.prototype);
D3D.Paths.prototype.setPaths = function (paths) {
	"use strict";

	for (var i = 0; i < paths.length; i ++) {
		var path = paths[i];
		if (path.length > 0) {
			this.push(path);
		}
	}

	return this;
};
D3D.Paths.prototype.clip = function (path, type) {
	"use strict";

	var solution = new ClipperLib.Paths();

	var clipper = new ClipperLib.Clipper();
	clipper.AddPaths(this, ClipperLib.PolyType.ptSubject, this.closed);
	clipper.AddPaths(path, ClipperLib.PolyType.ptClip, path.closed);
	clipper.Execute(type, solution);

	return new D3D.Paths(solution, this.closed);
};
D3D.Paths.prototype.union = function (path) {
	"use strict";

	return this.clip(path, ClipperLib.ClipType.ctUnion);
};
D3D.Paths.prototype.difference = function (path) {
	"use strict";

	return this.clip(path, ClipperLib.ClipType.ctDifference);
};
D3D.Paths.prototype.intersect = function (path) {
	"use strict";

	return this.clip(path, ClipperLib.ClipType.ctIntersection);
};
D3D.Paths.prototype.xor = function () {
	"use strict";

	return this.clip(path, ClipperLib.ClipType.ctXor);
};
D3D.Paths.prototype.offset = function (offset) {
	"use strict";

	var solution = new ClipperLib.Paths();
	var co = new ClipperLib.ClipperOffset(1, 1);
	co.AddPaths(this, ClipperLib.JoinType.jtRound, ClipperLib.EndType.etClosedPolygon);
	co.Execute(solution, offset);

	return new D3D.Paths(solution);
};
D3D.Paths.prototype.scaleUp = function (factor) {
	"use strict";

	var path = ClipperLib.JS.ScaleUpPaths(this, factor);

	return this;
};
D3D.Paths.prototype.scaleDown = function (factor) {
	"use strict";

	var path = ClipperLib.JS.ScaleDownPaths(this, factor);

	return this;
};
D3D.Paths.prototype.lastPoint = function () {
	"use strict";

	var lastPath = this[this.length - 1];
	var lastPoint = this.closed ? lastPath[0] : lastPath[lastPath.length - 1];
	return new THREE.Vector2(lastPoint.X, lastPoint.Y);
};
D3D.Paths.prototype.optimizePath = function (start) {
	"use strict";

	var optimizedPaths = new D3D.Paths([], this.closed);
	var donePaths = [];

	while (optimizedPaths.length !== this.length) {
		var minLength = false;
		var reverse;
		var minPath;
		var offset;
		var pathIndex;

		for (var i = 0; i < this.length; i ++) {
			var path = this[i];

			if (donePaths.indexOf(i) === -1) {

				if (this.closed) {
					for (var j = 0; j < path.length; j ++) {
						var point = new THREE.Vector2(path[j].X, path[j].Y);
						var length = point.sub(start).length();
						if (minLength === false || length < minLength) {
							minPath = path;
							minLength = length;
							offset = j;
							pathIndex = i;
						}
					}
				}
				else {
					var startPoint = new THREE.Vector2(path[0].X, path[0].Y);
					var length = startPoint.sub(start).length();
					if (minLength === false || length < minLength) {
						minPath = path;
						minLength = length;
						reverse = false;
						pathIndex = i;
					}
					var endPoint = new THREE.Vector2(path[path.length - 1].X, path[path.length - 1].Y);
					var length = endPoint.sub(start).length();
					if (length < minLength) {
						minPath = path;
						minLength = length;
						reverse = true;
						pathIndex = i;
					}
				}
			}
		}

		if (this.closed) {
			minPath = minPath.concat(minPath.splice(0, offset));
			var point = minPath[0];
		}
		else {
			if (reverse) {
				minPath.reverse();	
			}
			var point = minPath[minPath.length - 1];
		}
		donePaths.push(pathIndex);
		start = new THREE.Vector2(point.X, point.Y);
		optimizedPaths.push(minPath);
	}

	return optimizedPaths;
};
D3D.Paths.prototype.tresholdArea = function (minArea) {
	//code not tested yet
	"use strict";

	for (var i = 0; i < this.length; i ++) {
		var shape = this[i];

		var area = ClipperLib.Clipper.Area(shape);

		if (area < minArea) {
			this.splice(i, 1);
			i --;
		}
	}
	
	return areas;
};
D3D.Paths.prototype.join = function (path) {
	"use strict";

	for (var i = 0; i < path.length; i ++) {
		this.push(path[i]);
	}

	return this;
};
D3D.Paths.prototype.clone = function () {
	"use strict";

	return new D3D.Paths(ClipperLib.JS.Clone(this), this.closed);
};
D3D.Paths.prototype.bounds = function () {
	"use strict";

	return ClipperLib.Clipper.GetBounds(this);
};
D3D.Paths.prototype.draw = function (context, color) {
	"use strict";

	context.strokeStyle = color;
	for (var i = 0; i < this.length; i ++) {
		var shape = this[i];

		//var point = shape[0];
		//context.fillText(i, point.X*2, point.Y*2);

		context.beginPath();
		var length = this.closed ? (shape.length + 1) : shape.length;
		for (var j = 0; j < length; j ++) {
			var point = shape[j % shape.length];

			context.lineTo(point.X*2, point.Y*2);
		}
		context.stroke();
	}
};
/******************************************************
*
* Slicer
*
* TODO (optimalisatie)
* sorteer lijnen op laagste hoogte -> stop loop wanneer hij een lijn zonder intersectie heeft gevonden
* verwijder lijnen die ooit interactie gehad hebben, maar nu niet meer
* helft van lijnen toevoegen omdat 4face altijd recht is, en 3face dus te veel data bevat
* 
* omliggende lagen -> difference && sum omliggende lijnen
* voor laag 5 = 5 diff (3 && 4 && 6 && 7))
*
******************************************************/

D3D.Slicer = function () {
	"use strict";
};
D3D.Slicer.prototype.setMesh = function (mesh) {
	"use strict";

	//convert buffergeometry to geometry;
	var geometry = mesh.geometry.clone();
	if (geometry instanceof THREE.BufferGeometry) {
		geometry = new THREE.Geometry().fromBufferGeometry(geometry);
	}

	//remove duplicate vertices;
	for (var i = 0; i < geometry.vertices.length; i ++) {
		var vertexA = geometry.vertices[i];

		for (var j = i + 1; j < geometry.vertices.length; j ++) {
			var vertexB = geometry.vertices[j];

			if (vertexA.equals(vertexB)) {
				geometry.vertices[j] = vertexA;
			}
		}
	}
	geometry.mergeVertices();

	//apply mesh matrix on geometry;
	mesh.updateMatrix();
	geometry.applyMatrix(mesh.matrix);
	geometry.computeFaceNormals();
	geometry.computeBoundingBox();
	
	this.geometry = geometry;

	//get unique lines from geometry;
	this.createLines();

	return this;
};
D3D.Slicer.prototype.createLines = function () {
	"use strict";

	this.lines = [];
	var lineLookup = {};

	var self = this;
	function addLine (a, b) {

		//think lookup can only be b_a, a_b is only possible when face is flipped
		var index = lineLookup[b + "_" + a] || lineLookup[a + "_" + b];

		if (index === undefined) {
			index = self.lines.length;
			lineLookup[a + "_" + b] = index;

			self.lines.push({
				line: new THREE.Line3(self.geometry.vertices[a], self.geometry.vertices[b]), 
				connects: [], 
				normals: [],
				ignore: 0
			});
		}

		return index;
	}

	for (var i = 0; i < this.geometry.faces.length; i ++) {
		var face = this.geometry.faces[i];

		if (face.normal.y !== 1 && face.normal.y !== -1) {
			var normal = new THREE.Vector2().set(face.normal.z, face.normal.x).normalize();

			//check for only adding unique lines
			//returns index of said line
			var a = addLine(face.a, face.b);
			var b = addLine(face.b, face.c);
			var c = addLine(face.c, face.a);

			//set connecting lines (based on face)
			this.lines[a].connects.push(b, c);
			this.lines[b].connects.push(c, a);
			this.lines[c].connects.push(a, b);

			this.lines[a].normals.push(normal);
			this.lines[b].normals.push(normal);
			this.lines[c].normals.push(normal);
		}
	}
};
D3D.Slicer.prototype.slice = function (height, step) {
	"use strict";

	var layersIntersections = [];

	for (var i = 0; i < this.lines.length; i ++) {
		var line = this.lines[i];

		var min = Math.ceil(Math.min(line.line.start.y, line.line.end.y) / step);
		var max = Math.floor(Math.max(line.line.start.y, line.line.end.y) / step);

		for (var layerIndex = min; layerIndex <= max; layerIndex ++) {
			if (layerIndex >= 0) {
				if (layersIntersections[layerIndex] === undefined) {
					layersIntersections[layerIndex] = [];
				}
				layersIntersections[layerIndex].push(i);
			}
		}
	}

	var slices = [];

	//still error in first layer, so remove first layer & last layer
	//see https://github.com/Doodle3D/Doodle3D-Slicer/issues/1
	for (var layer = 1; layer < layersIntersections.length-1; layer ++) {
	//for (var layer = 0; layer < layersIntersections.length; layer ++) {
		var layerIntersections = layersIntersections[layer];
		var y = layer*step;

		var intersections = [];
		for (var i = 0; i < layerIntersections.length; i ++) {
			var index = layerIntersections[i];
			var line = this.lines[index].line;

			var alpha = (y - line.start.y) / (line.end.y - line.start.y);
			var x = line.end.x * alpha + line.start.x * (1 - alpha);
			var z = line.end.z * alpha + line.start.z * (1 - alpha);

			intersections[index] = new THREE.Vector2(z, x);
		}

		var done = [];
		var slice = [];
		for (var i = 0; i < layerIntersections.length; i ++) {
			var index = layerIntersections[i];

			if (done.indexOf(index) === -1) {
				var shape = [];

				while (index !== -1) {
					var intersection = intersections[index];
					shape.push({X: intersection.x, Y: intersection.y});

					done.push(index);

					var connects = this.lines[index].connects;
					var faceNormals = this.lines[index].normals;
					for (var j = 0; j < connects.length; j ++) {
						index = connects[j];

						if (intersections[index] && done.indexOf(index) === -1) {
							var a = new THREE.Vector2(intersection.x, intersection.y);
							var b = intersections[index];
							var normal = a.sub(b).normal().normalize();
							var faceNormal = faceNormals[Math.floor(j/2)];

							if (normal.dot(faceNormal) > 0) {
								break;
							}
							else {
								index = -1;
							}
						}
						else {
							index = -1;
						}
					}
				}

				/*
				for (var i = 0; i < shape.length; i ++) {
					var point = shape[i];
					var previousPoint = shape[(i + shape.length - 1) % shape.length];
					var nextPoint = shape[(i + 1) % shape.length];

					var point = new THREE.Vector2(point.X, point.Y);
					var previousPoint = new THREE.Vector2(previousPoint.X, previousPoint.Y);
					var nextPoint = new THREE.Vector2(nextPoint.X, nextPoint.Y);
					//var lineLength = nextPoint.sub(previousPoint).length();

					var normal = nextPoint.sub(previousPoint).normal().normalize();
					var distance = Math.abs(normal.dot(point.sub(previousPoint)));

					//something better for offset check
					if (distance <= 0.01) {
						shape.splice(i, 1);
						i --;
					}
				}
				*/

				//think this check is not nescesary, always higher as 0
				if (shape.length > 0) {
					slice.push(new D3D.Paths([shape]));
				}
			}
		}

		var layerParts = [];

		for (var i = 0; i < slice.length; i ++) {
			var layerPart1 = slice[i];
			var merge = false;

			for (var j = 0; j < layerParts.length; j ++) {
				var layerPart2 = layerParts[j];

				if (layerPart2.intersect(layerPart1).length > 0) {
					layerPart2.join(layerPart1);
					merge = true;
					break;
				}
			}
			if (!merge) {
				layerParts.push(layerPart1);
			}
		}

		//stop when ther are no intersects
		if (layerParts.length > 0) {
			slices.push(layerParts);
		}
		else {
			break;
		}
	}

	return slices;
};
D3D.Slicer.prototype.slicesToData = function (slices, printer) {
	"use strict";

	var scale = 100;

	var layerHeight = printer.config["printer.layerHeight"] * scale;
	var dimensionsZ = printer.config["printer.dimensions.z"] * scale;
	var wallThickness = printer.config["printer.wallThickness"] * scale / 2;
	var shellThickness = printer.config["printer.shellThickness"] * scale;
	var fillSize = printer.config["printer.fillSize"] * scale;
	var brimOffset = printer.config["printer.brimOffset"] * scale;
	var bottomThickness = printer.config["printer.bottomThickness"] * scale;
	var topThickness = printer.config["printer.topThickness"] * scale;

	var bottomSkinCount = Math.ceil(bottomThickness/layerHeight);
	var topSkinCount = Math.ceil(topThickness/layerHeight);

	var start = new THREE.Vector2(0, 0);

	var data = [];

	var lowFillTemplate = this.getFillTemplate({
		left: this.geometry.boundingBox.min.z * scale, 
		top: this.geometry.boundingBox.min.x * scale, 
		right: this.geometry.boundingBox.max.z * scale, 
		bottom: this.geometry.boundingBox.max.x * scale
	}, fillSize, true, true);

	for (var layer = 0; layer < slices.length; layer ++) {
		var slice = slices[layer];
		
		var layerData = [];
		data.push(layerData);
		
		var downSkin = new D3D.Paths([], true);
		if (layer - bottomSkinCount >= 0) {
			var downLayer = slices[layer - bottomSkinCount];
			for (var i = 0; i < downLayer.length; i ++) {
				downSkin.join(downLayer[i]);
			}
		}
		var upSkin = new D3D.Paths([], true);
		if (layer + topSkinCount < slices.length) {
			var downLayer = slices[layer + topSkinCount];
			for (var i = 0; i < downLayer.length; i ++) {
				upSkin.join(downLayer[i]);
			}
		}
		var surroundingLayer = upSkin.intersect(downSkin).scaleUp(scale);
		var sliceData = [];

		for (var i = 0; i < slice.length; i ++) {
			var part = slice[i];

			//var outerLayer = part.clone();
			var outerLayer = part.clone().scaleUp(scale).offset(-wallThickness/2);

			if (outerLayer.length > 0) {
				var insets = new D3D.Paths([], true);
				for (var offset = wallThickness; offset <= shellThickness; offset += wallThickness) {
					var inset = outerLayer.offset(-offset);

					insets.join(inset);
				}

				var fillArea = (inset || outerLayer).offset(-wallThickness/2);
				//var fillArea = (inset || outerLayer).clone();

				var highFillArea = fillArea.difference(surroundingLayer);

				var lowFillArea = fillArea.difference(highFillArea);

				var fill = new D3D.Paths([], false);

				if (lowFillTemplate.length > 0) {
					fill.join(lowFillTemplate.intersect(lowFillArea));
				}

				if (highFillArea.length > 0) {
					var bounds = highFillArea.bounds();
					var even = (layer % 2 === 0);
					var highFillTemplate = this.getFillTemplate(bounds, wallThickness, even, !even);
					fill.join(highFillTemplate.intersect(highFillArea));
				}
				
				outerLayer = outerLayer.optimizePath(start);
				if (insets.length > 0) {
					insets = insets.optimizePath(outerLayer.lastPoint());
					fill = fill.optimizePath(insets.lastPoint());
				}
				else {
					fill = fill.optimizePath(outerLayer.lastPoint());
				}

				if (fill.length > 0) {
					start = fill.lastPoint();
				}
				else if (insets.length > 0) {
					start = insets.lastPoint();
				}
				else {
					start = outerLayer.lastPoint();
				}
				
				layerData.push({
					outerLayer: outerLayer.scaleDown(scale),
					fill: fill.scaleDown(scale),
					insets: insets.scaleDown(scale)
				});
			}
		}
	}

	return data;
};
D3D.Slicer.prototype.getFillTemplate = function (bounds, size, even, uneven) {
	"use strict";

	var paths = new D3D.Paths([], false);

	if (even) {
		for (var length = Math.floor(bounds.left/size)*size; length <= Math.ceil(bounds.right/size)*size; length += size) {
			paths.push([{X: length, Y: bounds.top}, {X: length, Y: bounds.bottom}]);
		}
	}
	if (uneven) {
		for (var length = Math.floor(bounds.top/size)*size; length <= Math.floor(bounds.bottom/size)*size; length += size) {
			paths.push([{X: bounds.left, Y: length}, {X: bounds.right, Y: length}]);
		}
	}
	
	//return paths;
	return paths;
};
D3D.Slicer.prototype.dataToGcode = function (data, printer) {
	"use strict";

	var layerHeight = printer.config["printer.layerHeight"];
	var normalSpeed = printer.config["printer.speed"];
	var bottomSpeed = printer.config["printer.bottomLayerSpeed"];
	var firstLayerSlow = printer.config["printer.firstLayerSlow"];
	var bottomFlowRate = printer.config["printer.bottomFlowRate"];
	var travelSpeed = printer.config["printer.travelSpeed"];
	var filamentThickness = printer.config["printer.filamentThickness"];
	var wallThickness = printer.config["printer.wallThickness"];
	var enableTraveling = printer.config["printer.enableTraveling"];
	var retractionEnabled = printer.config["printer.retraction.enabled"];
	var retractionSpeed = printer.config["printer.retraction.speed"];
	var retractionMinDistance = printer.config["printer.retraction.minDistance"];
	var retractionAmount = printer.config["printer.retraction.amount"];

	function sliceToGcode (slice) {
		var gcode = [];

		for (var i = 0; i < slice.length; i ++) {
			var shape = slice[i];

			var previousPoint;

			var length = slice.closed ? (shape.length + 1) : shape.length;

			for (var j = 0; j < length; j ++) {
				var point = shape[j % shape.length];

				if (j === 0) {
					//TODO
					//add retraction
					if (extruder > retractionMinDistance && retractionEnabled) {
						gcode.push([
							"G0", 
							"E" + (extruder - retractionAmount).toFixed(3), 
							"F" + (retractionSpeed * 60).toFixed(3)
						].join(" "));
					}

					gcode.push([
						"G0", 
						"X" + point.X.toFixed(3) + " Y" + point.Y.toFixed(3) + " Z" + z, 
						"F" + (travelSpeed * 60)
					].join(" "));

					if (extruder > retractionMinDistance && retractionEnabled) {
						gcode.push([
							"G0", 
							"E" + extruder.toFixed(3), 
							"F" + (retractionSpeed * 60).toFixed(3)
						].join(" "));
					}
				}
				else {
					var a = new THREE.Vector2(point.X, point.Y);
					var b = new THREE.Vector2(previousPoint.X, previousPoint.Y);
					var lineLength = a.distanceTo(b);

					extruder += lineLength * wallThickness * layerHeight / filamentSurfaceArea * flowRate;

					gcode.push([
						"G1", 
						"X" + point.X.toFixed(3) + " Y" + point.Y.toFixed(3) + " Z" + z, 
						"F" + speed, 
						"E" + extruder.toFixed(3)
					].join(" "));
				}

				previousPoint = point;
			}
		}

		return gcode;
	}

	var gcode = printer.getStartCode();

	var extruder = 0.0;
	var speed = firstLayerSlow ? (bottomSpeed*60).toFixed(3) : (normalSpeed*60).toFixed(3);
	var filamentSurfaceArea = Math.pow((filamentThickness/2), 2) * Math.PI;
	var flowRate = bottomFlowRate;

	for (var layer = 0; layer < data.length; layer ++) {
		var slice = data[layer];

		//turn on fan on layer 2
		if (layer === 2) {
			gcode.push("M106");
			speed = (normalSpeed*60).toFixed(3);
			flowRate = 1;
		}

		var z = ((layer + 1) * layerHeight).toFixed(3);

		for (var i = 0; i < slice.length; i ++) {
			var layerPart = slice[i];

			gcode = gcode.concat(sliceToGcode(layerPart.outerLayer));
			gcode = gcode.concat(sliceToGcode(layerPart.insets));
			gcode = gcode.concat(sliceToGcode(layerPart.fill));
		}
	}

	gcode = gcode.concat(printer.getEndCode());

	return gcode;
};
//only for debug purposes
D3D.Slicer.prototype.drawPaths = function (printer, min, max) {
	"use strict";

	var layerHeight = printer.config["printer.layerHeight"];
	var dimensionsZ = printer.config["printer.dimensions.z"];

	var slices = this.slice(dimensionsZ, layerHeight);

	var data = this.slicesToData(slices, printer);

	var canvas = document.createElement("canvas");
	canvas.width = 400;
	canvas.height = 400;
	var context = canvas.getContext("2d");

	for (var layer = min; layer < max; layer ++) {
		var slice = data[layer % data.length];

		for (var i = 0; i < slice.length; i ++) {
			var layerPart = slice[i];

			layerPart.insets.draw(context, "blue");
			layerPart.outerLayer.draw(context, "green");
			layerPart.fill.draw(context, "red");
		}
	}

	return canvas;
};
D3D.Slicer.prototype.getGcode = function (printer) {
	"use strict";

	var layerHeight = printer.config["printer.layerHeight"];
	var dimensionsZ = printer.config["printer.dimensions.z"];

	var start = new Date().getTime();
	var slices = this.slice(dimensionsZ, layerHeight);
	var end = new Date().getTime();
	console.log("Slicing: " + (end - start) + "ms");

	var start = new Date().getTime();
	var data = this.slicesToData(slices, printer);
	var end = new Date().getTime();
	console.log("Data: " + (end - start) + "ms");

	var start = new Date().getTime();
	var gcode = this.dataToGcode(data, printer);
	var end = new Date().getTime();
	console.log("Gcode: " + (end - start) + "ms");

	return gcode;
};