/**
 * Chess - Proof of concept
 *
 * @license		MIT-style license
 * @author		Harald Kirschner <mail [at] digitarald.de>
 * @copyright	Author
 */

var Chess = new Class({

	linesX: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'],
	linesY: [1, 2, 3, 4, 5, 6, 7, 8],
	fieldSize: 50,
	colorClass: ['white', 'black'],
	colorId: ['White', 'Black'],
	shapeNames: {
		'p': 'Pawn',
		'q': 'Queen',
		'k': 'King',
		'b': 'Bishop',
		'n': 'Knight',
		'r': 'Rook'
	},
	rounds: 0,
	active: 0,

	initialize: function(el) {
		this.element = $(el);
		this.board = new Element('div', {'class': 'field-wrap'});
		this.stateLog = $('chess-log-state');
		this.createField();
		this.createShapes();
		this.startRound();
		this.element.adopt(this.board);
	},

	startRound: function() {
		this.round = 0;
		this.moves = [];
		this.bashed = [[], []];
		this.state = {check: null, checkmate: null};
		this.nextRound();
	},

	nextRound: function() {
		var c = this.active = this.round++ % 2;
		var r = this.rival = !c * 1;

		var aux = this.calculateMoves(r, this.calculateMoves(c).flatten()).flatten();
		if (this.state.check === r) {
			this.undo.delay(1, this);
			return;
		}

		this.state.check = null;
		this.state.checkmate = null;
		var movesC = this.calculateMoves(c, aux);

		var log = [this.colorId[c], ' is playing'];
		if (this.state.checkmate === c) log.push(', CHECKMATE! Reload to restart!');
		else if (this.state.check === c) log.push(', CHECK!');
		this.stateLog.set('html', log.join(''));

		this.eachShape(function(shape, i) {
			var moves = movesC[i];
			if (!shape.state || !moves.length) return;

			shape.el.addClass('active').addEvents({
				'mouseover': function() {
					if (this.dragShape) return;
					moves.each(this.highlightFieldOn, this);
				}.bind(this),
				'mouseout': function() {
					if (this.dragShape) return;
					moves.each(this.highlightFieldOff, this);
				}.bind(this),
				'mousedown': function(e) {
					if (this.dragShape || shape.fx.timer) return;
					var clone = shape.el.clone().addClass('shape-clone').set('opacity', 0.4)
						.injectBefore(shape.el.addClass('dragging'));
					var drops = new Elements(moves.map(function(field) {
						return field.el.addEvents({
							'drop': this.makeMove.create({'arguments': [shape, field], 'bind': this, 'delay': 1})
						});
					}, this));
					this.dragShape = new Drag.Move(shape.el, {
						'droppables': drops,
						'preventDefault': true,
						'onDrop': function(element, drop) {
							var fx = new Fx.Tween(clone).start('opacity', 0).chain(function() {
								clone.destroy();
								fx = null;
							});
							if (drop) drop.fireEvent('drop');
							else shape.fx.start({
								'left': clone.getStyle('left').toInt(),
								'top': clone.getStyle('top').toInt()});
							drops.removeEvents('drop');
							this.dragShape.detach();
							this.dragShape = null;
							shape.el.removeClass('dragging').fireEvent('mouseout');
						}.bind(this)
					});
					this.dragShape.start(e.stop());
				}.bind(this)
			});
		}, c);
	},

	makeMove: function(shape, field) {
		if (this.dragShape) this.dragShape.stop();
		this.eachShape(this.removeEvents, this.active);
		this.eachField(this.removeEvents);
		var bashed = field.shape;
		if (bashed) this.bashShape(bashed);
		this.moves.push([shape, shape.field, field, bashed]);
		shape.moves++;
		this.moveShape(shape, field);
		this.nextRound();
	},

	undo: function(shape, field) {
		if (!this.moves.length) {
			this.round--;
			this.nextRound();
			return;
		}
		this.eachShape(this.removeEvents, this.active);
		this.eachField(this.removeEvents);
		this.round -= 2;
		var move = this.moves.splice(this.moves.length - 1, 1)[0];
		this.moveShape(move[0], move[1]);
		move[0].moves--;
		if (move[3]) {
			var bashed = move[3];
			this.moveShape(bashed, bashed.field);
			bashed.moves--;
			bashed.state = 1;
			this.bashed[bashed.color].erase(bashed.index);
		}
		this.nextRound();
	},

	moveShape: function(shape, field) {
		var to = {
			'left': field.pos[0] * this.fieldSize,
			'top': field.pos[1] * this.fieldSize};
		if (!shape.moves) shape.el.setStyles(to);
		else shape.fx.start(to);
		if (shape.field) shape.field.shape = null;
		shape.field = field;
		field.shape = shape;
	},

	bashShape: function(shape) {
		shape.state = 0;
		shape.fx.start({
			'top': (-this.fieldSize) + this.bashed[shape.color].length * this.fieldSize,
			'left': shape.color ? -this.fieldSize : 400});
		this.bashed[shape.color].push(shape.index);
	},

	calculateMoves: function(c, aux) {
		var ret = [];
		if (aux) this.state = {check: null, checkmate: null};
		this.eachShape(function(shape) {
			if (!shape.state) {
				ret.push([]);
				return;
			}
			var moves = [];
			var pos = shape.field.pos;
			var x = shape.field.pos[0], y = shape.field.pos[1];
			var special = {};
			switch (shape.tag) {
				case 'p':
					var ns = (c ? 1 : -1);
					var dir = [[x, y + ns]];
					if (!shape.moves) dir.push([x, y + 2 * ns]);
					moves.push(dir, [[x - 1, y + ns]], [[x + 1, y + ns]]);
					special.p = 1;
					break;
				case 'k':
					moves.push(
						[[x - 1, y - 1]], [[x, y - 1]], [[x + 1, y - 1]],
						[[x + 1, y]], [[x + 1, y + 1]],
						[[x, y + 1]], [[x - 1, y + 1]], [[x - 1, y]]
					);
					if (aux){
						special.k = 1;
						moves.unshift([pos]);
						moves = moves.filter(function(pos) {
							pos = pos[0];
							return aux.every(function(no) {
								return !pos.arrayEquals(no.pos);
							});
						});
						if (!moves[0].arrayEquals(pos)) this.state.check = c;
						else moves.shift();
					}
					break;
				case 'n':
					moves.push(
						[[x + 1, y - 2]], [[x + 2, y - 1]],
						[[x + 1, y + 2]], [[x + 2, y + 1]],
						[[x - 1, y - 2]], [[x - 2, y - 1]],
						[[x - 1, y + 2]], [[x - 2, y + 1]]
					);
					break;
				default:
					if (['q', 'r'].contains(shape.tag)) {
						moves.push(
							pos.fill2dVector([x, 0]),
							pos.fill2dVector([7, y]),
							pos.fill2dVector([x, 7]),
							pos.fill2dVector([0, y]));
					}
					if (['q', 'b'].contains(shape.tag)) {
						moves.push(
							pos.fill2dVector([x + 7, y - 7]),
							pos.fill2dVector([x + 7, y + 7]),
							pos.fill2dVector([x - 7, y - 7]),
							pos.fill2dVector([x - 7, y + 7]));
					};
			}
			if (!moves.length) {
				ret.push([]);
				return;
			}
			moves = moves.map(function(dir, n) {
				if (!dir.length) return [];
				var hit = false, ret;
				return dir.map(this.posToField, this).filter(function(field){
					if (hit || !field) return false;
					if (field.shape) {
						hit = (!aux && field.shape.tag == 'k') ? false : true;
						return ((special.p && !n) ? (!aux) : ((!aux) ? true : (field.shape.color != c)));
					}
					return (special.p) ? (n ? !aux : !!aux) : true;
				}, this);
			}, this).flatten();
			if (special.k && (this.state.check == c) && !moves.length) this.state.checkmate = c;
			ret.push(moves);
		}, c);
		return ret;
	},

	posToField: function(pos) {
		return this.field[pos[0]] ? (this.field[pos[0]][pos[1]] || null) : null;
	},

	fieldToElement: function(field) {
		return field.el;
	},

	removeEvents: function(entry) {
		entry.el.removeClass('active').removeEvents();
	},

	highlightFieldOn: function(field) {
		field.el.addClass('field-' + this.colorClass[field.color] + '-hilighted');
	},
	highlightFieldOff: function(field) {
		field.el.removeClass('field-' + this.colorClass[field.color] + '-hilighted');
	},

	createField: function() {
		this.field = [];
		this.eachField(this.addField);
		var guide = new Element('div', {'class': 'guide'});
		for (var i = 1; i <= 8; i++) {
			this.element.adopt(
				guide.clone().addClass('guide-n').setStyle('left', this.fieldSize * i).set('html', this.linesX[i - 1]),
				guide.clone().addClass('guide-e').setStyle('top', this.fieldSize * i).set('html', this.linesY[i - 1]),
				guide.clone().addClass('guide-s').setStyle('left', this.fieldSize * i).set('html', this.linesX[i - 1]),
				guide.clone().addClass('guide-w').setStyle('top', this.fieldSize * i).set('html', this.linesY[i - 1])
			);
		}
	},

	addField: function(field, x, y, id, c) {
		this.field[x] = this.field[x] || [];
		var cls = 'field ' + this.colorClass[c] + ' ' + 'field-' + this.colorClass[c];
		field = {
			'shape': null,
			'id': id,
			'pos': [x, y],
			'color': c,
			'el': new Element('div', {'class': cls, 'id': id, 'title': id})
				.setStyles({
					'left': x * this.fieldSize,
					'top': y * this.fieldSize}).inject(this.board)
		};
		this.field[x].push(field);
	},

	createShapes: function() {
		this.shapes = [[], []];
		var names = ['r', 'n', 'b', 'q', 'k', 'b', 'n', 'r'], x, y, add, tag;
		for (var c = 0; c < 2; c++) {
			this.eachShape(function(s, i) {
				tag = names[i] || 'p';
				add = {
					'tag': tag,
					'color': c,
					'name': this.shapeNames[tag],
					'state': 1,
					'moves': 0,
					'index': i
				};
				var cls = 'shape shape-' + tag + ' shape-' + tag + '-' + this.colorClass[c] + ' ' + this.colorClass[c];
				add.el = new Element('div', {
						'class': cls,
						'title': this.colorId[c] + ' ' + add.name})
					.inject(this.board);
				add.fx = new Fx.Morph(add.el, {
					'chain': 'cancel',
					'duration': 600,
					'transition': Fx.Transitions.Back.easeOut});
				this.moveShape(add, this.field[i % 8][c ? (names[i] ? 0 : 1) : (names[i] ? 7 : 6)]);
				this.shapes[c].push(add);
			}, c, true);
		};
	},

	eachField: function(fn){
		for (var i = 0, x = 0, y = 0; i < 64; i++, y = i % 8, x = (i - y) / 8) {
			fn.call(this, this.field[x] ? this.field[x][y] || null : null, x, y, this.linesX[x] + this.linesY[y], (((x % 2) && !(y % 2)) || ((y % 2) && !(x % 2))) * 1);
		}
	},

	eachShape: function(fn, color){
		for (var i = 0; i < 16; i++) fn.call(this, this.shapes[color][i], i, color);
	}

});


Array.implement({

	arrayEquals: function(array) {
		return this.toString() == array.toString();
	},

	fill2dVector: function(to) {
		var max = Math.max(Math.abs(this[0] - to[0]), Math.abs(this[1] - to[1]));
		var ret = [], from = this.slice(0);
		for (var i = 0; i < max; i++) {
			ret.push([
				(to[0] == from[0]) ? to[0] : ((to[0] < from[0]) ? --from[0] : ++from[0]),
				(to[1] == from[1]) ? to[1] : ((to[1] < from[1]) ? --from[1] : ++from[1])]);
		}
		return ret;
	}

});
