'use strict';

var useLocalStorage = (typeof localStorage !== 'undefined');

/**
 * if true, allows placing any card in the Flower slot, and dragons are always movable.
 * @type {Boolean}
 */
var DEBUG = false;

/**
 * if true, the alternate stylesheet is loaded even if images load correctly.
 * @type {Boolean}
 */
var DEBUG_STYLE = false;

/**
 * Time in milliseconds cards take moving around
 * @type {Number}
 */
var CARD_ANIMATION_SPEED = 100;

/**
 * Gap in pixels between cards when fanned out.
 * @type {Number}
 */
var CARD_STACK_GAP = 30;

/**
 * The seed of the game being played right now.
 */
var currentSeed;

var bambooWhiteToGreen = 'sepia(100%) saturate(10000%) hue-rotate(63deg) brightness(.35)';

var SUITS = {
	BAMBOO: {
		order: 1,
		color: '#17714e',
		prefix_large: 'bamboo',
		small: 'bamboo',
		fixAssetsFilter: bambooWhiteToGreen, // apply a color the the bamboo/green-dragon images since they changed to white.
	},
	CHARACTERS: {
		order: 2,
		color: '#000000',
		prefix_large: 'char',
		small: 'characters'
	},
	COINS: {
		order: 3,
		color: '#ae2810',
		prefix_large: 'coins',
		small: 'coins'
	}
};

var SPECIAL = {
	DRAGON_GREEN: {
		order: 1,
		large: 'dragon_green',
		small: 'dragon_green',
		equivalentSuit: 'bamboo',
		fixAssetsFilter: bambooWhiteToGreen, // apply a color the the bamboo/green-dragon images since they changed to white.
	},
	DRAGON_RED: {
		order: 2,
		large: 'dragon_red',
		small: 'dragon_red',
		equivalentSuit: 'coins',
	},
	DRAGON_WHITE: {
		order: 3,
		large: 'dragon_white',
		small: 'dragon_white',
		equivalentSuit: 'characters',
	},
	FLOWER: {
		order: 4,
		large: 'flower',
		small: 'flower',
		equivalentSuit: 'flower',
	}
};

/**
 * Number of each type of dragon to create.
 * @type {Number}
 */
var DRAGON_COUNT = 4;

/**
 * Height in px of the tray slots.
 * @type {Number}
 */
var SLOT_TALL = 500;

/**
 * Contains groupings of slots, which will have an "element" property added.
 * @type {Object}
 */
var SLOTS = {
	SPARE: [
		{
			type: 'spare',
			top: 18,
			left: 46
		},
		{
			type: 'spare',
			top: 18,
			left: 198
		},
		{
			type: 'spare',
			top: 18,
			left: 350
		}
	],
	FLOWER: [
		{
			type: 'flower',
			top: 18,
			left: 614
		}
	],
	OUT: [
		{
			type: 'out',
			top: 18,
			left: 806
		},
		{
			type: 'out',
			top: 18,
			left: 958
		},
		{
			type: 'out',
			top: 18,
			left: 1110
		}
	],
	TRAY: [
		{
			type: 'tray',
			fan: true,
			top: 282,
			left: 46,
			height: SLOT_TALL
		},
		{
			type: 'tray',
			fan: true,
			top: 282,
			left: 198,
			height: SLOT_TALL
		},
		{
			type: 'tray',
			fan: true,
			top: 282,
			left: 350,
			height: SLOT_TALL
		},
		{
			type: 'tray',
			fan: true,
			top: 282,
			left: 502,
			height: SLOT_TALL
		},
		{
			type: 'tray',
			fan: true,
			top: 282,
			left: 654,
			height: SLOT_TALL
		},
		{
			type: 'tray',
			fan: true,
			top: 282,
			left: 806,
			height: SLOT_TALL
		},
		{
			type: 'tray',
			fan: true,
			top: 282,
			left: 958,
			height: SLOT_TALL
		},
		{
			type: 'tray',
			fan: true,
			top: 282,
			left: 1110,
			height: SLOT_TALL
		}
	]
};

// Audio element for OST
var music = null;

jQuery.fn.visible = function () {
	return this.css('visibility', 'visible');
};

jQuery.fn.invisible = function () {
	return this.css('visibility', 'hidden');
};

jQuery.fn.visibilityToggle = function () {
	return this.css('visibility', function (i, visibility) {
		return (visibility == 'visible') ? 'hidden' : 'visible';
	});
};

/**
 * Creates a card of the given value and suit.
 * @param  {Integer} value
 * @param  {SUIT} suit 
 * @return {Card} {element: HTMLElement, value: Integer, suit: SUIT}
 */
function createCard(value, suit) {
	var smallImg = 'solitaire/small_icons/' + suit.small + '.png';
	var largeImg = 'solitaire/large_icons/' + suit.prefix_large + '_' + value + '.png';
	var card = $('<div class="card card-numbered nickardson card-' + suit.small + ' card-' + value + '">' +
		'<div class="card-count-a"></div>' +
		'<div class="card-count-b"></div>' +
		'<div class="card-mini-logo-a"></div>' +
		'<div class="card-mini-logo-b"></div>' +
		'<div class="card-logo"></div>' +
		'</div>');

	card.css('color', suit.color);
	card.find('.card-count-a,.card-count-b').text(value);
	card.find('.card-mini-logo-a,.card-mini-logo-b')
		.css({
			'background-image': 'url(' + smallImg + ')',
			'filter': suit.fixAssetsFilter
		});
	card.find('.card-logo')
		.css({
			'background-image': 'url(' + largeImg + ')',
			'filter': suit.fixAssetsFilter
		});

	var c = {
		element: card,
		value: value,
		suit: suit
	};
	card.data('card', c);
	return c;
}

/**
 * Creates a 'special' card of the given type.
 * @param  {SPECIAL} special Card type definitition from the SPECIAL table.
 * @return {Card}
 */
function createSpecialCard(special) {
	var smallImg = 'solitaire/small_icons/' + special.small + '.png';
	var largeImg = 'solitaire/large_icons/' + special.large + '.png';
	var card = $('<div class="card card-special card-' + special.equivalentSuit + '">' +
		'<div class="card-logo-a"></div>' +
		'<div class="card-logo-b"></div>' +
		'<div class="card-logo"></div>' +
		'</div>');

	card.find('.card-logo-a,.card-logo-b')
		.css({
			'background-image': 'url(' + smallImg + ')',
			'filter': special.fixAssetsFilter
		});
	card.find('.card-logo')
		.css({
			'background-image': 'url(' + largeImg + ')',
			'filter': special.fixAssetsFilter
		});

	var c = {
		element: card,
		special: special
	};
	card.data('card', c);
	return c;
}

/**
 * Places a card at the given level on the given slot.
 * @param {Card} card  The card to place.
 * @param {Subslot} slot  The destination slot
 * @param {Integer} depth How high up the card is. Higher values are further up the stack. When fanned out, higher values are displayed closer to the bottom.
 */
function insertCard(card, slot, depth) {
	if (card.slot !== undefined) {
		card.slot.cards.splice(card.slot.cards.indexOf(card), 1); // remove the card from the previous slot
	}
	// add the card to a new slot.
	slot.cards.splice(depth, 0, card);
	card.slot = slot;

	var e = $(slot.element);

	var ce = card.element.detach();
	if (depth === 0) {
		e.prepend(ce);
	} else {
		var target = e.find('.card:nth-child(' + depth + ')');
		if (target.length !== 0) {
			target.after(ce);
		} else {
			e.append(ce);
		}
	}

	var h = 0;
	if (slot.fan === true) {
		h = depth * CARD_STACK_GAP;
	}
	card.element.css({
		'top': h + 'px',
		'left': 0
	});
}

/**
 * Randomize array element order in-place.
 * Using Durstenfeld shuffle algorithm.
 * @param {Array[?]} array Array to shuffle in-place
 */
function shuffleArray(array) {
	for (var i = array.length - 1; i > 0; i--) {
		var j = Math.floor(Math.random() * (i + 1));
		var temp = array[i];
		array[i] = array[j];
		array[j] = temp;
	}
}

/**
 * Creates the elements for the slots, and prepares them to accept cards.
 * @param  {SLOTS} slots The SLOTS object, a set of arrays, each containing slots.
 * @param  {HTMLElement} board The element slots are parented to.
 */
function populateSlots(slots, board) {
	// Create the slots
	for (var slotname in slots) {
		if (slots.hasOwnProperty(slotname)) {
			var list = slots[slotname];
			for (var i = 0; i < list.length; i++) {
				var slot = $('<div class="slot"></div>').css({
					top: list[i].top,
					left: list[i].left
				}).addClass('slot-' + list[i].type);
				if (list[i].height !== undefined) {
					slot.css('height', list[i].height);
				}
				slot.appendTo(board);
				list[i].cards = [];
				list[i].element = slot;
				slot.data('slot', list[i]);
			}
		}
	}
}

/**
 * Creates all the cards in a full deck.
 * @return {Array[Card]} 
 */
function makeDeck() {
	var cards = [];
	var s; // 0-2 suit index.
	var suit; // the actual suit object
	for (var value = 1; value <= 9; value++) {
		for (s = 0; s < 3; s++) {
			switch (s) {
				case 0:
					suit = SUITS.BAMBOO;
					break;
				case 1:
					suit = SUITS.CHARACTERS;
					break;
				case 2:
					suit = SUITS.COINS;
					break;
			}
			cards.push(createCard(value, suit));
		}
	}

	for (s = 0; s < 3; s++) {
		for (var i = 0; i < DRAGON_COUNT; i++) {
			switch (s) {
				case 0:
					suit = SPECIAL.DRAGON_GREEN;
					break;
				case 1:
					suit = SPECIAL.DRAGON_WHITE;
					break;
				case 2:
					suit = SPECIAL.DRAGON_RED;
					break;
			}
			cards.push(createSpecialCard(suit));
		}
	}

	cards.push(createSpecialCard(SPECIAL.FLOWER));
	return cards;
}

/**
 * Places the given cards onto the given board in a top-to-bottom, left-to-right fashion.
 * @param  {Array[Card]} cards   List of cards to place
 * @param  {HTMLElement} board   Parent element for the cards.
 * @param  {SLOT} traySet An array of individual slots, (ex: SLOTS.TRAY)
 */
function placeCardsInTray(cards, board, traySet) {
	var row = 0;
	var col = 0;
	for (var i = 0; i < cards.length; i++) {
		var card = cards[i];
		card.element.appendTo(board);
		insertCard(card, traySet[col], row);

		row++;
		if (row >= 5) {
			row = 0;
			col++;
		}
	}
	onFieldUpdated();
}

/**
 * Makes sure there are no vertical gaps between cards in the tray, by moving them towards the top to fill the gap.
 */
function balanceCards() {
	for (var i = 0; i < SLOTS.TRAY.length; i++) {
		for (var y = 0; y < SLOTS.TRAY[i].cards.length; y++) {
			insertCard(SLOTS.TRAY[i].cards[y], SLOTS.TRAY[i], y);
		}
	}
}

/**
 * Gets all special cards of the given type.
 * @param  {SPECIAL} type
 * @return {Array[Card]}
 */
function getSpecialCards(type) {
	var list = [];
	for (var slotName in SLOTS) {
		if (SLOTS.hasOwnProperty(slotName)) {
			for (var i = 0; i < SLOTS[slotName].length; i++) {
				var stack = SLOTS[slotName][i].cards;
				for (var j = 0; j < stack.length; j++) {
					var card = stack[j];
					if (card.special == type) {
						list.push(card);
					}
				}
			}
		}
	}

	return list;
}

/**
 * Gets all special cards of the given type which are on the top of their respective stack.
 * @param  {SPECIAL} type
 * @return {Array[Card]}
 */
function getTopSpecialCards(type) {
	var list = [];
	for (var slotName in SLOTS) {
		if (SLOTS.hasOwnProperty(slotName)) {
			for (var i = 0; i < SLOTS[slotName].length; i++) {
				var stack = SLOTS[slotName][i].cards;
				if (stack.length === 0) {
					continue;
				}
				var card = stack[stack.length - 1];
				if (card.special == type) {
					list.push(card);
				}
			}
		}
	}

	return list;
}

/**
 * Gets the card with the given value and suit, if any.
 * @param  {Integer} value Card value
 * @param  {SUIT} suit  Card suit
 * @return {Card}       The found card, or undefined.
 */
function getCard(value, suit) {
	for (var slotName in SLOTS) {
		if (SLOTS.hasOwnProperty(slotName)) {
			for (var i = 0; i < SLOTS[slotName].length; i++) {
				var stack = SLOTS[slotName][i].cards;
				for (var j = 0; j < stack.length; j++) {
					if (stack[j].value === value && stack[j].suit == suit) {
						return stack[j];
					}
				}
			}
		}
	}
}

/**
 * Gets whether the dragons of the given type are all on the top of their respective stack, and a slot is open for them to go to.
 * @param  {SPECIAL}  type 
 * @return {Boolean}  Whether all dragon cards of that type are able to be moved, and there is an open slot.
 */
function isDragonReady(type) {
	if (!DEBUG) {
		var allAvailable = getTopSpecialCards(type).length == DRAGON_COUNT;
		var spaceOpen = false;

		for (var i = 0; i < SLOTS.SPARE.length; i++) {
			if (SLOTS.SPARE[i].cards.length === 0 || SLOTS.SPARE[i].cards[0].special === type) {
				spaceOpen = true;
			}
		}

		return allAvailable && spaceOpen;
	} else {
		return true;
	}
}

var DRAGON_BTNS = [{
	type: SPECIAL.DRAGON_RED,
	selector: '#btn_dragon_red',
	imgNone: 'solitaire/button_red_up.png',
	imgReady: 'solitaire/button_red_active.png',
	imgComplete: 'solitaire/button_red_down.png',
},
{
	type: SPECIAL.DRAGON_GREEN,
	selector: '#btn_dragon_green',
	imgNone: 'solitaire/button_green_up.png',
	imgReady: 'solitaire/button_green_active.png',
	imgComplete: 'solitaire/button_green_down.png',
},
{
	type: SPECIAL.DRAGON_WHITE,
	selector: '#btn_dragon_white',
	imgNone: 'solitaire/button_white_up.png',
	imgReady: 'solitaire/button_white_active.png',
	imgComplete: 'solitaire/button_white_down.png',
}
];

/**
 * To be called when cards are done moving. Handles setting up the UI for dragons, etc.
 */
function onFieldUpdated() {
	var i;
	// prepare buttons if dragons are available
	for (i = 0; i < DRAGON_BTNS.length; i++) {
		var btn = DRAGON_BTNS[i];
		if ($(btn.selector).data('complete') !== true) {
			if (isDragonReady(btn.type)) {
				$(btn.selector).css('background-image', 'url(\'' + btn.imgReady + '\')').data('active', true);
			} else {
				$(btn.selector).css('background-image', 'url(\'' + btn.imgNone + '\')').data('active', false);
			}
		}
	}

	// move cards to the out tray when possible.
	// it is movable when there are no cards that can be placed on that card, and the destination is 1 less than this card.
	// this means that, for a BAMBOO 5, there must be no 4s anywhere in the tray or spare slots.

	// build a list of cards which are on the top of their stacks, and are potentially eligible to be automatically moved.
	var movableTops = [];
	var cards;
	var card;
	for (i = 0; i < SLOTS.TRAY.length; i++) {
		cards = SLOTS.TRAY[i].cards;
		if (cards.length > 0) {
			card = cards[cards.length - 1];
			if (card.value || card.special === SPECIAL.FLOWER) {
				movableTops.push(card);
			}
		}
	}
	for (i = 0; i < SLOTS.SPARE.length; i++) {
		cards = SLOTS.SPARE[i].cards;
		if (cards.length > 0) {
			card = cards[cards.length - 1];
			if (card.value || card.special === SPECIAL.FLOWER) {
				movableTops.push(card);
			}
		}
	}

	for (i = 0; i < movableTops.length; i++) {
		var canOut = true;
		var outSlot = undefined;

		card = movableTops[i];
		if (card.special == SPECIAL.FLOWER) {
			// flower can always move to flower slot.
			outSlot = SLOTS.FLOWER[0];
		} else if (card.value > 1) {
			// output only if the card of the same suit with -1 value is in the out tray,
			// AND if all cards with different suits and -2 value are in the out tray.
			for (var suit in SUITS) {
				var cardAbove;

				if (card.suit === SUITS[suit]) {
					// For checking the same suit, check if the value above has been placed.
					cardAbove = getCard(card.value - 1, SUITS[suit]);
				} else {
					// For a different suit, the reasoning is more complex.
					// The top card itself is free to move up if no other card would have a reason to be placed on it.
					//   Cards placable on it would have a value -1 from the top card.
					//   If the slot -1 from THAT card (-2 from the movable top card) is filled,
					//   then the moment the -1 card is revealed it will be moved, so the top card has no reason to consider that card as a reason to stay.

					// The 2 card doesn't care about the cards in other suits with -2 values.
					if (card.value === 2) {
						continue;
					}

					cardAbove = getCard(card.value - 2, SUITS[suit]);
				}

				if (cardAbove) {
					if (cardAbove.slot.type != 'out') {
						canOut = false;
						break;
					} else {
						// card-1 is in the out slot, save that location.
						if (cardAbove.suit == card.suit) {
							outSlot = cardAbove.slot;
						}
					}
				}
			}
		} else {
			// output this '1' valued card to the first empty 'out' slot
			for (var j = 0; j < SLOTS.OUT.length; j++) {
				if (SLOTS.OUT[j].cards.length === 0) {
					outSlot = SLOTS.OUT[j];
					break;
				}
			}
		}

		if (canOut && outSlot) {
			tweenCard(card, outSlot, outSlot.cards.length);
			setTimeout(onFieldUpdated, CARD_ANIMATION_SPEED);
			// don't move any more top cards in this iteration, next will be moved after this card finishes.
			break;
		}
	}

	// no more top cards to move, is the field clear?
	var allGood = true;
	for (i = 0; i < SLOTS.TRAY.length; i++) {
		if (SLOTS.TRAY[i].cards.length !== 0) {
			allGood = false;
			break;
		}
	}

	if (allGood) {
		if (!isInVictory) {
			localStorage.shenzhen_win_count++;
			updateWinCount();
		}
		isInVictory = true;
		// wait for any possible animations to finish.
		setTimeout(function () {
			victoryScreen();
		}, CARD_ANIMATION_SPEED);
	}
}

/**
 * Moves a card to a slot and position, then smoothly animates the transition.
 * @param  {Card}   card     The card to move
 * @param  {SLOT}   slot     The destination slot for the card
 * @param  {Integer}   depth    The position in the slot for the card
 * @param  {Function} callback Called when the animation is complete with the arguments (card, slot, depth)
 */
function tweenCard(card, slot, depth, callback) {
	// remember the original position, move the card to determine the final position, then reset to original and interpolate between them.

	var oldOffset = card.element.offset();
	insertCard(card, slot, depth);
	var newOffset = card.element.offset();

	var dY = newOffset.top - oldOffset.top,
		dX = newOffset.left - oldOffset.left;
	var finalY = parseInt(card.element.css('top')),
		finalX = parseInt(card.element.css('left'));

	card.element.css({
		top: finalY - dY,
		left: finalX - dX,
		'z-index': 99
	});

	card.element.animate({
		top: finalY,
		left: finalX
	}, CARD_ANIMATION_SPEED, 'swing', function () {
		card.element.css('z-index', '');

		if (typeof callback === 'function') {
			callback(card, slot, depth);
		}
	});
}

/**
 * Intended as a parameter for tweenCard.
 * Applies a backing to the card. Also adds special "dragon" backings if the win count is right.
 * @param  {Card} card  The card
 * @param  {SLOT} slot  ignored
 * @param  {Integer} depth ignored
 */
function applyCardBacking(card, _slot, _depth) {
	card.element.addClass('card-reverse');

	// special backing
	if (useLocalStorage) {
		if (localStorage.shenzhen_win_count >= 100) {
			card.element.addClass('grand_dragon');
		}
		if (localStorage.shenzhen_win_count >= 200) {
			card.element.addClass('grand_dragon_2');
		}
	}
}

/**
 * Creates an jQuery action function for a button from DRAGON_BTNS
 * @param  {DRAGON_BTNS element} b The button description
 * @return {Function}
 */
function dragonBtnListener(b) {
	return function () {
		if ($(this).data('active') === true) {
			var i;
			var list = getSpecialCards(b.type);

			var openSlot;
			for (i = 0; i < SLOTS.SPARE.length; i++) {
				var set = SLOTS.SPARE[i].cards;
				// TODO: if any spare slot already has this dragon, go to that one instead.
				if (set.length >= DRAGON_COUNT && set[0].special == b.type) {
					return false;
				}

				if (set.length === 0 || set[0].special == b.type && set.length < DRAGON_COUNT) {
					openSlot = SLOTS.SPARE[i];
					break;
				}
			}

			if (list.length > 0 && openSlot !== undefined) {
				for (i = 0; i < list.length; i++) {
					tweenCard(list[i], openSlot, openSlot.cards.length, applyCardBacking);
				}
				$(b.selector).css('background-image', 'url(\'' + b.imgComplete + '\')').data('complete', true);
				balanceCards();
				onFieldUpdated();
			}
		}
	};
}

/**
 * Creates a function which either highlights or unhighlights dragons as specified by the button.
 * @param  {DRAGON_BTNS item}  b    Button specification
 * @param  {Boolean} isEnter If true, highlights the items. If false, removes the highlights.
 * @return {Function}         The created function
 */
function dragonEnterLeaveListener(b, isEnter) {
	return function () {
		if (dragging) {
			return;
		}
		var cards = getSpecialCards(b.type);

		for (var i = 0; i < cards.length; i++) {
			if (isEnter) {
				$(cards[i].element).addClass('card-highlight');
			} else {
				$(cards[i].element).removeClass('card-highlight');
			}
		}
	};
}

for (var i = 0; i < DRAGON_BTNS.length; i++) {
	var btn = DRAGON_BTNS[i];
	$(btn.selector).click(dragonBtnListener(btn));
	$(btn.selector).mouseenter(dragonEnterLeaveListener(btn, true));
	$(btn.selector).mouseleave(dragonEnterLeaveListener(btn, false));
}

/**
 * Finds whether the given stack is valid to be picked up,
 * IE:
 * If the stack is a single card.
 * OR From the bottom-most card up, each is a numbered card, decreases by in value by 1, and is not the same color as the previous.
 * @param  {Array[Card]} stack A list of cards, with the first element being the "bottom-most" card.
 * @param  {SLOT} sourceSlot The type of slot this comes from
 * @return {Boolean}  Whether the stack can be picked up
 */
function canPickUpStack(stack, sourceSlot) {
	if (sourceSlot.type == 'tray') {
		if (stack.length == 1) {
			return true;
		} else {
			for (var i = 1; i < stack.length; i++) {
				var prev = stack[i - 1],
					curr = stack[i];
				if (prev.value && curr.value && prev.value == curr.value + 1 && prev.suit != curr.suit) {
					continue;
				} else {
					return false;
				}
			}
		}
		return true;
	} else if (sourceSlot.type == 'spare') {
		// once all dragons are stacked in there, you can't move it.
		return sourceSlot.cards.length != DRAGON_COUNT;
	}
}

/**
 * Finds whether the given stack can be put on the given destination.
 * The stack is assumed to be valid to pick up.
 *
 * The destination must be a numbered one. The stack bottom must be numbered, or undefined.
 * The destination must be numbered, one more than the stack bottom, and not the same suit.
 * @param  {Array[Card]} stack The list of cards which are picked up.
 * @param  {SLOT} destSlot
 * @param  {Card} dest  The card which the lowest of the stack will rest upon.
 * @return {Boolean} Whether the stack can be placed on it.
 */
function canPlaceStack(stack, destSlot, dest) {
	if (destSlot.type == 'tray') {
		if (stack.length === 0 || dest === undefined) {
			return true;
		} else {
			if (stack[0].value && dest.value) {
				return (stack[0].value + 1 == dest.value) && (stack[0].suit != dest.suit);
			} else {
				return false;
			}
		}
	} else if (destSlot.type == 'flower') {
		// only flower allowed in flower slot, except during debug
		return stack[0].special === SPECIAL.FLOWER || DEBUG === true;
	} else if (destSlot.type == 'spare') {
		// only 1 card manually placed in spare slot
		return destSlot.cards.length === 0 && stack.length == 1;
	} else if (destSlot.type == 'out') {
		// a single numbered card
		if (stack.length === 1 && stack[0].value) {
			if (dest === undefined) {
				// if empty, must be value '1' card.
				return stack[0].value == 1;
			} else {
				// otherwise, must be the next value
				return stack[0].value == dest.value + 1 && stack[0].suit == dest.suit;
			}
		}
	}
}

/**
 * Sorts the cards in a consistent order.
 * Modifies the given array.
 * @param  {Array[Card]} cards The array of cards to be sorted.
 */
function sortCards(cards) {
	cards.sort(function (a, b) {
		var aHas = typeof a.value !== 'undefined';
		var bHas = typeof b.value !== 'undefined';
		if (aHas && bHas) {
			if (a.value == b.value) {
				return a.suit.order - b.suit.order;
			} else {
				return a.value - b.value;
			}
		} else {
			if (aHas) {
				return -1;
			} else if (bHas) {
				return 1;
			} else {
				return a.special.order - b.special.order;
			}
		}
	});
}

/**
 * Sets up a new game with randomly placed cards.
 * @param  {Array[Card]} cards List of cards which will be placed.
 * @param  {HTMLElement} board The container for the cards.
 * @param  {Object} seed (optional) The random seed for shuffling the deck. If omitted, the time is used.
 */
function startNewGame(cards, board, seed) {
	clearInterval(looper);
	looper = undefined;
	isInVictory = false;

	// TODO: start cards face down in the flower slot, then move them into place.

	sortCards(cards);

	var truSeed = seed;
	// use time-based seed if there is no seed, or is an empty string.
	if (seed === undefined || (typeof seed === 'string' && seed.length === 0)) {
		truSeed = new Date().getTime();
	}
	// if input is a numeric string, convert to an integer ("123" and 123 behave differently)
	if (!isNaN(parseInt(truSeed, 10))) {
		truSeed = parseInt(truSeed, 10);
	}

	currentSeed = truSeed;

	Math.seedrandom(truSeed);

	// eslint-disable-next-line no-console
	console.log('Game id:', truSeed);

	shuffleArray(cards); // shuffle cards

	$('.card').finish().removeClass('card-reverse');
	$('.btn-dragon').data('complete', false);
	placeCardsInTray(cards, board, SLOTS.TRAY); // place cards
	$('.card').visible();
}

function updateWinCount() {
	if (useLocalStorage) {
		$('#win_count').text(localStorage.shenzhen_win_count);
	}
}

/**
 * Whether the victory screen is currently running.
 * @type {Boolean}
 */
var isInVictory = false;
var looper; // the interval identifier for the cards dropping in the victory screen.
/**
 * Runs the victory screen, where cards drop down the screen.
 */
function victoryScreen() {
	var cards = [];

	var stax = $('.slot-spare,.slot-flower,.slot-out');
	var foundThisRun = false;
	do {
		foundThisRun = false;
		for (var i = 0; i < stax.length; i++) {
			var childs = $(stax[i]).children();
			var searchI = $(stax[i]).data('search-i') === undefined ? (childs.length - 1) : $(stax[i]).data('search-i');
			if (searchI >= 0) {
				cards.push(childs[searchI]);
				foundThisRun = true;
				$(stax[i]).data('search-i', searchI - 1);
			}
		}
	} while (foundThisRun);
	stax.removeData('search-i');

	var row = 0;

	// each iteration, over time, take the first card and shunt it down.
	if (looper !== undefined) {
		clearInterval(looper);
	}
	looper = setInterval(function () {
		$(cards[row]).animate({
			top: parseInt($(cards[row]).css('top')) + 1000
		}, 1000);

		row++;
		if (row >= cards.length) {
			clearInterval(looper);
			looper = undefined;
			isInVictory = false;
		}
	}, 50);
}

/**
 * Loads the alternate stylesheet for when the images are missing.
 */
function loadAltStyle() {
	$('head').append('<link rel="stylesheet" type="text/css" href="css/noimages.css">');
}

function setColorblindMode(isColorblindMode) {
	var stylesheetId = 'colorblind-styles';

	if (isColorblindMode) {
		if (!$('#colorblind-styles').length) {
			$('head').append('<link id="' + stylesheetId + '" rel="stylesheet" type="text/css" href="css/colorblind.css">');
		}
	} else {
		$('#' + stylesheetId).remove();
	}

	if (useLocalStorage) {
		localStorage.shenzhen_colorblind = isColorblindMode;
	}
}

/**
 * Creates a stack of all cards including and stacked on top of the given card.
 * @param  {HTMLElement} cardElement The element for the card.
 * @return {Array[Card]}      An array of cards
 */
function getStackFromCardElement(cardElement) {
	var card = $(cardElement).data('card');

	var cardIndex = card.slot.cards.indexOf(card),
		cardLength = card.slot.cards.length;

	var stack = [];
	for (var i = cardIndex; i < cardLength; i++) {
		stack.push(card.slot.cards[i]);
	}

	return stack;
}

var cards;
var dragging = false;
$(document).ready(function () {

	if (useLocalStorage) {
		if (localStorage.shenzhen_win_count === undefined) {
			localStorage.shenzhen_win_count = 0;
		}
	}
	updateWinCount();

	var board = $('#cards');
	populateSlots(SLOTS, board);

	cards = makeDeck();

	// if there is a hash in the url upon load, load that as the seed.
	startNewGame(cards, board, location.hash.replace('#', ''));

	$('#newGame').click(function () {
		// Replace the current state with the previous game
		if (!location.hash) {
			history.replaceState('', document.title, window.location.pathname + window.location.search + '#' + currentSeed);
		}
		// clear the hash from the url.
		history.pushState('', document.title, window.location.pathname + window.location.search);

		startNewGame(cards, board);
	});

	$('#retryGame').click(function () {
		if (currentSeed !== null) {
			if (location.hash.replace('#', '') === currentSeed.toString()) {
				startNewGame(cards, board, location.hash.replace('#', ''));
			} else {
				location.hash = currentSeed;
				// Triggers hashchange
			}
		}
	});

	addEventListener("hashchange", function () {
		if (location.hash.replace('#', '')) {
			startNewGame(cards, board, location.hash.replace('#', ''));
		}
	});

	$('#playMusicButton').click(function () {
		music.play();
		if (music.currentTime > 0 && music.currentTime < 5) {
			music.currentTime = 0;
		}
		$('#playMusicButton').hide();
		$('#pauseMusicButton').show();
	});

	$('#pauseMusicButton').click(function () {
		music.pause();
		$('#playMusicButton').show();
		$('#pauseMusicButton').hide();
	});

	$('#toggleColorblind').change(function (event) {
		setColorblindMode(event.target.checked);
	});

	// When the alt style
	if (useLocalStorage) {
		if (localStorage.shenzhen_colorblind !== undefined && JSON.parse(localStorage.shenzhen_colorblind) === true) {
			$('#toggleColorblind').prop('checked', true);
			setColorblindMode(true);
		}
	}

	// Make the cards interactable
	$('.slot').droppable({
		drop: function (event, ui) {
			// drop is contingent on "accept", so this is a valid stack.
			var stack = getStackFromCardElement(ui.draggable);
			var slot = $(this).data('slot');

			// insert all from stack into the bottom of the slot.
			for (var i = 0; i < stack.length; i++) {
				insertCard(stack[i], slot, slot.cards.length);
			}
			onFieldUpdated();
		},
		accept: function (draggable) {
			var stack = getStackFromCardElement(draggable);
			var slot = $(this).data('slot');

			return canPlaceStack(stack, slot, slot.cards[slot.cards.length - 1]);
		},
		tolerance: 'pointer'
	});

	$('.card').draggable({
		'cursor': 'url(assets/cursor_normal.png) 40 40, default',
		'revert': 'invalid',
		'revertDuration': CARD_ANIMATION_SPEED,
		helper: function () {
			var cardSet = $('<div></div>');
			cardSet.css({
				'z-index': 100,
				'display': 'inline'
			});

			var card = $(this).data('card');
			var cardIndex = card.slot.cards.indexOf(card),
				cardLength = card.slot.cards.length;

			for (var i = cardIndex, height = 0; i < cardLength; i++, height++) {
				var e = card.slot.cards[i].element.clone();
				e.css({
					top: height * CARD_STACK_GAP,
					left: '',
				});
				cardSet.append(e);
			}
			return cardSet;
		},
		start: function (event, _ui) {
			var card = $(this).data('card');

			var stack = [];
			var cardIndex = card.slot.cards.indexOf(card),
				cardLength = card.slot.cards.length;
			var i;
			for (i = cardIndex; i < cardLength; i++) {
				stack.push(card.slot.cards[i]);
			}

			if (!card.element.is(':animated') && canPickUpStack(stack, card.slot)) {
				for (i = 0; i < stack.length; i++) {
					stack[i].element.invisible();
				}
				dragging = true;
			} else {
				event.stopPropagation();
				event.stopImmediatePropagation();
				event.preventDefault();
			}
		},
		stop: function (_event, _ui) {
			dragging = false;
			var card = $(this).data('card');

			var cardIndex = card.slot.cards.indexOf(card),
				cardLength = card.slot.cards.length;
			for (var i = cardIndex; i < cardLength; i++) {
				card.slot.cards[i].element.visible();
			}
		}
	});

	// detect failed image load
	var triggeredWarning = false;

	// prepare for a canary check for if we have images
	$('#canary').on('error', function (_data, _handler) {
		if (!triggeredWarning) {
			// eslint-disable-next-line no-console
			console.warn('Couldn\'t load an image from the original game. If you own SHENZHEN I/O, copy the game\'s "Content/textures/solitaire" folder into the "solitaire" directory of the cloned repository.');

			loadAltStyle();
		}
		triggeredWarning = true;
	});

	// start the canary check
	$('#canary').attr('src', 'solitaire/button_red_up.png');

	music = new Audio("solitaire/Solitaire.ogg");
	music.loop = true;
	$(music).on('canplay', function () {
		$('#playMusicButton').show();
		$(music).off('canplay');
	})
	$(music).on('error', function (_data, _handler) {
		console.warn('Couldn\'t load music from the original game. If you own SHENZHEN I/O, copy "Content/music/Solitaire.ogg" from the game into the "solitaire" directory of the cloned repository.');
	});

	$('html').keydown(function () { }); // UI breakpoint for debugging in Chrome

	if (DEBUG_STYLE) {
		loadAltStyle();
	}

});