/**
 * Стиралка блоков
 */
var htmlEraser = function() {
	
	function copyObj(obj){
		var result = {};
		for (var a in obj) if (obj.hasOwnProperty(a)) {
			result[a] = obj[a];
		}
		
		return result;
	}
	
	/**
	 * Точка
	 * 
	 * @param {Number} x
	 * @param {Number} y
	 */
	function p(x, y) {
		return {
			x : x,
			y : y
		};
	}

	/**
	 * Прямоугольник
	 * 
	 * @param {Number} x
	 * @param {Number} y
	 * @param {Number} width
	 * @param {Number} height
	 */
	function rect(x, y, width, height) {
		return {
			x : x,
			y : y,
			width : width,
			height : height
		};
	}
	
	/**
	 * Ластик
	 * 
	 * @param {Element} elem Элемент, представляющий ластик на странице
	 * @param {p()[]} Форма ластика
	 * @param {jQuery|Element} Блоки, которые нужно стирать
	 */
	var Eraser = this.Eraser = function(elem, shape, blocks) {
		/**
		 * Форма ластика. Координаты формы постоянно меняются,
		 * когда ластик перемещается
		 * @type {p()[]}
		 */
		this.shape = shape;
		
		/**
		 * Базовые координаты формы, не меняются никогда
		 * @private
		 */
		this.base_shape = shape;

		this.elem = $(elem);
		
		/**
		 * Текущее состояние ластика
		 * @private
		 */
		this.state = Eraser.STATE_IDLE;
		
		this.bound_box = rect(0, 0, elem.width(), elem.height());
		
		var eraser_blocks = [];
		blocks = $(blocks);
		
		blocks.each(function(){
			eraser_blocks.push(new EraserBlock(this));
		});
		
		/** 
		 * Блоки, которые может стирать ластик на странице
		 * @type {EraserBlock[]} 
		 */
		this.eraser_blocks = eraser_blocks;
	}
	
	Eraser.STATE_IDLE = 1;
	Eraser.STATE_ERASE = 1;
	
	Eraser.prototype = {
		/**
		 * Возвращает состояние ластика
		 * @return {Number}
		 */
		getState: function(){
			return this.state;
		},
		
		/**
		 * Устанавливает состояние ластика
		 * @param {Number} value
		 */
		setState: function(value){
			this.state = value;
		},
		
		/**
		 * Перемещает ластик в указанные координаты
		 * @param {Number} x
		 * @param {Number} y
		 */
		move: function(x, y){
			this.elem.css({
				left: x, 
				top: y
			});
			
			this.bound_box.x = x;
			this.bound_box.y = y;
			
			// обновляю координаты формы
			var shape = [];
			$.each(this.base_shape, function(i, /* p() */ point){
				shape.push(p(point.x + x, point.y + y));
			});
			
			this.shape = shape;
			
			if (this.getState() == Eraser.STATE_ERASE) {
				
				var l = this.eraser_blocks.length - 1;
				if (l == -1){
					// блоков не осталось
					return;
				}
				do {
					/**
					 * @type {EraserBlock}
					 */
					var block = this.eraser_blocks[l];
					if (this.isOverlap(block)) {
						block.erase(this);
					}
					
					if (block.canDestroy()) {
						block.destroy();
						this.eraser_blocks.splice(l, 1);
					}
				} while(l--);
				
//				for (var i = 0; i < this.eraser_blocks.length; i++) {
//					this.eraser_blocks[i].erase(this);
//				}
			}
		},
		
		/**
		 * Проверяет, пересекается ли стиралка с блоком
		 * @param {EraserBlock} block
		 */
		isOverlap: function(block){
			if (EraserBlock.isBBoxIntersects(block.bound_box, this.bound_box)) {
				return true;
				
				// TODO удалить?
				var l = this.shape.length - 1;
				var _sh = block.bound_box;
				var _esh = this.shape;
				var p1 = _esh[l], p2 = _esh[0];
				if (
					EraserBlock.isLinesCross(_sh.x, _sh.y, _sh.x + _sh.width, _sh.y, p1.x, p1.y, p2.x, p2.y) ||
					EraserBlock.isLinesCross(_sh.x + _sh.width,  _sh.y, _sh.x + _sh.width, _sh.y + _sh.height, p1.x, p1.y, p2.x, p2.y) ||
					EraserBlock.isLinesCross(_sh.x, _sh.y + _sh.height, _sh.x + _sh.width, _sh.y + _sh.height, p1.x, p1.y, p2.x, p2.y) ||
					EraserBlock.isLinesCross(_sh.x, _sh.y, _sh.x, _sh.y + _sh.height, p1.x, p1.y, p2.x, p2.y)
				)
					return true;
				do {
					p1 = _esh[l], p2 = _esh[l - 1];
					if(
						EraserBlock.isLinesCross(_sh.x, _sh.y, _sh.x + _sh.width, _sh.y, p1.x, p1.y, p2.x, p2.y) ||
						EraserBlock.isLinesCross(_sh.x + _sh.width, _sh.y, _sh.x + _sh.width,  _sh.y + _sh.height, p1.x, p1.y, p2.x, p2.y) ||
						EraserBlock.isLinesCross(_sh.x, _sh.y + _sh.height, _sh.x + _sh.width, _sh.y + _sh.height, p1.x, p1.y, p2.x, p2.y) ||
						EraserBlock.isLinesCross(_sh.x, _sh.y, _sh.x, _sh.y + _sh.height, p1.x, p1.y, p2.x, p2.y)
					)
						return true;
				} while(--l)
			}
			return false;
		}
	}

	/**
	 * Стираемый элемент. Представляет из себя блок шириной и высотой с
	 * контейнер, в котором находится. В блок записываются абсолютные координаты
	 * контейнера; таким образом, блок всегда отражает свою позицию на странице,
	 * а не в контейнере. Хранит в себе стираемые ячейки <code>EraserCell</code>
	 * 
	 * @param {Element}
	 *            elem Элемент, в который нужно добавить стираемый блок
	 */
	function EraserBlock(elem) {
		elem = $(elem);

		// получаю абсолютные координаты элемента
		var offset = elem.offset();

		var block_size = {
			width : elem.width(),
			height : elem.height()
		};

		var ptr = $('<div class="erase"></div>').css(block_size);

		var wrap = $('<div class="erase-wrap"></div>');
		ptr.append(wrap);
		elem.append(ptr);

		/**
		 * Указатель на контейнер стираемого блока
		 * 
		 * @param {jQuery}
		 */
		this.ptr = ptr;

		/**
		 * Указатель на обертку стираемого блока 
		 * !!! Нужен ли этот блок?
		 * 
		 * @param {jQuery}
		 */
		this.wrap = wrap;

		/**
		 * Флаг, указывающий, была ли выполнена тесселяция блока. Тесселяция —
		 * это разбивка блока на более мелкие ячейки (<code>EraserCell</code>),
		 * которые, ради экономии ресурсов, инициализируются только тогда, когда
		 * это нужно.
		 */
		this.tesselated = false;

		/** Прямоугольник, описывающий габариты и размеры блока */
		this.bound_box = rect(offset.left, offset.top, block_size.width,
				block_size.height);
				
		EraserBlock.instances.push(this);

		/**
		 * Стриаемые ячейки
		 * 
		 * @type {EraserCell[]}
		 */
		this.cells = [];

		this.tesselate();
//		console.log(this);
	}
	
	/**
	 * Экземпляры всех созданных блоков
	 * @type {EraserBlock[]}
	 */
	EraserBlock.instances = [];
	
	/**
	 * Обновляет координаты всех созданных блоков. Метод выполняется 
	 * на <code>window.onresize</code>
	 */
	EraserBlock.updateCoordsAll = function(){
		for (var i = 0; i < EraserBlock.instances.length; i++) {
			EraserBlock.instances[i].updateCoords();
		}
	}

	/**
	 * Проверяет, являются ли два числа одного знака: оба плюсы или оба минусы
	 * @param {Number} a
	 * @param {Number} b
	 * @return {Boolean}
	 */
	EraserBlock.sameSigns = function(a, b) {
		return ((a ^ b) > 0);
	}

	/**
	 * Проверяет, пересекаются ли две линии
	 * 
	 * @return {p()}
	 */
	EraserBlock.isLinesCross = function(x1, y1, x2, y2, x3, y3, x4, y4) {
		var a1, a2, b1, b2, c1, c2;
		var r1, r2, r3, r4;

		a1 = y2 - y1;
		b1 = x1 - x2;
		c1 = x2 * y1 - x1 * y2;

		r3 = a1 * x3 + b1 * y3 + c1;
		r4 = a1 * x4 + b1 * y4 + c1;

		if (r3 && r4 && EraserBlock.sameSigns(r3, r4))
			return false;

		a2 = y4 - y3;
		b2 = x3 - x4;
		c2 = x4 * y3 - x3 * y4;

		r1 = a2 * x1 + b2 * y1 + c2;
		r2 = a2 * x2 + b2 * y2 + c2;

		if (r1 && r2 && EraserBlock.sameSigns(r1, r2))
			return false;

		var denom = a1 * b2 - a2 * b1;
		if (denom == 0)
			return false;

		var offset = denom < 0 ? -denom / 2 : denom / 2;

		var num = b1 * c2 - b2 * c1;
		var x = Math.floor((num < 0 ? num - offset : num + offset) / denom);

		num = a2 * c1 - a1 * c2;
		var y = Math.floor((num < 0 ? num - offset : num + offset) / denom);
		
//		console.log(x1, y1, x2, y2, x3, y3, x4, y4);

		return p(x, y);
	}

	/**
	 * Проверяет, находится ли точка <code>point</code> внутри полигона
	 * <code>poly</code>
	 * 
	 * @return {Boolean}
	 */
	EraserBlock.isInsidePoly = function(poly, point) {
		var counter = 0;
		var i, xinters, p2;
		var p1 = poly[0];
		var l = poly.length;

		for (i = 1; i <= l; i++) {
			p2 = poly[i % l];
			if ((point.y > Math.min(p1.y, p2.y))
					&& (point.y <= Math.max(p1.y, p2.y))
					&& (point.x <= Math.max(p1.x, p2.x)) && (p1.y != p2.y)) {
				xinters = (point.y - p1.y) * (p2.x - p1.x) / (p2.y - p1.y)
						+ p1.x;
				if (p1.x == p2.x || point.x <= xinters)
					counter++;
			}
			p1 = p2;
		}

		return !(counter % 2 == 0);
	}
	
	/**
	 * Проверяет, пересекаются ли два прямоугольника
	 * @param {rect()} bbox1
	 * @param {rect()} bbox2
	 * @return {Boolean}
	 */
	EraserBlock.isBBoxIntersects = function(bbox1, bbox2){
		return (
			Math.min(bbox1.x + bbox1.width, bbox2.x + bbox2.width) > Math.max(bbox1.x, bbox2.x) && 
			Math.max(bbox1.y, bbox2.y) < Math.min(bbox1.y + bbox1.height, bbox2.y + bbox2.height) 
		);
	}

	EraserBlock.prototype = {
		/**
		 * Проверяет, можно ли уничтожить стираемый блок. Это возможно только в
		 * том случае, если у блока не осталось стираемых ячеек.
		 * 
		 * @return {Boolean}
		 */
		canDestroy : function() {
			return (this.tesselated && !this.cells.length);
		},

		/**
		 * Уничтожает стираемый блок
		 */
		destroy : function() {
			this.ptr.remove();
			for (var i = 0; i < EraserBlock.instances.length; i++) {
				if (EraserBlock.instances[i] == this) {
					/** @type {EraserBlock} */
					var block = EraserBlock.instances[i];
					
					// уничтожаем все ячейки
					for (var i = 0; i < block.cells.length; i++) {
						block.cells[i].destroy();
					}
					
					delete block.cells;
					
					EraserBlock.instances.splice(i, 1);
					break;
				}
			}
			
			delete this.ptr;
		},

		/**
		 * Обновляет абсолютные координаты блока. Этот метод нужно выполнять в
		 * тот момент, когда меняется размер окна браузера или размер шрифта
		 */
		updateCoords : function() {
			var offset = this.ptr.parent().offset();
			$.each(this.cells, function(i, /* EraserCell */cell) {
				cell.updateCoords(offset.left, offset.top);
			});

			this.bound_box.x = offset.left;
			this.bound_box.y = offset.left;
		},

		/**
		 * Тесселяция стираемого блока: разбивка его на более мелкие ячейки.
		 */
		tesselate : function() {
			this.сells = [];
			var r = rect(0, 0, this.bound_box.width, 1);
			
			for (var i = 0; i < this.bound_box.height; i++, r.y = i) {
				this.addCell(r, this.bound_box);
			}

			this.tesselated = true;
		},

		/**
		 * Добавляет стираемую ячейку в блок.
		 * 
		 * @param {rect()|EraserCell} cell Ячейка или ее габариты
		 */
		addCell : function(cell_rect) {
//			if (!(cell instanceof EraserCell))
//			console.log(cell_rect);
			var cell = new EraserCell(cell_rect, this.bound_box);
				
			this.cells.push(cell);
			this.wrap.append(cell.ptr);
		},
		
		/**
		 * Клонирует переданную ячейку
		 * @param {EraserCell} cell
		 */
		cloneCell: function(cell, pos, width){
			this.addCell(cell.clone(pos, width));
		},

		/**
		 * Стирание блока
		 * 
		 * @param {Eraser}
		 *            eraser Ластик, который стирает блок
		 */
		erase : function(eraser) {
			/**
			 * Полигон, описывающий форму ластика
			 * 
			 * @type {p()[]}
			 */
			var shape = eraser.shape;

			var bbox_x = this.bound_box.x;
			var bbox_y = this.bound_box.y;
			var bbox_width = this.bound_box.width;
			var bbox_height = this.bound_box.height;

			/**
			 * Пересечения ячейки с ластиком
			 * 
			 * @type {Array}
			 */
			var intersections;

			/**
			 * Максимальное количество пересечений, которое может быть у ластика
			 * с ячейкой. Используется для оптимизации расчетов пересечений.
			 * Чем больше значение, тем больше тестов будет совершать скрипт
			 */
			var max_intersections = 3;

			/**
			 * Флаг, указывающий, находится ли начало ячейки внутри ластика
			 * 
			 * @type {Boolean}
			 */
			var is_inside;

			/**
			 * Текущая ячейка
			 * 
			 * @param {EraserCell}
			 */
			var cell;

			/**
			 * Форма текущей ячейки
			 * 
			 * @type {rect()}
			 */
			var csh;

			/**
			 * Первая точка ластика
			 * 
			 * @type {p()}
			 */
			var p1;

			/**
			 * Вторая точка ластика
			 * 
			 * @type {p()}
			 */
			var p2;

			/**
			 * Точка пересечения двух линий
			 * 
			 * @type {p()}
			 */
			var lx;

			/** Индекс предыдущей точки формы ластика */
			var prev = shape.length - 1;

			var min_x, max_x;
			
			var _cells = this.cells;
			/**
			 * Удаляет ячейку из массива
			 * @param {Number} ix Индекс ячейки
			 */
			function removeCell(ix){
				_cells.splice(ix, 1);
			}
			
			/**
			 * Алгоритм сортировки точек в массиве
			 */
			function sortPoints(a, b){
				return a.x - b.x;
			}

			/*
			 * для каждой ячейки ErserCell проверяю, сколько раз она пересекает
			 * ластик и где находится начало ластика: внутри или вне ластика.
			 * Этот цикл очень ресурсоемкий, поэтому в плане производительности
			 * стараюсь выжать все соки из него
			 * 
			 * do...while работает немного быстрее, чем for
			 */
			var i = this.cells.length - 1;
			do {
				cell = this.cells[i];
				csh = cell.rect;
//				console.log(csh);
//				console.log(i, csh, cell);
				intersections = [];
				for (var j = 0; (j < shape.length) && (intersections.length < max_intersections); j++) {
					p1 = shape[j];
					p2 = shape[prev];
					if ((lx = EraserBlock.isLinesCross(csh.x, csh.y, csh.x + csh.width, csh.y + csh.height, p1.x, p1.y, p2.x, p2.y))) {
						intersections.push(lx);
					}
					prev = j;
				}

				is_inside = EraserBlock.isInsidePoly(shape, csh);
				
//				console.log(intersections);
				
				switch (intersections.length) {
					case 0 : // отрезок не пересекает полигон
						if (is_inside) { // ...но находится в нем
							cell.destroy();
							removeCell(i);
							continue;
						}
						break;
					case 1 :
						if (is_inside) // отрезок нужно укоротить и подвинуть вправо
							cell.erase(intersections[0].x - bbox_x, csh.width - intersections[0].x + csh.x);
							
						// если intersections[0].x == csh.x, то отрезки соприкасаются и делать ничего не надо
						else if(intersections[0].x > csh.x) {
							// отрезок нужно только укоротить
							cell.erase(csh.x - bbox_x, intersections[0].x - csh.x);
						}
						break;
					case 2 :
						/*
						 * FIXME неправильная обработка пересечений на границе
						 * intersections: [Object x=248 y=213, Object x=248 y=213] 
						 * is_inside: true
						 * в этом случае блок полностью удаляется
						 */
						min_x = Math.min(intersections[0].x, intersections[1].x);
						max_x = Math.max(intersections[0].x, intersections[1].x);
						
						if (min_x == max_x)
							// пересечения на границе
							break;

						if (!is_inside) { // делим отрезок на две части
							//FIXME нужно клонировать существующий блок (а не создавать новый), чтобы наследовались css-свойства элемента
//							this.cloneCell(cell, max_x - bbox_x, csh.width - max_x + csh.x);
							this.addCell(rect(max_x - bbox_x, csh.y - bbox_y, csh.width - max_x + csh.x, 1));
							cell.erase(csh.x - bbox_x, min_x - csh.x);
						} else { 
							// такая ситуация возможна, если отрезок
							// пересекает два вогнутых ребра
							cell.erase(min_x - bbox_x, max_x - min_x);
						}
						break;
					case 3 :
//						intersections.sort(sortPoints);
						
						if (is_inside) {
							this.addCell(rect(intersections[2].x - bbox_x, csh.y - bbox_y, csh.width - intersections[2].x + csh.x, 1));
							cell.erase(intersections[0].x - bbox_x, intersections[1].x - intersections[0].x);
						} else {
							this.addCell(rect(intersections[1].x - bbox_x, csh.y - bbox_y, intersections[2].x - intersections[1].x, 1));
							cell.erase(csh.x - bbox_x, intersections[0].x - csh.x);
						}
						break;
					case 4 :
						intersections.sort(sortPoints);
						this.addCell(rect(intersections[1].x - bbox_x, csh.y - bbox_y, intersections[2].x - intersections[1].x, 1));
						this.addCell(rect(intersections[3].x - bbox_x, csh.y - bbox_y, csh.w - intersections[3].x + csh.x, 1));
						cell.erase(csh.x - bbox_x, intersections[0].x - csh.x);
						break;
				}
				if (cell.canDestroy()) {
//					console.log('destroy', csh, intersections, cell);
//					debugger;
					cell.destroy();
					removeCell(i)
				}
			} while (i--);
		}

	};
	
	/**
	 * Стираемая ячейка. Является частью <code>EraserBlock</code>
	 * @param {rect()} cell_rect Габариты ячейки
	 * @param {rect()} parent_rect Габариты контейнера ячеек
	 */
	function EraserCell(cell_rect, parent_rect, elem){
		this.ptr = $('<div class="eraser-cell"></div>').css({
			width: cell_rect.width,
			height: cell_rect.height,
			left: cell_rect.x,
			top: cell_rect.y,
			backgroundPosition: (-cell_rect.x) + 'px ' + (-cell_rect.y) + 'px'
		});
		
		this.rect = rect(cell_rect.x + parent_rect.x, cell_rect.y + parent_rect.y, cell_rect.width, cell_rect.height);
		this.parent_rect = parent_rect;
	}
	
	EraserCell.prototype = {
		/**
		 * Уничтожает ячейку
		 */
		destroy: function(){
//			console.log('destroying cell');
			this.ptr.remove();
		},
		
		/**
		 * Стирает ячейку
		 * @param {Number} pos Новая позиция ячейки
		 * @param {Number} width Новая ширина ячейки
		 */
		erase: function(pos, width) {
			width = Math.max(0, width);
			
			this.ptr.css({
				left: pos,
				width: width,
				backgroundPosition: (-pos) + 'px -' + (this.rect.y - this.parent_rect.y) + 'px'
			});
			
			this.rect.x = pos + this.parent_rect.x;
			this.rect.width = width;
		},
		
		/**
		 * Проверяет, можно ли уничтожать ячейку
		 * @return {Boolean}
		 */
		canDestroy: function(){
			return (this.rect.width <= 1);
		},
		
		/**
		 * Обновляет глобальные координаты ячейки. Метод вызывается 
		 * инкрементально при вызове <code>EraserBlock.prototype.updateCoords</code>
		 * @param {p()} point Новые координаты
		 */
		updateCoords: function(point){
			this.rect.x = this.rect.x - this.parent_rect.x + point.x;
			this.rect.y = this.rect.y - this.parent_rect.y + point.y;
		},
		
		/**
		 * Клонирует текущую ячейку
		 * @return {EraserCell}
		 */
		clone: function(pos, width){
			var elem = this.ptr.clone();
			return new EraserCell(rect(pos, this.rect.y - this.parent_rect.y, width, this.rect.height), this.parent_rect, elem);
		}
	};
	
	// на ресайз окна нужно обновлять координаты блоков
	$(window).resize(function(){
		EraserBlock.updateCoordsAll();
	});
	
	return {
		/**
		 * Создание ластика
		 * @param {Element|jQuery} elem Элемент, представляющий ластик на странице
		 * @param {p()[]} shape Многоугольник, описывающий форму ластика
		 * @param {Element|jQuery} blocks Блоки, которые должен стирать ластик
		 * @return {Eraser}
		 */
		create: function(elem, shape, blocks){
			return new Eraser(elem, shape, blocks);
		}
	};

}();

