/**
 * for compute detail see cf http://www.gamedev.net/reference/articles/article1800.asp
 */
var MapHex = Class.create();
MapHex.prototype = {
	collapsibleBorderSize : null,
	htmlMapContainer : null,
	gridBoxContent : null,
	htmlMapWidth : null,
	htmlMapHeight : null,
	htmlMapPos : null,
	tileDim : null,
	additionalBoxHeight : null,
	mapTrackOver : false,
	mapMovable : false,
	dropShadowFill : null,
	mapInactiveColumn : null,
	mouseClickTimeout : null,
	mouseHoldDownTimeout : null,
	mouseDbClickDelay : null,
	mouseHoldDownDelay : null,
	mapCoord : {
		x : 0,
		y : 0
	},
	mapCoordMax : {
		x : 0,
		y : 0
	},
	mapPosMax : {
		x : 0,
		y : 0
	},
	mouseDownCoord : {
		x : null,
		y : null
	},
	mouseOverPos : null,
	mapMoving : false,
	mouseEventListener : null,
	mouseEventBinded : null,
	mouseHoldDownBinded : null,
	preventNextMouseClick : false,
	preventNextMouseUp : false,

	/**
	 * @constructor
	 * @param {Object}
	 *          options options du constructeur :
	 *          <ul>
	 *          <li>htmlMapContainer : obligatoire, objet DOM dans lequel la map sera créée</li>
	 *          <li>gridBoxContent : url de l'image a mettre en fond de chaque case</li>
	 *          <li>tileA : obligatoire, largeur d'un hexa en pixel</li>
	 *          <li>tileS : optionnel, taille d'un coté vertical d'un hexa en pixel</li>
	 *          <li>tileB : optionnel, hauteur d'un hexa en pixel</li>
	 *          <li>additionalBoxHeight : optionnel (0 par défaut), hauteur supplémentaire pour les box des
	 *          cases en pixel</li>
	 *          <li>collapseBorder : optionnel (true par défaut), indique si les bordures doivent être
	 *          supperposée</li>
	 *          <li>mapInactiveColumn : optionnel (0 par défaut), 1 désactive la premiere colonne pour les
	 *          lignes paires, 2 désactive la derniere colonne pour les lignes impaires</li>
	 *          <li>mouseEventListener : optionnel (void par défaut), fonction a appeler lorsqu'un événement
	 *          lié à la souris se produit sur un hexagone. Cette fonction recevra 2 param :
	 *          <ul>
	 *          <li>String, type d'événement correspondant à l'une des valeurs suivantes : "click",
	 *          "contextmenu", "over"</li>
	 *          <li>Object {x:posX, y:poxY}, contient les coordonnées de l'hexagone source du l'event</li>
	 *          <li>Object {x:pixelX, y:pixelY}, contient les coordonnées de la position de la souris</li>
	 *          </ul>
	 *          </li>
	 *          </ul>
	 */
	initialize : function(options) {
		this.collapsibleBorderSize = options.collapsibleBorderSize || 0;
		this.htmlMapContainer = $($(options.htmlContainer).appendChild(document.createElement("DIV")));
		this.gridBoxContent = options.gridBoxContent;
		this.htmlMapWidth = $(options.htmlContainer).getWidth();
		this.htmlMapHeight = $(options.htmlContainer).getHeight();
		this.viewportChange();
		this.mouseEventListener = options.mouseEventListener || null;
		this.additionalBoxHeight = options.additionalBoxHeight || 0;
		this.mapInactiveColumn = options.mapInactiveColumn || 0;
		this.mapPosMax = options.mapPosMax;
		this.dropShadowFill = options.dropShadowFill;

		this.tileDim = {};
		this.tileDim.a = options.tileA;
		this.tileDim.r = options.tileA / 2;
		this.tileDim.s = options.tileS || Math.round(this.tileDim.r / Math.cos(30 * Math.PI / 180));
		this.tileDim.b = options.tileB
				|| (this.tileDim.s + 2 * Math.round(this.tileDim.r * Math.tan(30 * Math.PI / 180)));
		this.tileDim.h = (this.tileDim.b - this.tileDim.s) / 2;

		this.htmlMapContainer.style.MozUserSelect = "none";
		this.htmlMapContainer.unselectable = "on";

		this.htmlMapContainer.style.position = "absolute";
		this.moveMap(0, 0);

		this.mouseHoldDownDelay = 300;
		this.mouseHoldDownBinded = this.mouseHoldDown.bind(this);
		this.mouseEventBinded = this.mouseEvent.bindAsEventListener(this);
		this.htmlMapContainer.observe('mousedown', this.mouseEventBinded);
		this.htmlMapContainer.observe('mouseup', this.mouseEventBinded);
		this.htmlMapContainer.observe('mousemove', this.mouseEventBinded);
		this.htmlMapContainer.observe('click', this.mouseEventBinded);
		this.htmlMapContainer.observe('dblclick', this.mouseEventBinded);
		this.htmlMapContainer.oncontextmenu = function() {
			return false;
		};

		if (Prototype.Browser.Touch) {
			this.htmlMapContainer.observe('touchstart', this.mouseEventBinded);
			// this.htmlMapContainer.observe('touchmove', this.mouseEventBinded);
			this.htmlMapContainer.observe('touchend', this.mouseEventBinded);
		}

		if (this.dropShadowFill) {
			var r = Raphael(this.htmlMapContainer, this.htmlMapWidth+20, this.htmlMapHeight+20);
			var startPosX = 0;
			var endPosX = this.mapPosMax.x;
			var endPosY = this.mapPosMax.y;
			var points = [];
			$R(startPosX, endPosX - (1 == this.mapInactiveColumn ? 1 : 0)).each(function(x) {
				var pixelX = 1
						+ Math.floor(this.tileDim.r * (2 * x + ((1 == this.mapInactiveColumn) ? 1 : 0))
								- this.collapsibleBorderSize * x);
				var pixelY = this.tileDim.h;
				points.push(pixelX + ',' + pixelY);
				pixelX += this.tileDim.r;
				pixelY = 0;
				points.push(pixelX + ',' + pixelY);
			}, this);
			$R(0, endPosY).each(function(y) {
				var pixelX = 1
						+ Math
								.floor(this.tileDim.r
										* ((2 * (endPosX + 1 - ((y & 1) && (2 == this.mapInactiveColumn) ? 1 : 0))) + (y & 1) - ((1 == this.mapInactiveColumn)
												? 1
												: 0)) - this.collapsibleBorderSize
										* ((endPosX + 1) - (1 == this.mapInactiveColumn && (y & 1) ? 0 : 1)));
				var pixelY = y * this.tileDim.b - this.collapsibleBorderSize * y + ((1 - y) * this.tileDim.h);
				points.push(pixelX + ',' + pixelY);
				pixelY += this.tileDim.s;
				points.push(pixelX + ',' + pixelY);
			}, this);
			$R(startPosX + (!(endPosY & 1) && 1 == this.mapInactiveColumn ? 1 : 0),
					endPosX - ((endPosY & 1) && 2 == this.mapInactiveColumn ? 1 : 0)).collect(function(v) {
						return v;
					}).reverse().each(function(x) {
				var pixelX = 1
						+ Math.floor(this.tileDim.r * (1 + 2 * x + (endPosY & 1) - ((1 == this.mapInactiveColumn) ? 1 : 0))
								- this.collapsibleBorderSize * x);
				var pixelY = ((endPosY + 1) * (this.tileDim.b)) - ((endPosY) * (this.tileDim.h))
						- ((endPosY) * this.collapsibleBorderSize);
				points.push(pixelX + ',' + pixelY);
				pixelX -= this.tileDim.r;
				pixelY -= this.tileDim.h;
				points.push(pixelX + ',' + pixelY);
			}, this);
			$R(0, endPosY).collect(function(v) {
						return v;
					}).reverse().each(function(y) {
				var pixelX = 1 + this.tileDim.r * ((y & 1) ^ (1 == this.mapInactiveColumn) ? 1 : 0);
				var pixelY = ((y + 1) * (this.tileDim.b)) - ((y + 1) * (this.tileDim.h))
						- ((y) * this.collapsibleBorderSize);
				points.push(pixelX + ',' + pixelY);
				pixelY -= this.tileDim.s;
				points.push(pixelX + ',' + pixelY);
			}, this);

			var ctx = r.path('M' + points.join('L'));
			ctx.attr({
						stroke : 'none',
						fill : this.dropShadowFill
					});
			ctx.blur(10);
		}
	},

	viewportChange : function() {
		this.htmlMapPos = $(this.htmlMapContainer.parentNode).cumulativeOffset();
	},

	/**
	 * @private
	 */
	mouseEvent : function(e) {
		var cursCoord = {
			x : e.pointerX(),
			y : e.pointerY()
		};

		switch (e.type.toLowerCase()) {
			case 'click' :
				window.clearTimeout(this.mouseClickTimeout);
				if (this.preventNextMouseClick) {
					this.preventNextMouseClick = false;
				} else {
					if (this.mouseDbClickDelay > 0) {
						this.mouseClickTimeout = this.mouseClick.bind(this, cursCoord, e).delay(this.mouseDbClickDelay
								/ 1000);
					} else {
						this.mouseClick(cursCoord, e);
					}
				}
				break;

			case 'dblclick' :
				window.clearTimeout(this.mouseClickTimeout);
				this.throwMouseEvent('dblclick', cursCoord, null, e);
				break;

			case 'touchstart' :
			case 'mousedown' :
				if (this.mouseHoldDownDelay) {
					this.mouseHoldDownTimeout = this.mouseHoldDownBinded.delay(this.mouseHoldDownDelay / 1000,
							cursCoord, e);
				}
				if (this.mapMovable && !this.mapMoving) {
					this.mouseDownCoord.x = cursCoord.x;
					this.mouseDownCoord.y = cursCoord.y;
				}
				break;

			case 'touchend' :
			case 'mouseup' :
				if (this.preventNextMouseUp) {
					this.preventNextMouseUp = false;
				} else {
					window.clearTimeout(this.mouseHoldDownTimeout);
					if (this.mapMoving) {
						// it's a move
						Event.stopObserving(document.body, 'mouseup', this.mouseEventBinded);
						Event.stopObserving(document.body, 'mousemove', this.mouseEventBinded);
						this.htmlMapContainer.style.cursor = "";
						this.mapMoving = false;
						this.checkMouseOver(cursCoord, e);
					} else {
						// it's a click, catch only right click (oncontextmenu doesn't work on opera)
						if (!e.isLeftClick()) {
							this.throwMouseEvent('contextmenu', cursCoord, null, e);
						}
					}
					this.mouseDownCoord.x = null;
					this.mouseDownCoord.y = null;
				}
				break;

			case 'touchmove' :
			case 'mousemove' :
				window.clearTimeout(this.mouseHoldDownTimeout);
				e.stop();
				if (this.mapMovable && this.mouseDownCoord.x != null) {
					// it's a move
					if (!this.mapMoving) {
						this.mapMoving = true;
						this.htmlMapContainer.style.cursor = "move";
						Event.observe(document.body, 'mouseup', this.mouseEventBinded);
						Event.observe(document.body, 'mousemove', this.mouseEventBinded);
					}
					var moveX = this.mouseDownCoord.x - cursCoord.x;
					var moveY = this.mouseDownCoord.y - cursCoord.y;
					this.moveMap(this.mapCoord.x + moveX, this.mapCoord.y + moveY);
					this.mouseDownCoord.x = cursCoord.x;
					this.mouseDownCoord.y = cursCoord.y;
				} else {
					// it's a mouseover
					if (this.mapTrackOver) {
						this.checkMouseOver(cursCoord, e);
					}
				}
				break;
		}
	},

	/**
	 * @private
	 */
	mouseClick : function(cursCoord, e) {
		var isLeftClick = Prototype.Browser.IE || e.isLeftClick();
		this.throwMouseEvent((isLeftClick ? 'click' : 'contextmenu'), cursCoord, null, e);
	},

	/**
	 * @private
	 */
	mouseHoldDown : function(cursCoord, e) {
		this.preventNextMouseClick = true;
		this.preventNextMouseUp = true;
		this.throwMouseEvent('holddown', cursCoord, null, e);
	},

	/**
	 * @private
	 */
	checkMouseOver : function(cursCoord, ev) {
		var mouseOverPos = this.getPosFromCoord(cursCoord);
		var newPosAsString = (mouseOverPos == null ? "" : mouseOverPos.x + '-' + mouseOverPos.y);
		var oldPosAsString = (this.mouseOverPos == null ? "" : this.mouseOverPos.x + '-' + this.mouseOverPos.y);
		if (newPosAsString != oldPosAsString) {
			var oldPos = this.mouseOverPos;
			this.mouseOverPos = mouseOverPos;
			this.throwMouseEvent('over', cursCoord, mouseOverPos, ev, oldPos);
		}
	},

	/**
	 * @private
	 */
	throwMouseEvent : function(eventType, cursCoord, cursPos, ev) {
		if (this.mouseEventListener != null) {
			if (undefined === cursPos || null == cursPos) {
				var cursPos = this.getPosFromCoord(cursCoord);
			}
			this.mouseEventListener(eventType, cursPos, cursCoord, ev, arguments[4]);
		}
	},

	/**
	 * @private
	 */
	getPosFromCoord : function(coord) {
		var pixelX = coord.x + this.mapCoord.x - this.htmlMapPos.left
				+ (1 == this.mapInactiveColumn ? this.tileDim.r : 0);
		var pixelY = coord.y + this.mapCoord.y - this.htmlMapPos.top - this.additionalBoxHeight;

		var pos = {
			x : null,
			y : null
		};

		var sectionY = Math.floor(pixelY / (this.tileDim.h + this.tileDim.s - this.collapsibleBorderSize));
		var sectionX = Math.floor(pixelX / (this.tileDim.a - this.collapsibleBorderSize));
		var sectionPixelX = Math.round(pixelX - sectionX * (this.tileDim.a - this.collapsibleBorderSize));
		var sectionPixelY = Math.round(pixelY - sectionY
				* (this.tileDim.h + this.tileDim.s - this.collapsibleBorderSize)) - (1 == this.mapInactiveColumn ? 0 : 1);

		pos.x = sectionX;
		pos.y = sectionY;
		if ((sectionY & 1) == 0) {

			if (sectionPixelY < this.tileDim.h - sectionPixelX * (this.tileDim.h / this.tileDim.r)) {
				pos.x--;
				pos.y--;
			} else if (sectionPixelY < -1 * this.tileDim.h + sectionPixelX * (this.tileDim.h / this.tileDim.r)) {
				pos.y--;
			}

		} else {

			if (sectionPixelX >= this.tileDim.r) {
				if (sectionPixelY < 2 * this.tileDim.h - sectionPixelX * (this.tileDim.h / this.tileDim.r)) {
					// pos.x--;
					pos.y--;
				}
			} else {
				if (sectionPixelY < sectionPixelX * (this.tileDim.h / this.tileDim.r)) {
					pos.y--;
				} else {
					pos.x--;
				}
			}

		}

		return this.isInMap(pos) ? pos : null;
	},

	/**
	 * @private
	 */
	createBox : function(pos) {
		if (!this.isInMap(pos)) {
			throw "out of map " + pos.x + "," + pos.y;
		}

		var pixelX = Math.floor(this.tileDim.r
				* ((2 * pos.x) + (pos.y & 1) - (1 == this.mapInactiveColumn ? 1 : 0)) - this.collapsibleBorderSize
				* (pos.x - (1 == this.mapInactiveColumn && (pos.y & 1) ? 0 : 1))) - (1 != this.mapInactiveColumn ? 1 : 0);
		var pixelY = pos.y * (this.tileDim.h + this.tileDim.s) - this.collapsibleBorderSize * pos.y;

		var htmlBox = document.createElement("DIV");
		htmlBox.id = this.getBoxId(pos);
		htmlBox.style.position = 'absolute';
		htmlBox.style.height = (this.tileDim.b + this.additionalBoxHeight) + 'px';
		htmlBox.style.width = this.tileDim.a + 'px';
		htmlBox.style.top = pixelY + 'px';
		htmlBox.style.left = pixelX + 'px';
		htmlBox.style.zIndex = 1000 + pos.y;
		this.htmlMapContainer.appendChild(htmlBox);

		if (this.gridBoxContent) {
			var htmlBoxBg = document.createElement("DIV");
			htmlBoxBg.style.position = 'absolute';
			htmlBoxBg.style.height = htmlBox.style.height;
			htmlBoxBg.style.width = htmlBox.style.width;
			htmlBoxBg.style.top = htmlBox.style.top;
			htmlBoxBg.style.left = htmlBox.style.left;
			htmlBoxBg.style.zIndex = 1;
			htmlBoxBg.innerHTML = this.gridBoxContent;
			this.htmlMapContainer.appendChild(htmlBoxBg);
		}

		if (pixelX + this.tileDim.a > this.mapCoordMax.x
				|| pixelY + this.tileDim.b + this.additionalBoxHeight > this.mapCoordMax.y) {
			this.mapCoordMax.x = Math.max(this.mapCoordMax.x, pixelX + this.tileDim.a);
			this.mapCoordMax.y = Math.max(this.mapCoordMax.y, pixelY + this.tileDim.b + this.additionalBoxHeight);
			this.moveMap(this.mapCoord.x, this.mapCoord.y);
		}

		return htmlBox;
	},

	/**
	 * déplace la zone visible de la map
	 *
	 * @param {Number}
	 *          pixelX distance en pixel pour le déplacement horizontal
	 * @param {Number}
	 *          pixelY distance en pixel pour le déplacement vertical
	 * @return {void}
	 */
	moveMap : function(pixelX, pixelY) {
		var rect = {
			t : pixelY,
			r : (this.htmlMapWidth + pixelX),
			b : (this.htmlMapHeight + pixelY),
			l : pixelX
		};
		if (rect.r > this.mapCoordMax.x) {
			rect.l -= rect.r - this.mapCoordMax.x;
			rect.r -= rect.r - this.mapCoordMax.x;
		}
		if (rect.l < 0) {
			rect.r += -1 * rect.l;
			rect.l += -1 * rect.l;
		}
		if (rect.b > this.mapCoordMax.y) {
			rect.t -= rect.b - this.mapCoordMax.y;
			rect.b -= rect.b - this.mapCoordMax.y;
		}
		if (rect.t < 0) {
			rect.b += -1 * rect.t;
			rect.t += -1 * rect.t;
		}
		// IE8 bug : clip show nothing
		// this.htmlMapContainer.style.clip = "rect(" + rect.t + "px, " + rect.r + "px, " + rect.b + "px, " +
		// rect.l
		// + "px)";
		// this.htmlMapContainer.style.left = (-1 * rect.l) + "px";
		// this.htmlMapContainer.style.top = (-1 * rect.t) + "px";
		// this.mapCoord = {
		// x : rect.l,
		// y : rect.t
		// };
	},

	/**
	 * ajoute du contenu à une case
	 *
	 * @param {Object}
	 *          pos coordonnées de case {x:posX, y:posY}
	 * @param {String|Object}
	 *          data contenu à ajouter à l'hexagone (chaine html ou objet DOM)
	 * @param {Boolean}
	 *          clearBefore indique si l'hexa doit etre vidé avant d'y ajouter le nouveau contenu
	 * @return {void}
	 */
	fillBox : function(pos, data, clearBefore) {
		var htmlBox = $(this.getBoxId(pos)) || this.createBox(pos);
		if (clearBefore) {
			htmlBox.innerHTML = '';
		}
		switch (typeof(data)) {
			case 'string' :
				htmlBox.innerHTML += data;
				break;
			case 'object' :
				htmlBox.appendChild(data);
				break;
		}
	},

	/**
	 * centre la vue sur une case si celle-ci n'est pas entièrement visible
	 *
	 * @param {Object}
	 *          pos coordonnées de case {x:posX, y:posY}
	 * @return {Boolean} <code>true</code> si l'hex existe, <code>false</code> sinon
	 */
	centerIfHidden : function(pos) {
		if (!this.isInMap(pos)) {
			return false;
		}
		var htmlBox = $(this.getBoxId(pos)) || this.createBox(pos);

		var rect = {
			t : parseInt(htmlBox.style.top),
			r : parseInt(htmlBox.style.left) + parseInt(htmlBox.style.width),
			b : parseInt(htmlBox.style.top) + parseInt(htmlBox.style.height),
			l : parseInt(htmlBox.style.left)
		};
		if (rect.t < this.mapCoord.y || rect.b > this.mapCoord.y + this.htmlMapHeight || rect.l < this.mapCoord.x
				|| rect.r > this.mapCoord.x + this.htmlMapWidth) {
			this.moveMap((rect.l + rect.r) / 2 - this.htmlMapWidth / 2, (rect.t + rect.b) / 2 - this.htmlMapHeight
							/ 2);
		}
		return true;
	},

	/**
	 * renvoie l'id d'une boite en fonction de ses coordonnées
	 *
	 * @param {Object}
	 *          pos coordonnées de case {x:posX, y:posY}
	 * @return {void}
	 */
	getBoxId : function(pos) {
		return "mapHexBox_" + pos.x + '_' + pos.y;
	},

	/**
	 * calcul la distance entre 2 points
	 *
	 * @param {Object}
	 *          pos1 coordonnées du premier point de la forme {x:posX, y:posY}
	 * @param {Object}
	 *          pos1 coordonnées du premier point de la forme {x:posX, y:posY}
	 * @return {Number}
	 */
	computeDist : function(pos1, pos2) {
		if (pos1.x > pos2.x) {
			var posTmp = pos1;
			pos1 = pos2;
			pos2 = posTmp;
		}
		var distX = Math.abs(pos2.x - pos1.x);
		var distY = Math.abs(pos2.y - pos1.y);
		var dist = distY + Math.max(distX - Math.floor((distY + (pos1.y % 2) - (pos2.y % 2)) / 2), 0);
		return dist;
	},

	isInMap : function(pos) {
		var b = pos && (pos.x >= 0 && pos.x <= this.mapPosMax.x && pos.y >= 0 && pos.y <= this.mapPosMax.y);
		if (b) {
			var bOdd = 1 == (pos.y % 2);
			return !(0 == pos.x && 1 == this.mapInactiveColumn && !bOdd)
					&& !(pos.x == this.mapPosMax.x && 2 == this.mapInactiveColumn && bOdd);
		} else {
			return false;
		}
	}

};

