var SLIDE_PUZZLE = window.SLIDE_PUZZLE || {};

// Model constructor builds a solveable, shuffled slide puzzle model.
SLIDE_PUZZLE.Model = function(rows, columns) {
	'use strict';
	this.ROWS =  rows;
	this.COLUMNS = columns;
	this.EMPTY = this.ROWS * this.COLUMNS - 1;
	
	this._position = [];
	this._moves = [];
	
	for (var i = 0; i < this.ROWS * this.COLUMNS; i++) {
		this._position.push(i);
	}
	do {
		this._shuffle();
	} while (!this._isSolveable());
};

// getPosition returns the current layout of the puzzle pieces.
SLIDE_PUZZLE.Model.prototype.getPosition = function() {
	'use strict';
	return this._position.slice();
};

SLIDE_PUZZLE.Model.prototype.getPositionOf = function(piece) {
	'use strict';
	for (var i = 0; i < this._position.length; i++) {
		if (this._position[i] === piece) {
			return i;
		}
	}
	return -1;
}

// getMoves returns the history of all moves.
SLIDE_PUZZLE.Model.prototype.getMoves = function() {
	'use strict';
	return this._moves.slice();
};

// isSolved indicates whether the puzzle is in its solved order.
SLIDE_PUZZLE.Model.prototype.isSolved = function() {
	'use strict';
	for (var i = 0; i < this._position.length - 1; i++) {
		if (this._position[i] > this._position[i+1]) return false;
	}
	return true;
};

// move moves piece into the blank space and logs the move in the move history.
// move throws an error if the move is invalid or if the piece doesn't exist.
SLIDE_PUZZLE.Model.prototype.move = function(piece) {
	'use strict';
	if (!this._isValidMove(piece)) {
		throw new Error('Invalid move');
	}
	var iPiece = this._position.indexOf(piece);
	var iEmpty = this._position.indexOf(this.EMPTY);
	this._swap(iPiece, iEmpty);
	this._moves.push(piece);
};

// undo reverses the last move and removes it from the move history.
// undo throws an error if there are no moves in the history.
SLIDE_PUZZLE.Model.prototype.undo = function() {
	'use strict';
	if (this._moves.length === 0) {
		throw new Error('No moves to undo');
	}
	var piece = this._moves.pop();
	var iPiece = this._position.indexOf(piece);
	var iEmpty = this._position.indexOf(this.EMPTY);
	this._swap(iPiece, iEmpty);
};

// _isValidMove determines whether piece can move, that is, whether it is next to the empty space.
SLIDE_PUZZLE.Model.prototype._isValidMove = function(piece) {
	'use strict';
	if (!this._isValidPiece(piece)) {
		return false;
	}
	var iPiece = this._position.indexOf(piece);
	var iEmpty = this._position.indexOf(this.EMPTY);
	return Math.abs(iPiece - iEmpty) === this.COLUMNS ||
			Math.abs(iPiece - iEmpty) === 1;
};

// _isValidPiece indicates whether piece is present in this puzzle.
SLIDE_PUZZLE.Model.prototype._isValidPiece = function(piece) {
	'use strict';
	return piece >= 0 && piece < this.EMPTY;
};

// _isSolveable indicates whether the puzzle is solveable, using Mark Ryan's algorithm.
// See https://www.cs.bham.ac.uk/~mdr/teaching/modules04/java2/TilesSolvability.html.
SLIDE_PUZZLE.Model.prototype._isSolveable = function() {
	'use strict';
	function even(n) {
		return n % 2 === 0;
	}
	
	// Count inversions
	var inversions = 0;
	for (var i = 0; i < this._position.length; i++) {
		for (var j = i + 1; j < this._position.length; j++) {
			if (this._position[j] < this._position[i]) {
				inversions++;
			}
		}
	}
	
	var iEmpty = this._position.indexOf(this.EMPTY);
	var isEmptyOddFromBottom = !even(this.ROWS - Math.floor(iEmpty / this.ROWS));
	var width = this.COLUMNS;
	return (!even(width) && even(inversions)) ||
			(even(width) && isEmptyOddFromBottom === even(inversions));
};

// _swap swaps the pieces at indices i1 and i2 in this._position.
SLIDE_PUZZLE.Model.prototype._swap = function(i1, i2) {
	'use strict';
	var tmp = this._position[i1];
	this._position[i1] = this._position[i2];
	this._position[i2] = tmp;
};

// _shuffle performs a Fisher-Yates shuffle on this._position.
SLIDE_PUZZLE.Model.prototype._shuffle = function() {
	'use strict';
	// Returns a random int n where 0 ≤ n < max
	function randomInt(max) {
		return Math.floor(Math.random() * max);
	}
	
	//TODO: should possible random ints include i?
	for (var i = this._position.length - 1; i > 0; i--) {
		this._swap(randomInt(i), i);
	}
};

SLIDE_PUZZLE.Model.prototype.toString = function() {
	'use strict';
	var lines = [];
	for (var i = 0; i < this.ROWS; i++) {
		var rowStart = i * this.COLUMNS;
		lines.push(this._position.slice(rowStart, rowStart + this.COLUMNS).join('\t'));
	}
	return lines.join('\n');
};
