define([
	'js/misc/pathfinder'
], function (
	pathfinder
) {
	var sqrt = Math.sqrt.bind(Math);
	var ceil = Math.ceil.bind(Math);
	var random = Math.random.bind(Math);

	return {
		graph: null,

		collisionMap: null,
		cells: [],
		width: 0,
		height: 0,

		init: function (collisionMap) {
			this.collisionMap = collisionMap;

			this.width = collisionMap.length;
			this.height = collisionMap[0].length;

			this.cells = _.get2dArray(this.width, this.height, 'array');

			this.graph = new pathfinder.Graph(collisionMap, {
				diagonal: true
			});
		},

		addRegion: function (obj) {
			var lowX = obj.x;
			var lowY = obj.y;
			var highX = lowX + obj.width;
			var highY = lowY + obj.height;
			var cells = this.cells;

			for (var i = lowX; i <= highX; i++) {
				var row = cells[i];
				for (var j = lowY; j <= highY; j++) {
					row[j].push(obj);
				}
			}
		},

		addObject: function (obj, x, y, fromX, fromY) {
			var row = this.cells[x];

			if (!row)
				return;

			var cell = row[y];

			if (!cell)
				return;

			var cLen = cell.length;
			for (var i = 0; i < cLen; i++) {
				var c = cell[i];

				//If we have fromX and fromY, check if the target cell doesn't contain the same obj (like a notice area)
				if ((c.width) && (fromX)) {
					if ((fromX < c.x) || (fromY < c.y) || (fromX >= c.x + c.width) || (fromY >= c.y + c.height)) {
						c.collisionEnter(obj);
						obj.collisionEnter(c);
					}
				} else {
					c.collisionEnter(obj);
					obj.collisionEnter(c);
				}
			}

			cell.push(obj);
			return true;
		},
		removeObject: function (obj, x, y, toX, toY) {
			var row = this.cells[x];

			if (!row)
				return;

			var cell = row[y];

			if (!cell)
				return;

			var oId = obj.id;
			var cLen = cell.length;
			for (var i = 0; i < cLen; i++) {
				var c = cell[i];

				if (c.id != oId) {
					//If we have toX and toY, check if the target cell doesn't contain the same obj (like a notice area)
					if ((c.width) && (toX)) {
						if ((toX < c.x) || (toY < c.y) || (toX >= c.x + c.width) || (toY >= c.y + c.height)) {
							c.collisionExit(obj);
							obj.collisionExit(c);
						}
					} else {
						c.collisionExit(obj);
						obj.collisionExit(c);
					}
				} else {
					cell.splice(i, 1);
					i--;
					cLen--;
				}
			}
		},

		isValid: function (x, y) {
			var row = this.cells[x];

			if ((!row) || (row.length <= y) || (!this.graph.grid[x][y]))
				return false;
			else
				return true;
		},

		getCell: function (x, y) {
			var row = this.cells[x];

			if (!row)
				return [];

			var cell = row[y];

			if (!cell)
				return [];

			return cell;
		},
		getArea: function (x1, y1, x2, y2, filter) {
			var width = this.width;
			var height = this.height;

			x1 = ~~x1;
			y1 = ~~y1;

			x2 = ~~x2;
			y2 = ~~y2;

			if (x1 < 0)
				x1 = 0;
			else if (x2 >= width)
				x2 = width - 1;
			if (y1 < 0)
				y1 = 0;
			else if (y2 >= height)
				y2 = height - 1;

			var cells = this.cells;
			var grid = this.graph.grid;

			var result = [];
			for (var i = x1; i <= x2; i++) {
				var row = cells[i];
				var gridRow = grid[i];
				for (var j = y1; j <= y2; j++) {
					if (!gridRow[j])
						continue;

					var cell = row[j];
					var cLen = cell.length;
					for (var k = 0; k < cLen; k++) {
						var c = cell[k];

						if (filter) {
							if (filter(c))
								result.push(c);
						} else
							result.push(c);
					}
				}
			}

			return result;
		},

		getOpenCellInArea: function (x1, y1, x2, y2) {
			var width = this.width;
			var height = this.height;

			x1 = ~~x1;
			y1 = ~~y1;

			x2 = ~~x2;
			y2 = ~~y2;

			if (x1 < 0)
				x1 = 0;
			else if (x2 >= width)
				x2 = width - 1;
			if (y1 < 0)
				y1 = 0;
			else if (y2 >= height)
				y2 = height - 1;

			var cells = this.cells;
			var grid = this.graph.grid;

			var result = [];
			for (var i = x1; i <= x2; i++) {
				var row = cells[i];
				var gridRow = grid[i];
				for (var j = y1; j <= y2; j++) {
					if (!gridRow[j])
						continue;

					var cell = row[j];
					if (cell.length == 0) {
						return {
							x: i,
							y: j
						};
					}
				}
			}

			return result;
		},

		getPath: function (from, to) {
			var graph = this.graph;
			var grid = graph.grid;

			if (!to) {
				to = {
					x: ~~(random() * grid.length),
					y: ~~(random() * grid[0].length)
				};
			}

			var fromX = ~~from.x;
			var fromY = ~~from.y;

			if ((!grid[fromX]) || (grid[fromX].length <= fromY) || (fromX < 0) || (fromY < 0))
				return [];

			var toX = ~~to.x;
			var toY = ~~to.y;

			if ((!grid[toX]) || (grid[toX].length <= toY) || (toX < 0) || (toY < 0))
				return [];

			var path = pathfinder.astar.search(graph, {
				x: fromX,
				y: fromY
			}, {
				x: toX,
				y: toY
			}, {
				closest: true
			});

			return path;
		},
		isTileBlocking: function (x, y, mob, obj) {
			if ((x < 0) || (y < 0) || (x >= this.width) | (y >= this.height))
				return true;

			x = ~~x;
			y = ~~y;

			var node = this.graph.grid[x][y];

			return ((!node) || (node.weight == 0));
		},
		isCellOpen: function (x, y) {
			if ((x < 0) || (y < 0) || (x >= this.width) | (y >= this.height))
				return true;

			return (this.cells[x][y].length == 0);
		},
		hasLos: function (fromX, fromY, toX, toY) {
			if ((fromX < 0) || (fromY < 0) || (fromX >= this.width) | (fromY >= this.height) || (toX < 0) || (toY < 0) || (toX >= this.width) | (toY >= this.height))
				return false;

			var graphGrid = this.graph.grid;

			if ((!graphGrid[fromX][fromY]) || (!graphGrid[toX][toY]))
				return false;

			var dx = toX - fromX;
			var dy = toY - fromY;

			var distance = sqrt((dx * dx) + (dy * dy));

			dx /= distance;
			dy /= distance;

			fromX += 0.5;
			fromY += 0.5;

			distance = ceil(distance);

			var x = 0;
			var y = 0;

			for (var i = 0; i < distance; i++) {
				fromX += dx;
				fromY += dy;

				x = ~~fromX;
				y = ~~fromY;

				if (!graphGrid[x][y])
					return false;
				else if ((x == toX) && (y == toY))
					return true;
			}

			return true;
		},

		getClosestPos: function (fromX, fromY, toX, toY, target) {
			var tried = {};

			var hasLos = this.hasLos.bind(this, toX, toY);

			var width = this.width;
			var height = this.height;

			var collisionMap = this.collisionMap;
			var cells = this.cells;

			var reverseX = (fromX > toX);
			var reverseY = (fromY > toY);

			for (var c = 1; c <= 10; c++) {
				var x1 = toX - c;
				var y1 = toY - c;
				var x2 = toX + c;
				var y2 = toY + c;

				var lowX, lowY, highX, highY, incX, incY;

				if (reverseX) {
					incX = -1;
					lowX = x2;
					highX = x1 - 1;
				} else {
					incX = 1;
					lowX = x1;
					highX = x2 + 1;
				}

				if (reverseY) {
					incY = -1;
					lowY = y2;
					highY = y1 - 1;
				} else {
					incY = 1;
					lowY = y1;
					highY = y2 + 1;
				}

				for (var i = lowX; i != highX; i += incX) {
					if ((i < 0) || (i >= width))
						continue;

					var row = collisionMap[i];
					var cellRow = cells[i];

					var t = tried[i];
					if (!t) {
						t = tried[i] = {};
					}

					for (var j = lowY; j != highY; j += incY) {
						if (t[j])
							continue;

						t[j] = 1;

						if (
							((i == toX) && (j == toY)) ||
							((j < 0) || (j >= height)) ||
							(row[j])
						)
							continue;

						var cell = cellRow[j];
						var cLen = cell.length;
						var blocking = false;
						for (var k = 0; k < cLen; k++) {
							var aggro = cell[k].aggro;
							if (aggro) {
								blocking = aggro.list.some(a => a.obj == target);
								if (blocking)
									break;
							}
						}
						if (blocking)
							continue;
						else if (!hasLos(i, j))
							continue;

						return {
							x: i,
							y: j
						};
					}
				}
			}
		},

		mobsCollide: function (x, y, obj) {
			if ((x < 0) || (y < 0) || (x >= this.width) | (y >= this.height))
				return true;

			var cell = this.cells[x][y];
			var cLen = cell.length;

			if (cLen == 1)
				return false;

			var found = false;
			for (var i = 0; i < cLen; i++) {
				var c = cell[i];
				if (c.aggro) {
					if ((!found) && (c == obj))
						found = true;
					else
						return true;
				}
			}

			return false;
		},

		setCollision: function (x, y, collides) {
			var node = this.graph.grid[x][y];
			if (!node) {
				var grid = this.graph.grid;
				node = grid[x][y] = new pathfinder.gridNode(x, y, collides ? 0 : 1);
			}

			node.weight = collides ? 0 : 1;
		}
	};
});
