/****************************************************************************************
                  Script to provide an API for managing card games
                  Written by Mark Wilton-Jones, 20/12/2005-8/1/2006
*****************************************************************************************

Please see http://www.howtocreate.co.uk/jslibs/ for details and a demo of this script
Please see http://www.howtocreate.co.uk/tutorials/jsexamples/solitaire.html for a demonstration
Please see http://www.howtocreate.co.uk/jslibs/termsOfUse.html for terms of use
_____________________________________________________________________________________________________

Card game core API documentation:

public class cardSet()
	Class representing a deck of cards.
cardSet.defaultCardNames
	Array of strings used as alternative text for card faces. Defaults to the standard 'Ace' to 'King'. Use
	cardSet.setCardNames to change this.
cardSet.defaultCardWord
	String used as alternative text on the back of the card. Defaults to 'Card'. Use cardSet.setCardNames to
	change this.
cardSet.imagePacks
	Reference to an imagePacks object, which contains a list of all available image packs that have been added
	using addImagePack (see the imagePacks definition for more information).
cardSet.playingCards
	Array of all cards in the deck.
cardSet.addCard(string: suit,integer: cardnumber,mixed: color)
	Adds the specified card into the deck.
	Suit should normally be one of 'spades', 'hearts', 'clubs', 'diamonds'.
	Cardnumber should be in the range 1-13 (unless you wish to create a joker - you may want to create this as
	card 14 in a new suit - remember to use changeCardNames as appropriate [add an extra card name on the end]).
	Color is an arbitrary value and is for your own use only.
cardSet.addImageCache(string: src)
	Adds an image into the cache, to ensure image changes run smoothly. If an image fails to load, an alert will
	be displayed, unless you set the global variable 'hideCardGameErrors' to true.
	addImageCache is called automatically when setting image packs.
cardSet.create52Cards(optional string array: suits)
	Creates a standard set of 52 cards, and adds them to the existing deck. No redrawing is performed. If you
	provide suits, it will use the first 4 strings to select the 4 suits. These will also be used by the images.
cardSet.forcePageRedraw()
	Attempts to force an entire page redraw in a variety of browsers. Use to avoid bad rendering bugs but do
	not overuse - it is expensive in terms of performance.
cardSet.redrawCards()
	Forces cards to redraw - can be used to avoid rendering bugs, or just to refresh after making a change.
cardSet.setCardNames(optional string: cardword,optional array: cardnames)
	Changes card names (used as alt text).
	The cardword should be the word used in place of the card back - default is 'Card'.
	Cardnames should be an array of strings with the names of all cards, normally from 'Ace' to 'King' (default).
cardSet.setCardSize(string: width,string: height)
	Sets the width and height styles of all card images. Cards will redraw if needed.
cardSet.setImagePack(string: imageset,string: backimage,string: extension)
	Tells the cards to use the images specified (must be called in any game before dealing).
	Can be called at any time during the game for an instant facelift. Cards will redraw if needed.
	Card faces will be created as; imageset + suit + number + extension
	Card backs will be created as; imageset + 'back' + backimage + extension
	Suit is one of 'spades', 'hearts', 'clubs', 'diamonds'.
	Number is 1-14.
cardSet.shuffleCards(optional integer: times)
	Sorts the cards in a random order within the cardSet.playingCards array. No redrawing is performed. If
	you pass an integer to it, it will sort that many times (in case the browser has a bad random number
	generator). Default is 3 times.

public class cardStack(mixed: type,mixed: index,bool: createDiv,optional string: stackText)
	A class representing a visual stacking of cards within the game area, for example, the deck of undealt
	cards in a game of solitaire. type and index are arbitrary, and can be used simply to keep a reference
	of the features of a specific stack. If createDiv is true, the hotspot property will be created as a
	div with position:absolute, and a className of 'hotspot'. If a hotspot is created, then any text you
	provide in stackText will be added to the hotspot.
cardStack.cardsInStack
	Array of all cards in the stack.
cardStack.hotspot
	A reference to a div element with position set to absolute.
cardStack.type
	The specified stack type.
cardStack.index
	The specified stack index.
cardStack.moveToStack(playingCard)
	Moves a given card into the current stack, and removes it from any existing stack. Note that
	cardStack.cardsInStack does not automatically collapse down when cards are removed from it. This is
	behaviourly synonymous to playingCard.moveToStack
cardStack.setStyles(left,top,zIndex,width,height,fontSize)
	Sets the appropriate styles on the card stack's hotspot.
cardStack.truncate(optional integer: length)
	Shortens the number of cards in the stack to the given length. If no length is given, the card stack
	will be shortened to remove all trailing empty cells.

public class imagePacks()
	Class representing an available set of images, including their sizes, so that appropriate size of
	image set can be chosen for the available space. Do not create instances of this class directly,
	they are created atomatically for each cardSet and can be accessed through cardSet.imagePacks.
imagePacks.availCombo
	Object with property names matching each combination of image set and back image. The property names are
	created as width+'x'+height+'|'+imageset+'|'+backimage (for use with storing and retrieving preferences).
	Each property references an image pack that match that image combination. Image packs are stored in
	object form with the properties 'imageset', 'backimages', 'extension', 'width', 'height', and 'name'
	(these will match the values passed to the addImagePack method).
imagePacks.availHeights
	Array of available card heights, added using imagePacks.addCardPack. Array will be sorted in descending
	order.
imagePacks.availWidths
	Array of available card widths, added using imagePacks.addCardPack. Array will be sorted in descending
	order.
imagePacks.heights
	Object with property names matching the sizes of the available cards. Each property references an array
	of image packs that match that size (in the same way as with imagePacks.availCombo). For example,
	imagePacks.heights[100][0] will reference the first image pack added with a height of 100.
imagePacks.widths
	Same as imagePacks.heights, but for widths instead of heights.
imagePacks.addImagePack(string: imageset,array: backimages,string: extension,integer: cardWidth,integer: cardHeight,string: name)
	Adds an image pack into the list of available image packs. The values of imageset and extension should be
	compatible with those used by cardSet.setImagePack. cardWidth and cardHeight are for use with
	getFittingImageSize. backimages should be an array of entries. Each entry should be an array with two
	cells: string backimage, string name. This should list all available card back images in this image set.
	Each should be compatible with the backimage value expected by cardSet.setImagePack. The name is a name
	that you want to refer to the image pack as (this is used only for your own reference, so multiple image
	packs may share the same name).
imagePacks.getFittingImageSize(bool: heightOrWidth,integer: size)
	Attempts to find the largest possible card size within the given size limit. The appropriate size of the
	card is returned. If none can be found, it returns the smallest available card size. If no card packs are
	available, it returns null. heightOrWidth should be true if you want to check heights, and false if you
	want to check widths.

public class playingCard(string: suit,integer: cardnumber,mixed: color,object: cardSet)
	Class representing a card in the deck. It is generally best to use cardSet.create52Cards,
	cardSet.addCard, or playingCard.changeCard instead of creating instances manually - if you do,
	you will need to add them to the cardSet.playingCards array.
playingCard.suit playingCard.number playingcard.color
	The values provided when creating the card.
playingCard.cardImage
	A reference to the card image. It will have display:block.
playingCard.cardStack
	A reference to the cardStack object that the card is attached to (can be changed manually if needed).
playingCard.positionOnStack
	The index of the card on the cardStack
playingCard.representation
	A reference to the div containing the card image. It will have position:absolute, and a className of
	'playingcard'.
playingCard.wayup
	Boolean true if the card is face up, false if it is face down.
playingCard.changeCard(string: suit,integer: cardnumber)
	Changes the card from its current suit and number to the new number specified. Cards will be redrawn
	as needed.
playingCard.moveToStack(object: cardStack)
	Removes a card from its current stack (if it is on a stack) and puts it onto the new one. Note that
	cardStack.cardsInStack does not automatically collapse down when cards are removed from it, so you
	should use cardStack.truncate when all changes have been made. No visual changes and redraws will occur.
playingCard.nextOnStack()
	Returns a reference to the next card (with a higher stack position) on the stack. Null or undefined
	if none.
playingCard.previousOnStack()
	Returns a reference to the previous card (with a lower stack position) on the stack. Null or
	undefined if none.
playingCard.inheritCardDesign()
	Picks up the card's current design as specified by cardSet.setBackImage or cardSet.setImagePack
	Card will be redrawn if needed.
playingCard.redrawCardImage()
	Forces a card to redraw - can be used to avoid rendering bugs, or just to refresh after making a change.
	If the card has not yet inherited a card design (inheritCardDesign), this method will fail silently.
playingCard.showFace(bool: face)
	Sets the card to be either face up or face down, and redraws if needed.
	true = face up, false = face down.
playingCard.setCardSize(string: width,string: height)
	Sets the CSS width and height properties of the card image. This does _not_ change the image used. Card
	will be redrawn if needed.
_____________________________________________________________________________________________________*/

/**********************************************
 A class representing the entire deck of cards
**********************************************/

function cardSet() {
	
	// Storage for card references
	this.playingCards = [];
	this.massiveImageCache = {};
	this.imagePacks = new imagePacks();

}

cardSet.prototype.defaultCardNames = ['Ace','2','3','4','5','6','7','8','9','10','Jack','Queen','King'];
cardSet.prototype.defaultCardWord = 'Card';

cardSet.prototype.toString = function () { return '[object cardSet]'; };

cardSet.prototype.addCard = function (oSuit,oNumber,oColour) {
	// Add a card to the deck
	this.playingCards[this.playingCards.length] = new playingCard(oSuit,oNumber,oColour,this);
};

cardSet.prototype.addImageCache = function (imUrl) {
	// Add an image into the cache
	if( !this.massiveImageCache[imUrl] ) {
		var oSet = this;
		this.massiveImageCache[imUrl] = new Image();
		this.massiveImageCache[imUrl].onerror = function () {
			if( !oSet.hasAlertedImageError ) {
				oSet.hasAlertedImageError = true;
				if( !window.hideCardGameErrors ) { alert('Warning: Card game image failed\n\nA card image failed to load - the card game may not play correctly:\n'+this.src+'\n\nNo more warnings will be shown for cards in this card set.'); }
			}
		};
		this.massiveImageCache[imUrl].src = imUrl;
	}
};

cardSet.prototype.setImagePack = function (oImageSet,oBackImage,oExtension) {
	// Set card images
	this.cardSet = oImageSet;
	this.imageExtension = oExtension;
	this.backImage = oImageSet+'back'+oBackImage+oExtension;
	this.addImageCache(this.backImage);
	for( var i = 0; i < this.playingCards.length; i++ ) {
		this.playingCards[i].inheritCardDesign();
	}
};

cardSet.prototype.shuffleCards = function (oTimes) {
	// Sorting function - based on the easier Knuth shuffle
	if( !oTimes ) { oTimes = 3; }
	for( var n = 0; n < oTimes; n++ ) {
		// Three times, just in case the browser's random number generator is not very good
		for( var i = 0; i < this.playingCards.length; i++ ) {
			this.playingCards[i].tmpShuffleSortingIndex = Math.random();
		}
		this.playingCards.sort( function (a,b) {
			// OmniWeb and older Safari insists that I return a whole number, not a fraction
			return ( ( b.tmpShuffleSortingIndex - a.tmpShuffleSortingIndex ) > 0 ) ? 1 : -1;
		} );
	}
	// Enable this for debugging
//	for( var i = 0, s=''; i < this.playingCards.length - 1; i++ ) { s+= this.playingCards[i].number + ' ' + this.playingCards[i].suit + '\n'; } alert(s);
};

cardSet.prototype.setCardSize = function (oWidth,oHeight) {
	// Set a nice width for the cards - any CSS width value is allowed
	for( var i = 0; i < this.playingCards.length; i++ ) {
		this.playingCards[i].setCardSize(oWidth,oHeight);
	}
};

cardSet.prototype.redrawCards = function () {
	// Redraws all cards (resets their images and alt text to the correct values)
	for( var i = 0; i < this.playingCards.length; i++ ) {
		this.playingCards[i].redrawCardImage();
	}
};

cardSet.prototype.setCardNames = function (oCardWord,oCardNames) {
	// Change the text representation of the cards
	if( !oCardWord ) { oCardWord = this.defaultCardWord; }
	if( !oCardNames ) { oCardNames = this.defaultCardNames; }
	this.cardWord = oCardWord;
	this.cardNames = oCardNames;
	this.redrawCards();
};

cardSet.prototype.forcePageRedraw = function () {
	// Force full document redraw
	document.body.className = document.body.className ? ( document.body.className + '' ) : '';
};

cardSet.prototype.create52Cards = function (cardSuits) {
	// Create 52 cards
	this.cardSuitNames = cardSuits ? cardSuits : ['spades','hearts','clubs','diamonds']
	for( var i = 0; i < this.cardSuitNames.length; i++ ) {
		for( var n = 1; n < 14; n++ ) {
			this.addCard(this.cardSuitNames[i],n,i%2,this);
		}
	}
}

/*****************************************
 A class representing a visual card stack
 - this can be extended by the game code
*****************************************/

function cardStack(oType,oIndex,oWithDiv,oDivText) {

	this.cardsInStack = [];
	this.type = oType;
	this.index = oIndex;
	if( oWithDiv ) {
		this.hotspot = document.createElement('div');
		this.hotspot.relatedObject = this;
		this.hotspot.className = 'hotspot';
		this.hotspot.style.position = 'absolute';
		if( oDivText ) { this.hotspot.appendChild(document.createTextNode(oDivText)); }
	}
	
};

cardStack.prototype.toString = function () { return '[object cardStack: type '+this.type+', index '+this.index+']'; };

cardStack.prototype.moveToStack = function (oCard) {
	// Moving is actually done in the oposite direction
	oCard.moveToStack(this);
};

cardStack.prototype.truncate = function (oLength) {
	// Truncate the cardsInStack array
	if( typeof(oLength) == typeof(0) ) {
		this.cardsInStack.length = oLength;
	} else {
		while( this.cardsInStack.length && !this.cardsInStack[this.cardsInStack.length-1] ) {
			this.cardsInStack.length--;
		}
	}
};

cardStack.prototype.setStyles = function (oLeft,oTop,zIndex,oWidth,oHeight,oFont) {
	// Set the position of the card stack
	this.leftPos = oLeft;
	this.topPos = oTop;
	this.hotspot.style.left = oLeft + 'px';
	this.hotspot.style.top = oTop + 'px';
	this.hotspot.style.zIndex = zIndex;
	this.hotspot.style.width = oWidth;
	this.hotspot.style.height = oHeight;
	this.hotspot.style.fontSize = oFont;
	this.hotspot.style.overflow = 'hidden';
};

/*******************************************************************************
 A class representing a set of available image packs, used to display the cards
*******************************************************************************/
  
function imagePacks() {
	this.availWidths = [];
	this.availHeights = [];
	this.widths = {};
	this.heights = {};
	this.packNames = {};
	this.availCombo = {};
}

imagePacks.prototype.toString = function () { return '[object imagePacks]'; };

imagePacks.prototype.addImagePack = function (oImageSet,oBackImages,oExtension,oWidth,oHeight,oName) {
	// Add an image back with size information
	if( !this.widths[oWidth] ) {
		this.availWidths[this.availWidths.length] = oWidth;
		this.widths[oWidth] = [];
	}
	if( !this.heights[oHeight] ) {
		this.availHeights[this.availHeights.length] = oHeight;
		this.heights[oHeight] = [];
	}
	var oStore = this.widths[oWidth][this.widths[oWidth].length] = this.heights[oHeight][this.heights[oHeight].length] =
		{imageset:oImageSet,backimages:oBackImages,extension:oExtension,width:oWidth,height:oHeight,name:oName,toString:function () { return '[private object imagePack: '+this.imageset+']'; }};
	for( var i = 0; i < oBackImages.length; i++ ) {
		this.availCombo[oWidth+'x'+oHeight+'|'+oImageSet+'|'+oBackImages[i][0]] = oStore;
	}
	var sortFunc = function ( a, b ) { return b - a; };
	this.availWidths.sort(sortFunc);
	this.availHeights.sort(sortFunc);
};

imagePacks.prototype.getFittingImageSize = function (oHeightWidth,oSize) {
	// Get the nearest image set that fits. If none fit, then get the first set up.
	var checkingList = oHeightWidth ? this.availWidths : this.availHeights;
	for( var i = 0; i < checkingList.length; i++ ) {
		if( checkingList[i] <= oSize ) { return checkingList[i]; }
	}
	return checkingList.length ? checkingList[checkingList.length-1] : null;
};

/****************************
 A class representing a card
****************************/

function playingCard(oSuit,oNumber,oColour,oCardSet) {

	// Initialise settings
	this.number = oNumber;
	this.suit = oSuit;
	this.color = oColour;
	this.wayup = false;
	this.cardSet = oCardSet;
	this.cardStack = null;
	this.positionOnStack = 0;

	// Create the card image and placeholder
	this.representation = document.createElement('div');
	this.representation.relatedObject = this;
	this.representation.style.position = 'absolute';
	this.representation.className = 'playingcard';
	this.cardImage = document.createElement('img');
	this.cardImage.style.display = 'block';
	this.representation.appendChild(this.cardImage);

}

playingCard.prototype.toString = function () { return '[object playingCard: '+this.number+' '+this.suit+']'; };

playingCard.prototype.moveToStack = function (oNewStack) {
	// Move onto another card stack
	if( this.cardStack ) {
		this.cardStack.cardsInStack[ this.positionOnStack ] = null;
	}
	this.cardStack = oNewStack;
	this.positionOnStack = oNewStack.cardsInStack.length;
	oNewStack.cardsInStack[this.positionOnStack] = this;
};

playingCard.prototype.nextOnStack = function () {
	// Like nextSibling but related to card stacks
	if( !this.cardStack ) { return null; }
	return this.cardStack.cardsInStack[ this.positionOnStack + 1 ];
};

playingCard.prototype.previousOnStack = function () {
	// Like previousSibling but related to card stacks
	if( !this.cardStack ) { return null; }
	return this.cardStack.cardsInStack[ this.positionOnStack - 1 ];
};

playingCard.prototype.inheritCardDesign = function () {
	// Get the new card set images
	this.faceImage = this.cardSet.cardSet+this.suit+this.number+this.cardSet.imageExtension;
	this.cardSet.addImageCache(this.faceImage);
	this.redrawCardImage();
};

playingCard.prototype.changeCard = function (oSuit,oNumber) {
	this.number = oNumber;
	this.suit = oSuit;
	this.inheritCardDesign();
};

playingCard.prototype.redrawCardImage = function () {
	// Set or change the image showing on the card face
	if( !this.faceImage || !this.cardSet.backImage ) { return; }
	// Bug in Firefox - alt attributes do not change unless they are made _before_ an SRC change
	this.cardImage.setAttribute('alt',this.wayup?(this.cardSet.cardNames[this.number-1]+' '+this.suit):this.cardSet.cardWord);
	this.cardImage.src = this.wayup ? this.faceImage : this.cardSet.backImage;
};

playingCard.prototype.showFace = function (oWhich) {
	// Used to flip a card over
	if( this.redrawNewImage != oWhich ) {
		this.wayup = oWhich;
		this.redrawCardImage();
	}
};

playingCard.prototype.setCardSize = function (oWidth,oHeight) {
	// Set the width of the card image
	this.cardImage.style.width = oWidth;
	this.cardImage.style.height = oHeight;
	this.representation.style.width = oWidth;
	this.representation.style.height = oHeight;
};