/****************************************************************************************
               Script to create a working game of solitaire (Klondike)
                  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 detailed instructions
Please see http://www.howtocreate.co.uk/jslibs/termsOfUse.html for terms of use
_____________________________________________________________________________________________________*/

/****************************
 The main card game handlers
****************************/

function solitaireGame(oUniqueName,oWorkspace,oDeck,oWastePile,oFoundation,oTableau,oCardWord,oCardNames,oSuits,youWin,oDeal,oOptions,
	prefDone,prefCanc,prefName,imgPref,dealPref,cardPref,backPref,deal1,deal3) {

	// Check for invalid configuration
	if( !window.playingCard ) { return; } // Already covered earlier
	if( !this.throwError ) {
		solitaireGame.prototype.throwError('SolitaireIncorrectCall','You must use the \'new\' keyword when creating a game of solitaire.');
	}
	if( !document.createElement || !document.childNodes || !document.createElement || !document.createElement('div') ) {
		this.throwError('SolitaireBrowserNotGoodEnough','The browser you are using does not have high enough DOM support to play solitaire.',true);
	}
	if( !oUniqueName || ( typeof(oUniqueName) != typeof('') ) || solitaireGame.prototype.gameList[oUniqueName] ) {
		this.throwError('SolitaireNoUniqueName',(solitaireGame.prototype.gameList[oUniqueName]?('Attempted to reuse the name: '+oUniqueName+'\n'):'')+
			'You must specify a unique name when creating a solitaire game.');
	}
	if( !oWorkspace || ( oWorkspace.nodeType != 1 ) ) {
		this.throwError('SolitaireNoWorkspaceParent','You must specify a parent element when creating a solitaire game.\nThe element must exist before you create the solitaireGame object.\nIf needed, use an onload listener, and use that to create the game.');
	}
	if( arguments.length > 2 ) {
		for( var i = 2, oStr = typeof(''); ( i < arguments.length ) && ( i < 21 ); i++ ) {
			if( ( i != 7 ) && ( i != 8 ) && arguments[i] && typeof(arguments[i]) != oStr ) {
				this.throwError('SolitaireInvalidString','Expected a string as parameter '+(i+1)+' when creating a solitaire game.');
			} else if( ( ( i == 7 ) || ( i == 8 ) ) && arguments[i] ) {
				if( ( typeof( arguments[i] ) != typeof([]) ) || ( arguments[i].length < ((i==7)?13:4) ) ) {
					this.throwError('SolitaireInvalidString','Expected an array containing '+((i==7)?'thirteen':'four')+' strings as parameter '+(i+1)+' when creating a solitaire game.');
				}
				for( var n = 0; n < ((i==7)?13:4); n++ ) {
					if( !arguments[i][n] || ( typeof(arguments[i][n]) != oStr ) ) {
						this.throwError('SolitaireInvalidString','Expected a string as entry '+(n+1)+' of the array passed as parameter '+(i+1)+' when creating a solitaire game.');
					}
				}
			}
		}
	}

	// Prepare the cards
	solitaireGame.prototype.gameList[oUniqueName] = this;
	this.name = oUniqueName;
	this.cheatsAllowed = true;
	this.cards = new cardSet();
	this.cards.setCardNames(oCardWord,oCardNames);
	this.cards.create52Cards(oSuits)
	this.cardStacks = [];
	this.cardStacks[0] = new cardStack(0,0,true,oDeck?oDeck:'Deck');
	this.cardStacks[1] = new cardStack(1,1,true,oWastePile?oWastePile:'Waste pile');
	for( var i = 2; i < 6; i++ ) {
		this.cardStacks[i] = new cardStack(2,i,true,oFoundation?oFoundation:'Foundation');
	}
	for( var i = 6; i < 13; i++ ) {
		this.cardStacks[i] = new cardStack(3,i,true,oTableau?oTableau:'Tableau');
	}
	this.dealNum = ( this.solitairepref[this.name+'deal'] == '3' ) ? 3 : 1;

	this.strings = {};
	this.strings.youWin = youWin ? youWin : 'Congratulations, you won! Deal again?';
	this.strings.prefDone = prefDone ? prefDone : 'Done';
	this.strings.prefCanc = prefCanc ? prefCanc : 'Cancel';
	this.strings.prefName = prefName ? prefName : 'Options';
	this.strings.imgPref = imgPref ? imgPref : 'Images for size ';
	this.strings.dealPref = dealPref ? dealPref : 'Dealing';
	this.strings.cardPref = cardPref ? cardPref : 'Card set:';
	this.strings.backPref = backPref ? backPref : 'Image back:';
	this.strings.deal1 = deal1 ? deal1 : 'Deal 1';
	this.strings.deal3 = deal3 ? deal3 : 'Deal 3';

	// Set up handlers
	for( var i = 0; i < this.cards.playingCards.length; i++ ) {
		this.cards.playingCards[i].game = this;
		this.cards.playingCards[i].representation.onmousedown = this.handleMousedownOnCards;
		this.cards.playingCards[i].representation.onclick = this.handleClicksOnCards;
		this.cards.playingCards[i].representation.ondblclick = this.handleDoubleClicksOnCards;
		this.cards.playingCards[i].representation.ondragstart = function () { return false; };
		if( window.opera ) {
			// Make spatnav work better
			// Still not perfect since it has trouble selecting the right elements with them all so close together
			this.cards.playingCards[i].representation.onfocus = function () {
				if( this.relatedObject == this.relatedObject.cardStack.cardsInStack[this.relatedObject.cardStack.cardsInStack.length-1] ||
					( this.relatedObject.cardStack.type == 3 && this.wayup ) ) {
					this.style.border = '1px solid #ff0';
					this.style.margin = '-1px 0px 0px -1px';
				}
			};
			this.cards.playingCards[i].representation.onblur = function () { this.style.border = ''; this.style.margin = ''; };
		}
	}
	for( var i = 0; i < this.cardStacks.length; i++ ) {
		this.cardStacks[i].game = this;
		this.cardStacks[i].hotspot.onclick = this.handleClicksOnHotspots;
	}

	// Produce a div with known size, so we can always guarantee that the parent element is a parent container
	this.workspace = document.createElement('div');
	this.workspace.style.position = 'relative';
	this.workspace.className = 'workspace';
	// Give it some initial content so IE 5.x can get the offsetWidth right
	this.workspace.appendChild(document.createTextNode(' '));
	if( navigator.product == 'Gecko' && navigator.taintEnabled && !this.cardStacks.filter ) {
		// Firefox 1.0 does not treat text nodes as content
		this.workspace.appendChild(document.createElement('br'));
		this.workspace.appendChild(document.createTextNode(' '));
	}
	oWorkspace.appendChild(this.workspace);

	// Buttons to activate prefs, and re-deal
	var relatedGame = this;
	this.prefsButs = document.createElement('div');
	this.prefsButs.style.position = 'absolute';
	this.prefsButs.style.zIndex = '2';
	this.prefsButs.className = 'extrabuttons';
	var dealDiv = document.createElement('div'),
		optDiv = document.createElement('div');
	this.dealimg = document.createElement('img'),
	this.optimg = document.createElement('img');
	this.dealimg.setAttribute('alt',this.dealimg.title = (oDeal?oDeal:'Deal'));
	this.optimg.setAttribute('alt',this.optimg.title = (oOptions?oOptions:'Options'));
	dealDiv.appendChild(this.dealimg);
	optDiv.appendChild(this.optimg);
	this.prefsButs.appendChild(dealDiv);
	if( !( window.ActiveXObject && navigator.platform.indexOf( 'Mac' ) + 1 && window.ScriptEngine && ScriptEngine() == 'JScript' ) && !( window.opera && !window.XMLHttpRequest ) ) {
		// IE Mac will crash if it tries to do this much DOM manipulation
		// Opera 7.x (fixed in 8) does not show the prefs but halts the game
		// There is no workaround, so the options button will simply not appear in IE Mac or Opera 7.x
		this.prefsButs.appendChild(optDiv);
	}
	this.dealimg.style.display = 'block';
	this.optimg.style.display = 'block';
	dealDiv.onclick = function () {
		if( !relatedGame.ondeal || relatedGame.ondeal({type:'deal',target:relatedGame,currentTarget:relatedGame}) ) {
			relatedGame.startGame();
		}
	};
	optDiv.onclick = function () {
		if( !relatedGame.onshowprefs || relatedGame.onshowprefs({type:'showprefs',target:relatedGame,currentTarget:relatedGame,cardSet:relatedGame.usingCards,cardBack:relatedGame.cardBack,dealNum:relatedGame.dealNum}) ) {
			relatedGame.showPrefs();
		}
	};

	// WebCore 1.0 and 1.1 ( currently only affecting OmniWeb) is unable to remove or change outlines
	this.cantDoOutline = !window.ActiveXObject && !navigator.taintEnabled && document.childNodes && !window.XMLHttpRequest;

}

solitaireGame.prototype.gameList = {};

solitaireGame.prototype.throwError = function (oName,oMessage,oSurpress) {
	// Throw an error and alert if needed
	if( !window.hideCardGameErrors && !oSurpress ) { alert('Fatal error: '+oName+'\n\n'+oMessage); }
	throw({name:oName,message:oMessage});
};

solitaireGame.prototype.setCheatMode = function (oMode) { this.cheatsAllowed = oMode; };

solitaireGame.prototype.toString = function () { return '[object solitaireGame: '+this.name+']'; };

solitaireGame.prototype.addImagePack = function (oImageSet,oBackImages,oExtension,oWidth,oHeight,oName) {
	// Used to make life easier for users :)
	if( !window.playingCard ) { return; } // Already covered earlier
	var oStr = typeof(''), oNum = typeof(0), oArr = typeof([]);
	if( typeof( oImageSet ) != oStr ) {
		this.throwError('SolitaireInvalidImagePack','Expected a string as first parameter to addImagePack method.');
	}
	if( typeof( oBackImages ) != oArr || !oArr.length ) {
		this.throwError('SolitaireInvalidImagePack','Expected an array of back images as second parameter to addImagePack method.');
	}
	for( var i = 0; i < oBackImages.length; i++ ) {
		if( typeof( oBackImages[i] ) != oArr || oBackImages[i].length > 2 || typeof( oBackImages[i][0] ) != oStr || typeof( oBackImages[i][1] ) != oStr ) {
			this.throwError('SolitaireInvalidImagePack','Back image number '+(i+1)+' passed to addImagePack method is not in the correct format.\nExpected an array containing two strings.');
		}
	}
	if( typeof( oExtension ) != oStr ) {
		this.throwError('SolitaireInvalidImagePack','Expected a string as third parameter to addImagePack method.');
	}
	if( typeof( oWidth ) != oNum || oWidth < 1 || oWidth != Math.floor( oWidth ) ) {
		this.throwError('SolitaireInvalidImagePack','Expected a positive integer as fourth parameter to addImagePack method.');
	}
	if( typeof( oHeight ) != oNum || oHeight < 1 || oHeight != Math.floor( oHeight ) ) {
		this.throwError('SolitaireInvalidImagePack','Expected a positive integer as fifth parameter to addImagePack method.');
	}
	if( typeof( oName ) != oStr ) {
		this.throwError('SolitaireInvalidImagePack','Expected a string as sixth parameter to addImagePack method.');
	}
	this.cards.imagePacks.addImagePack(oImageSet,oBackImages,oExtension,oWidth,oHeight,oName);
};

solitaireGame.prototype.checkForWinner = function () {
	// Check if all foundation stacks are full
	for( var i = 2; i < 6; i++ ) {
		if( this.cardStacks[i].cardsInStack.length != 13 ) { return; }
	}
	// They won
	this.gameInPlay = false;
	var theGame = this;
	setTimeout(function () {
		if( theGame.ongamewon ) {
			if( theGame.ongamewon({type:'gamewon',target:theGame,currentTarget:theGame}) ) {
				theGame.startGame();
			}
		} else if( confirm(theGame.strings.youWin) ) { theGame.startGame(); }
	},10);
};

solitaireGame.prototype.startGame = function (oInstantWin) {

	// Start or restart a game

	if( !window.playingCard ) { return; } // Already covered earlier
	if( !this.cards.imagePacks.availWidths.length ) {
		this.throwError('SolitaireNoImagePackSpecified','You must specify at least one image pack before starting a game of solitaire.');
	}

	if( this.showingPrefs ) { return; }

	if( this.downOnCard || this.moveOnCard ) {
		this.clearMouseEvents();
	}
	this.doingSpatNav = null;

	// Deal the cards (move them onto the correct stacks)
	for( var i = 0; i < this.cardStacks.length; i++ ) {
		// Reset stacks so it works for re-deal
		this.cardStacks[i].cardsInStack.length = 0;
	}
	for( var i = 0; i < this.cards.playingCards.length; i++ ) {
		// Reset cards so they do not mess up the stack on re-deal
		this.cards.playingCards[i].cardStack = null;
	}
	if( oInstantWin ) {
		// Perform an instant win, for testing purposes
		for( var i = 2, curcard = 0; i < 6; i++ ) {
			for( var j = 0; j < 13; j++ ) {
				this.cards.playingCards[curcard].showFace(true);
				this.cards.playingCards[curcard++].moveToStack(this.cardStacks[i]);
			}
		}
		this.cards.shuffleCards();
	} else {
		this.cards.shuffleCards();
		for( var i = 0, curcard = 0; i < 7; i++ ) {
			for( var j = 0; j < i; j++ ) {
				this.cards.playingCards[curcard].showFace(false);
				this.cards.playingCards[curcard++].moveToStack(this.cardStacks[i + 6]);
			}
			this.cards.playingCards[curcard].showFace(true);
			this.cards.playingCards[curcard++].moveToStack(this.cardStacks[i + 6]);
		// Enable this for debugging
//		for( var j = 0, s = ''; j < this.cardStacks[i + 6].cardsInStack.length; j++ ) { s += '\n'+this.cardStacks[i + 6].cardsInStack[j].number+' '+this.cardStacks[i + 6].cardsInStack[j].suit+' face '+(this.cardStacks[i + 6].cardsInStack[j].wayup?'up':'down'); } alert('stack '+(i+1)+s);
		}
		for( var i = curcard; i < this.cards.playingCards.length; i++ ) {
			this.cards.playingCards[i].showFace(false);
			this.cards.playingCards[i].moveToStack(this.cardStacks[0]);
		}
		// Enable this for debugging
//		for( var j = 0, s = ''; j < this.cardStacks[0].cardsInStack.length; j++ ) { s += '\n'+this.cardStacks[0].cardsInStack[j].number+' '+this.cardStacks[0].cardsInStack[j].suit+' face '+(this.cardStacks[0].cardsInStack[j].wayup?'up':'down'); } alert('undealt deck'+s);
	}

	// Display the cards where they belong
	this.usingCards = null;
	this.setAppropriateSize();

	// Reposition after a resize change (only after they stop, in order to reduce redraw)
	if( !this.resizeFix && this.cards.imagePacks.availWidths.length > 1 ) {
		var obRef = this, resizeTimeout;
		this.resizeFix = function () {
			if( resizeTimeout ) { clearTimeout(resizeTimeout); }
			resizeTimeout = setTimeout(function () {
				obRef.setAppropriateSize();
			},200);
		};
		if( window.addEventListener ) {
			window.addEventListener('resize',this.resizeFix,false)
		} else if( window.attachEvent ) {
			window.attachEvent('onresize',this.resizeFix)
		}
	}

	this.gameInPlay = true;
	if( oInstantWin ) { this.checkForWinner(); }

};

solitaireGame.prototype.setAppropriateSize = function (oNotAgain) {

	if( this.downOnCard || this.moveOnCard ) {
		this.clearMouseEvents();
	}

	// Pick the most appropriate card set
	var availW = this.workspace.offsetWidth;
	var maxWidth = availW / 8;
	var cardSize = this.cards.imagePacks.getFittingImageSize(true,maxWidth);
	var cardPref = this.solitairepref[this.name+'x'+cardSize], usingCards;
	if( cardPref && ( usingCards = this.cards.imagePacks.availCombo[cardPref] ) ) {
		cardPref = cardPref.split('|')[2];
	} else {
		usingCards = this.cards.imagePacks.widths[cardSize][0];
		cardPref = usingCards.backimages[0][0];
	}
	this.workspace.style.height = ( usingCards.height * 4.1 ) + 'px';

	// Try to avoid scrollbars overlapping cards if the window is within a scrollbar's width of a size change
	if( !oNotAgain && ( this.workspace.offsetWidth < availW ) ) {
		this.setAppropriateSize(true);
		return;
	}

	// Avoid recalculating if nothing has changed
	if( this.usingCards == usingCards ) { return; }
	this.usingCards = usingCards;
	this.cardBack = cardPref;

	var fonts = Math.round(usingCards.height/10) + 'px';

	// Set the card stack positions
	this.cardStacks[0].setStyles(Math.round(usingCards.width / 8 ),Math.round(usingCards.height / 10 ),1,usingCards.width+'px',usingCards.height+'px',fonts);
	if( this.cardStacks[0].hotspot.parentNode != this.workspace ) {
		this.workspace.appendChild(this.cardStacks[0].hotspot);
	}
	this.cardStacks[1].setStyles(Math.round(usingCards.width / 4 ) + usingCards.width,Math.round(usingCards.height / 10 ),1,usingCards.width+'px',usingCards.height+'px',fonts);
	if( this.cardStacks[1].hotspot.parentNode != this.workspace ) {
		this.workspace.appendChild(this.cardStacks[1].hotspot);
	}
	for( var i = 2; i < 6; i++ ) {
		this.cardStacks[i].setStyles(( Math.round(usingCards.width / 8 ) * ( i + 2 ) ) + ( usingCards.width * ( i + 1 ) ),Math.round(usingCards.height / 10 ),1,usingCards.width+'px',usingCards.height+'px',fonts);
		if( this.cardStacks[i].hotspot.parentNode != this.workspace ) {
			this.workspace.appendChild(this.cardStacks[i].hotspot);
		}
	}
	for( var i = 6; i < 13; i++ ) {
		this.cardStacks[i].setStyles(( Math.round(usingCards.width / 8 ) * ( i - 5 ) ) + ( usingCards.width * ( i - 6 ) ),Math.round(usingCards.height / 5 ) + usingCards.height,1,usingCards.width+'px',usingCards.height+'px',fonts);
		if( this.cardStacks[i].hotspot.parentNode != this.workspace ) {
			this.workspace.appendChild(this.cardStacks[i].hotspot);
		}
	}
	this.prefsButs.style.left = Math.floor( ( 23 / 8 ) * usingCards.width ) + 'px';
	this.prefsButs.style.top = Math.round( usingCards.height / 10 ) + 'px';
	this.dealimg.style.width = this.optimg.style.width = Math.ceil( usingCards.width / 2 ) + 'px';
	var buttonHeight = Math.ceil( ( 9 / 20 ) * usingCards.height );
	this.dealimg.style.height = this.optimg.style.height = buttonHeight + 'px';
	this.dealimg.src = this.usingCards.imageset + 'deal' + this.usingCards.extension;
	this.optimg.src = this.usingCards.imageset + 'options' + this.usingCards.extension;
	this.optimg.parentNode.style.marginTop = ( usingCards.height - ( 2 * buttonHeight ) ) + 'px';
	this.prefsButs.style.fontSize = fonts;
	if( this.prefsButs.parentNode != this.workspace ) {
		this.workspace.appendChild(this.prefsButs);
	}

	// Set the size and position of each card
	this.cards.setCardSize(usingCards.width+'px',usingCards.height+'px');
	this.cards.setImagePack(usingCards.imageset,cardPref,usingCards.extension);
	for( var i = 0; i < this.cards.playingCards.length; i++ ) {
		this.cards.playingCards[i].wasDragged = false;
		this.cards.playingCards[i].representation.style.fontSize = fonts;
		this.cards.playingCards[i].autoPositionSolitaire();
		if( this.cards.playingCards[i].representation.parentNode != this.workspace ) {
			this.workspace.appendChild(this.cards.playingCards[i].representation);
		}
	}

};

/**********************************
 Aditions to playing card handlers
**********************************/

if( !window.playingCard ) {
	solitaireGame.prototype.throwError('SolitaireCardGameCoreNotAvailable','Could not find the CardGameCore objects.\nThe cardgamecore.js file must be included BEFORE solitaire.js.');
}

playingCard.prototype.autoPositionSolitaire = function () {
	// Set the position of the card stack
	this.representation.style.zIndex = this.positionOnStack + 2;
	if( this.representation.style.opacity && this.representation.style.opacity != '1' ) {
		if( !this.game.cantDoOutline ) {
			this.representation.style.outline = 'none';
		}
		this.representation.style.opacity = '1';
		this.representation.style.MozOpacity = '1';
		this.representation.style.filter = 'alpha(opacity=100)';
		if( this.representation.style.setProperty ) {
			// Safari 1.1
			try { tmpCard.representation.style.setProperty('-khtml-opacity','1'); } catch(e) {}
		}
	}
	this.representation.style.left = this.cardStack.leftPos +
		( ( this.cardStack.type == 1 ) ? Math.round( this.dealNum * ( this.game.usingCards.width / 4 ) ) : 0 ) + 'px';
	this.representation.style.top = this.cardStack.topPos +
		( ( this.cardStack.type == 3 ) ? Math.round( this.positionOnStack * ( this.game.usingCards.height / 10 ) ) : 0 ) + 'px';
};

playingCard.prototype.dragIsStartingSolitaire = function () {
	// Make sure it (and those stacked on top of it) stacks above all other cards when dragging
	this.representation.style.zIndex = 21 + this.positionOnStack;
	if( this.nextOnStack() ) { this.nextOnStack().dragIsStarting(); }
};

playingCard.prototype.dragHasStoppedSolitaire = function () {
	// Reset stacking
	this.autoPositionSolitaire();
	if( this.nextOnStack() ) { this.nextOnStack().dragHasStopped(); }
};

playingCard.prototype.moveToStackIfPossible = function (oStack,oCheat) {
	// Check if the card is allowed to move stacks, move it if possible, and return true if it was permitted
	var currentStack = this.cardStack, canMove = false;
	oCheat = oCheat && this.game.cheatsAllowed;
	if( oStack.type == 2 ) {
		if( this.nextOnStack() ) {}
		else if( ( this.number == 1 ) && ( oStack.cardsInStack.length == 0 ) ) { canMove = true; }
		else if( ( this.number != 1 ) && oStack.cardsInStack.length && ( oStack.cardsInStack[oStack.cardsInStack.length-1].number == this.number - 1 ) && ( oStack.cardsInStack[oStack.cardsInStack.length-1].suit == this.suit ) ) { canMove = true; }
	} else if( oStack.type == 3 ) {
		if( oCheat && ( oStack.cardsInStack[oStack.cardsInStack.length-1] != this ) ) { canMove = true; }
		if( oStack.cardsInStack.length == 0 ) {
			if( this.number == 13 ) { canMove = true; }
			oCheat = false;
		} else {
			if( oStack.cardsInStack[oStack.cardsInStack.length-1].wayup && ( oStack.cardsInStack[oStack.cardsInStack.length-1].number == this.number + 1 ) && ( oStack.cardsInStack[oStack.cardsInStack.length-1].color != this.color ) ) { canMove = true; }
			oCheat = false;
		}
	}

	if( canMove && this.game.onmovecard && !this.game.onmovecard({type:'movecard',target:this,currentTarget:this,relatedTarget:oStack,cheat:(oCheat&&(oStack.type==3))?true:false}) ) {
		canMove = false;
	}

	if( canMove ) {
		var eachCard = this, nextCard;
		while(eachCard) {
			nextCard = eachCard.nextOnStack();
			eachCard.moveToStack(oStack);
			eachCard.autoPositionSolitaire();
			eachCard = nextCard;
		}
		currentStack.truncate();
		if( this.game.doingSpatNav && window.opera ) { this.game.workspace.className += ''; } //outline redraw fix (spatnav only)
		this.game.checkForWinner();
		return true;
	}
	return false;

};

playingCard.prototype.cleanUpSpatNav = function () {
	var tmpEl = this;
	while( tmpEl ) {
		tmpEl.autoPositionSolitaire();
		tmpEl = tmpEl.nextOnStack();
	}
	this.game.doingSpatNav = null;
	if( window.opera ) { this.game.workspace.className += ''; } //outline redraw fix
};

/***************
 Event handlers
***************/

solitaireGame.prototype.handleClicksOnHotspots = function(e) {
	if( !this.relatedObject.game.gameInPlay ) { return; }
	if( this.downOnCard || this.moveOnCard ) {
		this.clearMouseEvents();
	}
	if( this.relatedObject.game.doingSpatNav ) {
		// End a spatnav move attempt 
		if( !e ) { e = window.event; }
		if( !this.relatedObject.game.doingSpatNav.moveToStackIfPossible(this.relatedObject,e.altKey) ) {
			this.relatedObject.game.doingSpatNav.cleanUpSpatNav();
		}
		this.relatedObject.game.doingSpatNav = null;
	} else if( ( this.relatedObject.type == 0 ) && ( this.relatedObject.cardsInStack.length == 0 ) ) {
		// Put cards from the waste pile back on the deck
		if( this.relatedObject.game.onresetdeck && !this.relatedObject.game.onresetdeck({type:'resetdeck',target:this.relatedObject,currentTarget:this.relatedObject,relatedTarget:this.relatedObject.game.cardStacks[1]}) ) { return; }
		for( var i = this.relatedObject.game.cardStacks[1].cardsInStack.length - 1, thisCard; i >= 0; i-- ) {
			thisCard = this.relatedObject.game.cardStacks[1].cardsInStack[i];
			thisCard.showFace(false);
			thisCard.moveToStack(this.relatedObject);
			thisCard.autoPositionSolitaire();
		}
		this.relatedObject.game.cardStacks[1].truncate();
	}
};

solitaireGame.prototype.handleDoubleClicksOnCards = function() {
	// Ensure only the correct cards can be double clicked
	if( !this.relatedObject || !this.relatedObject.game.gameInPlay ) { return; }
	if( this.downOnCard || this.moveOnCard ) {
		this.clearMouseEvents();
	}
	if( this.relatedObject.game.doingSpatNav ) {
		this.relatedObject.game.doingSpatNav.cleanUpSpatNav();
	}
	if( !this.relatedObject.wayup ) { return; }
	if( this.relatedObject != this.relatedObject.cardStack.cardsInStack[this.relatedObject.cardStack.cardsInStack.length-1] ) { return; }
	this.relatedObject.wasDragged = true;
	var relObj = this.relatedObject;
	setTimeout(function () { relObj.wasDragged = false; },20); //in case browser fires dblclick before click
	if( this.relatedObject.cardStack.type == 2 ) { return; }
	var currentStack = this.relatedObject.cardStack;
	for( var i = 2; i < 6; i++ ) {
		if( this.relatedObject.moveToStackIfPossible(this.relatedObject.game.cardStacks[i]) ) { return; }
	}
};

solitaireGame.prototype.handleClicksOnCards = function (e) {

	if( !this.relatedObject.game.gameInPlay ) { return; }

	if( this.relatedObject.game.downOnCard ) {
		this.relatedObject.game.clearMouseEvents();
	} else if( this.relatedObject.game.moveOnCard ) {
		return;
	}

	// Click is generally only important on cards that are at the top of their stack (except with spatnav)
 	var ontop = ( this.relatedObject == this.relatedObject.cardStack.cardsInStack[this.relatedObject.cardStack.cardsInStack.length-1] );
	if( !ontop && ( ( this.relatedObject.cardStack.type != 3 ) || !this.relatedObject.wayup ) ) { return; }

	if( this.relatedObject.game.doingSpatNav ) {
		// End a spatnav move attempt
		if( !e ) { e = window.event; }
		if( !this.relatedObject.game.doingSpatNav.moveToStackIfPossible(this.relatedObject.cardStack,e.altKey) ) {
			this.relatedObject.game.doingSpatNav.cleanUpSpatNav();
		}
		this.relatedObject.game.doingSpatNav = null;
	} else if( ontop && ( this.relatedObject.cardStack.type == 0 ) ) {
		// Dealing cards onto the wastepile
		var eachCard = [
			this.relatedObject,
			( this.relatedObject.cardStack.cardsInStack.length > 1 ) ? this.relatedObject.previousOnStack() : null,
			( this.relatedObject.cardStack.cardsInStack.length > 2 ) ? this.relatedObject.previousOnStack().previousOnStack() : null
		];
		if( this.relatedObject.game.onwastepile && !this.relatedObject.game.onwastepile({type:'wastepile',target:this.relatedObject,currentTarget:this.relatedObject,relatedTarget:this.relatedObject.game.cardStacks[1],threeCards:eachCard,dealNum:this.relatedObject.game.dealNum}) ) { return; }
		for( var i = 0; i < this.relatedObject.game.dealNum; i++ ) {
			if( eachCard[i] ) {
				eachCard[i].dealNum = i;
				eachCard[i].moveToStack(this.relatedObject.game.cardStacks[1]);
				eachCard[i].showFace(true);
				eachCard[i].autoPositionSolitaire();
			}
		}
		this.relatedObject.game.cardStacks[0].truncate();
	} else if( ontop && ( this.relatedObject.wayup == false ) && ( this.relatedObject.cardStack.type == 3 ) ) {
		// Flipping cards over when they reach the top of a tableau
		if( this.relatedObject.game.onflipcard && !this.relatedObject.game.onflipcard({type:'flipcard',target:this.relatedObject,currentTarget:this.relatedObject}) ) { return; }
		this.relatedObject.showFace(true);
	} else {
		if( this.relatedObject.wasDragged ) {
			this.relatedObject.wasDragged = false;
			return;
		}
		// Start a spatnav move attempt 
		this.relatedObject.game.doingSpatNav = this.relatedObject;
		var tmpCard = this.relatedObject;
		while( tmpCard ) {
			if( !this.relatedObject.game.cantDoOutline ) {
				// WebCore 1.0 and 1.1 ( currently only affecting OmniWeb) is unable to remove or change outlines
				tmpCard.representation.style.outline = '2px solid';
			}
			tmpCard.representation.style.opacity = '0.4';
			tmpCard.representation.style.MozOpacity = '0.4';
			tmpCard.representation.style.filter = 'alpha(opacity=40)';
			if( tmpCard.representation.style.setProperty ) {
				// Safari 1.1
				try { tmpCard.representation.style.setProperty('-khtml-opacity','0.4'); } catch(e) {}
			}
			tmpCard = tmpCard.nextOnStack();
		}
		if( window.opera ) { this.relatedObject.game.workspace.className += ''; } //outline redraw fix
	}

};

solitaireGame.prototype.checkBrowserScrollBug = function (oScroll) {
	//compensate for bugs in older versions of current browsers (all now use pageX/Y)
	if( window.navigator.userAgent.indexOf( 'Opera' ) + 1 ) { return 0; }
	if( window.ScriptEngine && ScriptEngine().indexOf( 'InScript' ) + 1 ) { return 0; }
	if( window.navigator.vendor == 'KDE' ) { return 0; }
	return oScroll;
}

solitaireGame.prototype.handleMousedownOnCards = function (e) {

	// Do all the drag/drop stuff - this part is _not_ easy, especially since it must not be confused with clicks
	this.relatedObject.wasDragged = false;
	if( !this.relatedObject.game.gameInPlay ) { return; }
	if( solitaireGame.prototype.downOnSolitaireCard ) { return; }
	if( this.relatedObject.game.doingSpatNav ) { return; }

	// Only the main mouse button can be used for dragging
	if( !e ) { e = window.event; }
	if( !e.which ) { e.which = e.button; }
	if( e.which > 1 ) { return; }
	if( this.relatedObject.game.downOnCard || this.relatedObject.game.moveOnCard ) {
		this.relatedObject.game.clearMouseEvents();
	}

	// Cards that can be dragged are:
	//  - Cards that are on the top of stack types 1 or 2
	//  - Cards that are face up on stack type 3
	var ontop = ( this.relatedObject == this.relatedObject.cardStack.cardsInStack[this.relatedObject.cardStack.cardsInStack.length-1] );

	if( !( ontop && this.relatedObject.cardStack.type && ( this.relatedObject.cardStack.type < 3 ) ) && !( this.relatedObject.wayup && ( this.relatedObject.cardStack.type == 3 ) ) ) { return; }

	// JavaScript limitation - I want to be able to remove listeners from any method, but they run in scope of document.
	// I need to be able to get back into the correct scope without restricting access to the methods (otherwise I could
	// add them into the current scope). So I add an ugly global variable, and make sure that only one solitaire instance
	// uses it at any time. I could not find a better solution.
	solitaireGame.prototype.downOnSolitaireCard = this.relatedObject.game.downOnCard = this.relatedObject;

	var oNum = typeof(0);
	this.relatedObject.eventInformation = [
		( typeof( e.pageX ) == oNum ) ? e.pageX : ( e.clientX + this.relatedObject.game.checkBrowserScrollBug( document.documentElement.scrollLeft ? document.documentElement.scrollLeft : document.body.scrollLeft ) ),
		( typeof( e.pageY ) == oNum ) ? e.pageY : ( e.clientY + this.relatedObject.game.checkBrowserScrollBug( document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop ) ),
		parseInt(this.style.left),
		parseInt(this.style.top)
	];

	if( document.addEventListener ) {
		document.addEventListener('mouseup',this.relatedObject.game.handleMouseupOnCards,false);
		document.addEventListener('mousemove',this.relatedObject.game.handleMousemoveOnCards,false);
	} else if( document.attachEvent ) {
		document.attachEvent('onmouseup',this.relatedObject.game.handleMouseupOnCards);
		document.attachEvent('onmousemove',this.relatedObject.game.handleMousemoveOnCards);
	} else {
		this.relatedObject.game.oldmouseup = document.onmouseup;
		this.relatedObject.game.oldmousemove = document.onmousemove;
		document.onmouseup = this.relatedObject.game.handleMouseupOnCards;
		document.onmousemove = this.relatedObject.game.handleMousemoveOncards;
	}
	if( e.preventDefault ) { e.preventDefault(); }
	e.returnValue = false;
	return false;
	
};

solitaireGame.prototype.clearMouseEvents = function (noRepositioning) {

	if( !this.downOnCard && !this.moveOnCard ) { return; }

	// Put dragged cards in the right place here, only if the action was a move, not a down
	var oCard = this.moveOnCard;

	solitaireGame.prototype.downOnSolitaireCard = this.downOnCard = this.moveOnCard = null;

	// Remove all event handlers
	if( document.removeEventListener ) {
		document.removeEventListener('mouseup',this.handleMouseupOnCards,false);
		document.removeEventListener('mousemove',this.handleMousemoveOnCards,false);
	} else if( document.detachEvent ) {
		document.detachEvent('onmouseup',this.handleMouseupOnCards);
		document.detachEvent('onmousemove',this.handleMousemoveOnCards);
	} else {
		document.onmouseup = this.oldmouseup;
		document.onmousemove = this.oldmousemove;
		this.oldmouseup = null;
		this.oldmousemove = null;
	}

	while( !noRepositioning && oCard ) {
		oCard.autoPositionSolitaire();
		oCard = oCard.nextOnStack();
	}

};

solitaireGame.prototype.handleMousemoveOnCards = function (e) {

	// Drag a card if needed
	var thisCard = solitaireGame.prototype.downOnSolitaireCard;
	if( !thisCard ) { return; } // Hope this never happens :)
	if( !thisCard.game.gameInPlay ) { return; }
	if( thisCard.game.doingSpatNav ) { return; }
	if( !thisCard.game.downOnCard && !thisCard.game.moveOnCard ) { return; }

	if( thisCard.game.downOnCard ) {
		// Start dragging
		thisCard.game.moveOnCard = thisCard.game.downOnCard;
		thisCard.game.downOnCard = null;
		var tmpCard = thisCard;
		while( tmpCard ) {
			tmpCard.representation.style.zIndex = 30 + tmpCard.positionOnStack;
			tmpCard.representation.style.opacity = '0.4';
			tmpCard.representation.style.MozOpacity = '0.4';
			tmpCard.representation.style.filter = 'alpha(opacity=40)';
			if( tmpCard.representation.style.setProperty ) {
				// Safari 1.1
				try { tmpCard.representation.style.setProperty('-khtml-opacity','0.4'); } catch(e) {}
			}
			tmpCard = tmpCard.nextOnStack();
		}
	}

	// Move all dragged cards
	var oNum = typeof(0);
	var oNewInfo = [
		( typeof( e.pageX ) == oNum ) ? e.pageX : ( e.clientX + thisCard.game.checkBrowserScrollBug( document.documentElement.scrollLeft ? document.documentElement.scrollLeft : document.body.scrollLeft ) ),
		( typeof( e.pageY ) == oNum ) ? e.pageY : ( e.clientY + thisCard.game.checkBrowserScrollBug( document.documentElement.scrollTop ?