#!/usr/bin/env python2.5

# Freecell4Maemo, Copyright 2008, Roy Wood
#                 Copyright 2010, Justin Quek

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.


# To Do:
# - on-screen toggle for smart move mode
# - intelligent use of empty stacks when moving columns
# - smart-move a column?


"""	
Freecell4Maemo is an implementation of the classic Freecell cardgame for the Nokia "Maemo" platform.
	
The code is pretty small, and I have tried to comment it effectively throughout, so you should have be able to
figure things out pretty easily.

Some of the more significant pieces are as follows:
	
class Rect - a rectangle; important members are top, left, width, height
class Card - a playing card; important members are cardnum(0-51), screen location/size, pixbuf
class CardStack - a stack of Card objects; important members are screen location/size, cards, "empty stack" pixbuf, stack suit
class Freecell - the main class for the app; uses the other classes as needed


Some significant points about the main "Freecell" class are:

- the __init__ method creates all the basic object members, loads all the card images, creates the GUI
- the GUI is a single window containing a GTK DrawingArea
- all drawing is done in an offscreen PixMap
- the offscreen PixMap is blitted to the DrawingArea in response to expose events
- the offscreen PixMap is created in the configure event handler, not __init__
- all GraphicContext objects are created in the configure event handler
- the configure handler also triggers a call to set the rects of the CardStacks (important for switching between fullscreen and smallscreen)
- the real game logic is in the button_press_event handler (and yes, it gets a little messy)

"""


ABOUT_TEXT = """    
    Freecell for Maemo
    Ver: %s

    (c) 2008 Roy Wood
    (c) 2010 Justin Quek

    roy.wood@gmail.com
    jkq3@engineering.uiuc.edu

This game is an implementation of 
the classic Freecell card game for 
the Nokia Maemo platform.

To move a card, click once to select 
the card, then click in the 
destination location.

Click the return or space key to
auto-move cards to the ace stacks.

Click the backspace key or use the
menu button to undo a move.

Original code:
  http://code.google.com/p/freecell4maemo/
Modifications:
  (In Maemo extras-devel repository)

This program is free software: you 
can redistribute it and/or modify 
it under the terms of the GNU 
General Public License as published 
by the Free Software Foundation, 
either version 3 of the License, or 
(at your option) any later version.

This program is distributed in the 
hope that it will be useful, but 
WITHOUT ANY WARRANTY; without 
even the implied warranty of 
MERCHANTABILITY or FITNESS FOR A 
PARTICULAR PURPOSE.  See the GNU 
General Public License for more 
details.

You should have received a copy of 
the GNU General Public License 
along with this program.  If not, 
a copy may be found here: 
 
 <http://www.gnu.org/licenses/>

""" % 'foo'

import gtk
import pygtk
import time 
import random
import logging
import math

import pickle
import os

try:
	import hildon
	import osso
	
	osso_c = osso.Context("com.nokia.freecell4maemo", "1.0.0", False)
	
	hildonMode = True
	
except:
	hildonMode = False
	

# Size of the inset border for the window
FULLSCREEN_BORDER_WIDTH = 10
SMALLSCREEN_BORDER_WIDTH = 2

# Border between upper/lower sets of cards
VERT_SEPARATOR_WIDTH = 10


# Suit IDs
CLUBS = 0
DIAMONDS = 1
SPADES = 2
HEARTS = 3

SUITNAMES = [ "Clubs", "Diamonds", "Spades", "Hearts" ]

# Suit colours
BLACK = 0
RED = 1

# Number of cards per suit
CARDS_PER_SUIT = 13

# Card pixbufs 0-51 are the regular cards, 
NUMCARDS = 52

# Cards 52-55 are the suit-back cards (aces in top right of screen)
CLUBS_BACK = 52
DIAMONDS_BACK = 53
SPADES_BACK = 54
HEARTS_BACK = 55

# Card 56 is the the blank-back card (used to draw stacks with no cards)
BLANK_BACK = 56

# Card 57 is the fancy-back card (not currently used)
FANCY_BACK = 57

# Total number of card images
TOTALNUMCARDS = FANCY_BACK

# Number of card columns
NUMCOLUMNS = 8

# Number of "free cells"
NUMFREECELLS = 4

# Number of ace cards
NUMACES = 4

# Types of cards
FREECELL_TYPE = 0
ACE_TYPE = 1
REGULAR_TYPE = 2

# Folder containing the card images
CARDFOLDER = "/opt/freecell4maemo/card_images"

# Response constants for the "Move card or column" dialog (OK/CANCEL are the constants that the hildon.Note dialog returns)
MOVE_CARD_ID = gtk.RESPONSE_CANCEL
MOVE_COLUMN_ID = gtk.RESPONSE_OK


class Rect(object):
	# A basic rectangle object
	
	def __init__(self, left = 0, top = 0, width = 0, height = 0):
		self.left = int(left)
		self.top = int(top)
		self.width = int(width)
		self.height = int(height)
	
	def setRect(self, left = 0, top = 0, width = 0, height = 0):
		self.left = int(left)
		self.top = int(top)
		self.width = int(width)
		self.height = int(height)
	
	def enclosesXY(self, x, y):
		# Determine if a point lies within the Rect
		return ((x >= self.left) and (x < self.left + self.width) and (y >= self.top) and (y < self.top + self.height))
	
	def getTopLeft(self):
		return (self.left, self.top)
	
	def getLeftTopWidthHeight(self):
		return (self.left, self.top, self.width, self.height)
	
	def unionWith(self, otherRect):
		# Modify the Rect to include another Rect
		left = min(self.left, otherRect.left)
		right = max(self.left + self.width, otherRect.left + otherRect.width)
		top = min(self.top, otherRect.top)
		bottom = max(self.top + self.height, otherRect.top + otherRect.height)
		
		self.left = left
		self.top = top
		self.width = (right - left)
		self.height = (bottom - top)
	
	
	
class Card(object):
	# A Card object defined by card number (0-51), screen location and size, and pixbuf
	# Note that the cards are ordered A,2,3,4,5,6,7,8,9,10,J,Q,K
	# The suits are ordered Clubs, Diamonds, Hearts, Spades
	
	def __init__(self, cardNum, left = 0, top = 0, width = 0, height = 0, pixBuf = None):
		self.cardNum = cardNum
		self.rect = Rect(left, top, width, height)
		self.pixBuf = pixBuf
	
	def getSuit(self):
		return self.cardNum // CARDS_PER_SUIT
	
	def getSuitColour(self):
		return (self.cardNum // CARDS_PER_SUIT) % 2
	
	def getValue(self):
		return self.cardNum % CARDS_PER_SUIT
	
	def setRect(self, left = 0, top = 0, width = 0, height = 0):
		self.rect.setRect(left, top, width, height)
	
	def enclosesXY(self, x, y):
		# Determine if a point lies within the Card
		return self.rect.enclosesXY(x, y)
	
	def drawCard(self, drawable, gc, xyPt = None):
		# Draw the Card in the given drawable, using the supplied GC
		if (xyPt != None):
			left, top = xyPt
		else:
			left, top = self.rect.getTopLeft()
		
		drawable.draw_pixbuf(gc, self.pixBuf, 0, 0, left, top)
	
	def getTopLeft(self):
		return self.rect.getTopLeft()
	
	def getLeftTopWidthHeight(self):
		return self.rect.getLeftTopWidthHeight()
	
	def getRect(self):
		left, top, w, h = self.rect.getLeftTopWidthHeight()
		return Rect(left, top, w, h)


class CardStack(object):
	# An object representing a stack of cards
	# The CardStack contains a list of Card objects, possesses an onscreen location
	# The CardStack can draw itself; if there are no Cards, then the emptyStackPixBuf is displayed
	# The CardStack's yOffset controls the vertical offset of cards in the stack
	
	def __init__(self, left, top, emptyStackPixBuf, stackSuit, yOffset = 0):
		self.left = int(left)
		self.top = int(top)
		self.emptyStackPixBuf = emptyStackPixBuf
		self.yOffset = yOffset
		self.cardWidth = emptyStackPixBuf.get_width()
		self.cardHeight = emptyStackPixBuf.get_height()
		self.rect = Rect(self.left, self.top, self.cardWidth, self.cardHeight)
		self.stackSuit = stackSuit
		self.cards = [ ]
	
	def getNumCards(self):
		return len(self.cards)
	
	def clearStack(self):
		self.cards = [ ]
	
	def getRect(self):
		left, top, w, h = self.rect.getLeftTopWidthHeight()
		return Rect(left, top, w, h)
	
	def getLeftTopWidthHeight(self):
		left, top, w, h = self.rect.getLeftTopWidthHeight()
		return left, top, w, h
	
	def setLeftTop(self, left, top):
		self.left = left
		self.top = top
		self.rect = Rect(self.left, self.top, self.cardWidth, self.cardHeight + self.yOffset * len(self.cards))
		for i in range(len(self.cards)):
			self.cards[i].setRect(self.left, self.top + self.yOffset * i, self.cardWidth, self.cardHeight)
	
	def pushCard(self, card):
		card.setRect(self.left, self.top + self.yOffset * len(self.cards), self.cardWidth, self.cardHeight)
		self.cards.append(card)
		self.rect = Rect(self.left, self.top, self.cardWidth, self.cardHeight + self.yOffset * len(self.cards))
	
	def getCardValueSuitColour(self, cardIndex):
		# Get the card value, suit, and colour of a card on the CardStack; negative cardIndex values work the expected way (e.g. -1 is last/top card); if a bad index value is supplied, return the stack suit (i.e. ace stack suit)
		if (cardIndex >= len(self.cards) or abs(cardIndex) > len(self.cards)):
			return -1, self.stackSuit, self.stackSuit % 2
		else:
			card = self.cards[cardIndex]
			return card.getValue(), card.getSuit(), card.getSuitColour()
	
	def getTopCardRect(self):
		# Get the rect of top card on the CardStack; return bare rect if there are no cards
		if (len(self.cards) > 0):
			return self.cards[-1].getRect()
		else:
			left, top, w, h = self.rect.getLeftTopWidthHeight()
			return Rect(left, top, w, h)
	
	def getNextTopCardLeftTop(self):
		# Get the top/left of the next card location on the stack (useful for animation)
		if (len(self.cards) > 0):
			x,y,w,h = self.cards[-1].getLeftTopWidthHeight()
			return (x, y + self.yOffset)
		else:
			return (self.left, self.top)
	
	def popCard(self):
		# Remove the top card on the CardStack; return the popped Card or None
		if (len(self.cards) > 0):
			card = self.cards[-1]
			del self.cards[-1]
			self.rect.setRect(self.left, self.top, self.cardWidth, self.cardHeight + self.yOffset * len(self.cards))
			return card
		else:
			return None
	
	def enclosesXY(self, x, y):
		# Determine if a point lies within the CardStack
		return self.rect.enclosesXY(x, y)
	
	def drawStack(self, drawable, gc):
		# Draw the stack (or the "empty stack" image) in the given drawable, using the supplied GC
		if (len(self.cards) <= 0):
			left, top = self.rect.getTopLeft()
			drawable.draw_pixbuf(gc, self.emptyStackPixBuf, 0, 0, left, top)
		elif (self.yOffset == 0):
			self.cards[-1].drawCard(drawable, gc)
		else:
			for c in self.cards:
				c.drawCard(drawable, gc)
	
	def drawTopCard(self, drawable, gc):
		# Draw the top card (or the "empty stack" image) in the given drawable, using the supplied GC
		if (len(self.cards) <= 0):
			left, top = self.rect.getTopLeft()
			drawable.draw_pixbuf(gc, self.emptyStackPixBuf, 0, 0, left, top)
		else:
			self.cards[-1].drawCard(drawable, gc)
		

class FreeCell(object):
	
	def __init__(self):
		# Init the rendering objects to None for now; they will be properly populated during the expose_event handling
		self.offscreenPixmap = None
		self.offscreenGC = None
		self.greenColour = None
		self.redColour = None
		self.blackColour = None
		self.whiteColour = None
		self.tmpPixmap = None
		self.tmpGC = None
		self.gameover = False	# jkq
		
		# Load the cards
		self.cardPixbufs = [ gtk.gdk.pixbuf_new_from_file("%s/%02d.gif" % (CARDFOLDER, i)) for i in range(TOTALNUMCARDS) ]
		
		# Load the pretty star
		#self.starPixbuf = gtk.gdk.pixbuf_new_from_file("%s/star.gif" % (CARDFOLDER))
		self.starPixbuf = gtk.gdk.pixbuf_new_from_file("%s/lightning.gif" % (CARDFOLDER))
		self.starRect = Rect()
		
		# All cards are supposed to be the same height and width
		self.cardHeight = self.cardPixbufs[0].get_height()
		self.cardWidth = self.cardPixbufs[0].get_width()
		
		
		# Each group of cards (freecells, aces, columns) is stored in a list of CardStacks
		# We also keep track of a bounding rect for each group and use this rect when doing hit-testing of mouse clicks
		
		# Set up the "free cells" (4 cells in top left of screen)
		self.freecellStacks = [ CardStack(0, 0, self.cardPixbufs[BLANK_BACK], -1, 0) for i in range(NUMFREECELLS) ]
		self.freeCellsRect = None
		
		# Set up the "aces" (4 cells in top right of screen); order is important!
		self.acesStacks = [ CardStack(0, 0, self.cardPixbufs[CLUBS_BACK + i], i, 0) for i in range(NUMACES) ]
		self.acesRect = None
		
		# Set up the columns
		self.mainCardStacks = [ CardStack(0, 0, self.cardPixbufs[BLANK_BACK], -1, self.cardHeight // 5) for i in range(NUMCOLUMNS) ]
		self.mainCardsRects = None
		
		# Keep track of all card stack moves so we can undo moves
		self.undoStack = [ ]

# jkq
		# try opening the save file
		try:
			fp = open(os.path.join(os.environ['HOME'], '.freecell4maemo'), 'rb')
		except IOError:
			# error, so file must not exist. new game!
			newgame = True
			cardstr = pickle.dumps([])
			moves = []
		else:
			# success! setup for loading state
			newgame = False
			moves = []
			# get the state from the file
			lines = fp.readlines()
			fp.close()
			# get the initial card positions
			lastline = lines.index('---***---\n')
			cardstr = ''.join(lines[:lastline])
			# get the undo list
			lines = lines[lastline+1:]
			while len(lines) > 0:
				# one entry at a time
				thisitem = lines.index('------\n')
				moves.append(pickle.loads(''.join(lines[:thisitem])))
				lines = lines[thisitem+1:]
			# delete the save game
			try:
				os.remove(os.path.join(os.environ['HOME'], '.freecell4maemo'))
			except IOError:
				pass
# jkq
		
		# Initialize the cards
#		self.startCardOrder = []
#		self.setupCards()
		self.startCardOrder = pickle.loads(cardstr)
		self.setupCards(newgame)
		
		# Default to manual play mode
		self.smartPlayMode = False
		
		
		# These get set properly during the configure event handler
		self.windowWidth = 0
		self.windowHeight = 0
		self.windowFullscreen = False
		
		
		# Create menus
#		self.menu = gtk.Menu()
		self.menu = hildon.AppMenu()
		
#		menuItem = gtk.MenuItem("_New Game")
#		menuItem.connect("activate", self.new_game_menu_cb)
		menuItem = gtk.Button("New Game")
		menuItem.connect("clicked", self.new_game_menu_cb)
		self.menu.append(menuItem)
		menuItem.show()
		
#		menuItem = gtk.MenuItem("_Restart Game")
#		menuItem.connect("activate", self.restart_game_menu_cb)
		menuItem = gtk.Button("Restart Game")
		menuItem.connect("clicked", self.restart_game_menu_cb)
		self.menu.append(menuItem)
		menuItem.show()
		
# jkq
		menuItem = gtk.Button("Undo")
		menuItem.connect("clicked", self.undo_move_cb)
		self.menu.append(menuItem)
		menuItem.show()
# jkq

#		menuItem = gtk.MenuItem("_About...")
#		menuItem.connect("activate", self.about_menu_cb)
		menuItem = gtk.Button("About")
		menuItem.connect("clicked", self.about_menu_cb)
		self.menu.append(menuItem)
		menuItem.show()
		
#		menuItem = gtk.MenuItem("E_xit")
#		menuItem.connect("activate", self.exit_menu_cb)
#		self.menu.append(menuItem)
#		menuItem.show()
		
		# Main part of window is a DrawingArea
		self.drawingArea = gtk.DrawingArea()
		
		
		global hildonMode
		
		if (hildonMode):
			# Main window contains a single DrawingArea; menu is attached to Hildon window
			self.app = hildon.Program()
#			self.mainWindow = hildon.Window()
			self.mainWindow = hildon.StackableWindow()
			self.mainWindow.set_title("Freecell")
			self.app.add_window(self.mainWindow)
			self.mainWindow.add(self.drawingArea)
#			self.mainWindow.set_menu(self.menu)
			self.mainWindow.set_main_menu(self.menu)
			
			# Hildon dialogs are different than regular Gtk dialogs
			self.cardOrColumnDialog = hildon.Note("confirmation", self.mainWindow, "Move column or card?", gtk.STOCK_DIALOG_QUESTION)
			self.cardOrColumnDialog.set_button_texts ("Column", "Card")
			
			self.youWinDialog = hildon.Note("information", self.mainWindow, "You won!", gtk.STOCK_DIALOG_INFO)
			
			
		else:
			# Main window contains a VBox with a MenuBar and a DrawingArea
			self.mainWindow = gtk.Window(gtk.WINDOW_TOPLEVEL)
			#self.mainWindow.set_default_size(800,600)
			self.drawingArea.set_size_request(800, 480)
			
			fileMenu = gtk.MenuItem("_File")
			fileMenu.set_submenu(self.menu)
			menuBar = gtk.MenuBar()
			menuBar.append(fileMenu)
			vbox = gtk.VBox()
			vbox.pack_start(menuBar, False, False, 2)
			vbox.pack_end(self.drawingArea, True, True, 2)
			self.mainWindow.add(vbox)
			
			# Create the dialogs in advance and then reuse later
			self.cardOrColumnDialog = gtk.Dialog(parent = self.mainWindow, flags = gtk.DIALOG_MODAL, buttons=("Column", MOVE_COLUMN_ID, "Card", MOVE_CARD_ID))
			self.cardOrColumnLabel = gtk.Label("Move column or card?")
			self.cardOrColumnDialog.vbox.pack_start(self.cardOrColumnLabel)
			self.cardOrColumnLabel.show()
			
			self.youWinDialog = gtk.MessageDialog(self.mainWindow, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_OK, "You won!")
		
		
		# Wire up the event callbacks
		self.mainWindow.connect("delete_event", self.delete_event_cb)
		self.mainWindow.connect("destroy", self.destroy_cb)
		self.mainWindow.connect("key-press-event", self.key_press_cb)
		self.mainWindow.connect("window-state-event", self.window_state_change_cb)
		self.drawingArea.connect("expose_event", self.expose_event_cb)
		self.drawingArea.connect("configure_event", self.configure_event_cb)
		self.drawingArea.connect("button_press_event", self.button_press_event_cb)
		self.drawingArea.set_events(gtk.gdk.EXPOSURE_MASK | gtk.gdk.BUTTON_PRESS_MASK)
		
		# Create the "About" dialog
		self.aboutDialog = gtk.Dialog(parent = self.mainWindow, flags = gtk.DIALOG_MODAL, buttons=(gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
		#self.aboutDialog.set_geometry_hints(self.mainWindow, min_width=400, min_height=200)
		self.aboutDialog.set_default_size(480,300)
		self.aboutScrolledWin = gtk.ScrolledWindow()
		self.aboutScrolledWin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
		self.aboutTextView = gtk.TextView()
		self.aboutTextView.set_editable(False)
		self.aboutTextView.get_buffer().set_text(ABOUT_TEXT)
		self.aboutScrolledWin.add(self.aboutTextView)
		self.aboutDialog.vbox.pack_start(self.aboutScrolledWin)
		
		# Behold!
		self.mainWindow.show_all()
		
		# Track the currently selected card
		self.selectedCardRect = Rect()
		self.selectedCardStack = None
		self.selectedCardType = None
		
		self.debugMode = False
	
# jkq
		if len(moves) > 0:
			for (src, dst) in moves:
				print '%s, %s' % (src, dst)
				src_stack = self.retrieve_stack(src[0])
				dst_stack = self.retrieve_stack(dst[0])
				self.moveCard(src_stack[src[1]], dst_stack[dst[1]])

	def compare_stack(self, stack):
		for s in range(NUMFREECELLS):
			if stack == self.freecellStacks[s]:
				return (0, s)
		for s in range(NUMACES):
			if stack == self.acesStacks[s]:
				return (1, s)
		for s in range(NUMCOLUMNS):
			if stack == self.mainCardStacks[s]:
				return (2, s)
		return (-1, -1)

	def retrieve_stack(self, stack):
		if stack == 0:
			print 'free'
			return self.freecellStacks
		elif stack == 1:
			print 'aces'
			return self.acesStacks
		elif stack == 2:
			print 'main'
			return self.mainCardStacks
		else:
			# pray we never get here!
			return None

	def save_and_quit(self):
		if not self.gameover:
			fp = open(os.path.join(os.environ['HOME'], '.freecell4maemo'), 'w')
			fp.write('%s\n' % pickle.dumps(self.startCardOrder))
			fp.write('---***---\n')
			for (src, dst) in self.undoStack:
				move = ( self.compare_stack(src), self.compare_stack(dst) )
				fp.write('%s\n' % pickle.dumps(move))
				fp.write('------\n')
			fp.close()
			logging.info("save_and_quit")
		gtk.main_quit()
# jkq
	
	def exit_menu_cb(self, widget):
		self.save_and_quit()
#		gtk.main_quit()
	
	
	def restart_game_menu_cb(self, widget):
		self.undoStack = [ ]
		self.acesStacks = [ CardStack(0, 0, self.cardPixbufs[CLUBS_BACK + i], i, 0) for i in range(NUMACES) ]
		self.freecellStacks = [ CardStack(0, 0, self.cardPixbufs[BLANK_BACK], -1, 0) for i in range(NUMFREECELLS) ]
		self.setupCards(False)
		self.setCardRects()
		self.redrawOffscreen()
		self.updateRect(None)
	
	
	def about_menu_cb(self, widget):
		self.aboutDialog.show_all()
		self.aboutDialog.run()
		self.aboutDialog.hide()
	
	
	def new_game_menu_cb(self, widget):
		self.undoStack = [ ]
		self.acesStacks = [ CardStack(0, 0, self.cardPixbufs[CLUBS_BACK + i], i, 0) for i in range(NUMACES) ]
		self.freecellStacks = [ CardStack(0, 0, self.cardPixbufs[BLANK_BACK], -1, 0) for i in range(NUMFREECELLS) ]
		self.setupCards()
		self.setCardRects()
		self.redrawOffscreen()
		self.updateRect(None)
	
	
	def key_press_cb(self, widget, event, *args):
		if (event.keyval == gtk.keysyms.F6):
			if (self.windowFullscreen):
				self.mainWindow.unfullscreen()
			else:
				self.mainWindow.fullscreen()
		
		elif (event.keyval == gtk.keysyms.Up):
			print "Up!"
			self.smartPlayMode = False
			self.redrawStar()
			self.updateRect(self.starRect)
		
		elif (event.keyval == gtk.keysyms.Down):
			print "Down!"
			self.smartPlayMode = True
			self.redrawStar()
			self.updateRect(self.starRect)
		
		elif (event.keyval == gtk.keysyms.Left):
			print "Left!"
		
		elif (event.keyval == gtk.keysyms.Right):
			print "Right!"
			
		elif (event.keyval == gtk.keysyms.BackSpace):
			print "Backspace!"
			self.undoMove()
		
		elif (event.keyval == gtk.keysyms.space):
			print "Space!"
			self.autoMoveCardsHome()
		
		elif (event.keyval == gtk.keysyms.KP_Enter):
			print "Return!"
			self.autoMoveCardsHome()
		
		elif (event.keyval == gtk.keysyms.F7):
			print "Zoom +!"
			self.debugMode = False
		
		elif (event.keyval == gtk.keysyms.F8):
			print "Zoom -!"
			self.debugMode = True
	
# jkq
	def undo_move_cb(self, widget):
		self.undoMove()

	def auto_move_cb(self, widget):
		self.autoMoveCardsHome()
# jkq
	
	def autoMoveCardsHome(self):
		# Move cards to the ace stacks, where possible
		
		cardStacks = self.freecellStacks + self.mainCardStacks
		
		while (True):
			movedACard = False
			
			for srcStack in cardStacks:
				srcCardValue, srcCardSuit, srcCardSuitColour = srcStack.getCardValueSuitColour(-1)
				if (srcCardSuit >= 0):
					aceCardValue, aceCardSuit, aceCardSuitColour = self.acesStacks[srcCardSuit].getCardValueSuitColour(-1)
					if (srcCardValue == aceCardValue + 1):
						tempRect = srcStack.getTopCardRect()
						self.flashRect(tempRect)
						self.moveCard(srcStack, self.acesStacks[srcCardSuit])
						movedACard = True
			
			if (movedACard != True):
				break
		
		self.clearCardSelection()
		
		self.checkGameOver()
	
	
	def checkGameOver(self):
		# Game over?
		numFullAceStacks = 0
		
		for stack in self.acesStacks:
			cardVal, cardSuit, cardColour = stack.getCardValueSuitColour(-1)
			if (cardVal == CARDS_PER_SUIT - 1):
				numFullAceStacks += 1
		
		if (numFullAceStacks == NUMACES):
			self.gameover = True	# jkq
			self.youWinDialog.show()
			dialogResult = self.youWinDialog.run()
			self.youWinDialog.hide()
	
	
	def undoMove(self):
		# Undo a move
		if (len(self.undoStack) > 0):
			srcStack, dstStack = self.undoStack[-1]
			self.moveCard(dstStack, srcStack)
			# The call to moveCard actually records the undo as a move, so we need to pop the last TWO entries in the stack
			del self.undoStack[-1]
			del self.undoStack[-1]
			self.clearCardSelection()
	
	
	def window_state_change_cb(self, widget, event, *args):
		# Handle a window state change to/from fullscreen
		logging.info("window_state_change_cb")
		if (event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN):
			self.windowFullscreen = True
		else:
			self.windowFullscreen = False
	
	
	def setupCards(self, doShuffle = True):
		# Shuffle deck, distribute cards into the columns
		
		if (doShuffle):
			self.gameover = False	# jkq
			cards = [i for i in range(NUMCARDS) ]
			random.shuffle(cards)
			self.startCardOrder = cards
		else:
			cards = self.startCardOrder
		
		for i in range(NUMCOLUMNS):
			self.mainCardStacks[i].clearStack()
		
		for i in range(NUMCARDS):
			cardNum = cards[i]
			cardCol = i % NUMCOLUMNS
			newCard = Card(cardNum, pixBuf = self.cardPixbufs[cardNum])
			self.mainCardStacks[cardCol].pushCard(newCard)
	
	
	def getStackListEnclosingRect(self, cardStackList):
		# Get a rect that encloses all the cards in the given list of CardStacks
		
		rect = cardStackList[0].getRect()
		for i in range(1, len(cardStackList)):
			rect.unionWith(cardStackList[i].getRect())
		return rect
	
	
	def setCardRects(self):
		# Set the position of all card stacks; this is done in response to a configure event
		
		# Set location of main stacks of cards
		cardHorizSpacing = self.windowWidth / 8.0
		for i in range(NUMCOLUMNS):
			x = int(i * cardHorizSpacing + (cardHorizSpacing - self.cardWidth) // 2)
			self.mainCardStacks[i].setLeftTop(x, VERT_SEPARATOR_WIDTH + self.cardHeight + VERT_SEPARATOR_WIDTH)
		
		# Set location of free cells and aces
		cardHorizSpacing = self.windowWidth / 8.5
		for i in range(NUMFREECELLS):
			x = i * cardHorizSpacing + (cardHorizSpacing - self.cardWidth) // 2
			self.freecellStacks[i].setLeftTop(x, VERT_SEPARATOR_WIDTH)
			
			x = int((i + NUMFREECELLS + 0.5) * cardHorizSpacing + (cardHorizSpacing - self.cardWidth) // 2)
			self.acesStacks[i].setLeftTop(x, VERT_SEPARATOR_WIDTH)
		
		# Get the enclosing rects for click-testing
		self.mainCardsRects = self.getStackListEnclosingRect(self.acesStacks)
		self.freeCellsRect = self.getStackListEnclosingRect(self.freecellStacks)
		self.acesRect = self.getStackListEnclosingRect(self.acesStacks)
	
	
	def delete_event_cb(self, widget, event, data=None):
		# False means okay to delete
		return False
	
	
	def destroy_cb(self, widget, data=None):
		# Tell gtk to quit
		self.save_and_quit()
#		gtk.main_quit()
	
	
	def flashRect(self, rect, repeats = 3):
		# Flash/invert a rect onscreen
		
		if (rect == None):
			return
		
		for i in range(repeats):
			self.invertRect(rect)
			#while (gtk.events_pending()): gtk.main_iteration(False)
			gtk.gdk.window_process_all_updates()
			time.sleep(0.125)
	
	
	def updateRect(self, rect):
		# Queue a redraw of an onscreen rect
		
		if (rect == None):
			x, y, w, h = 0, 0, self.windowWidth, self.windowHeight
		else:
			x, y, w, h = rect.getLeftTopWidthHeight()
		
		#logging.info("updateRect: (%d,%d) %dx%d" % (x, y, w + 1, h + 1))
		
		self.drawingArea.queue_draw_area(x, y, w + 1, h + 1)
	
	
	def invertRect(self, rect):
		# Invert a rect onscreen
		
		if (rect == None):
			return
		x, y, w, h = rect.getLeftTopWidthHeight()
		self.drawingAreaGC.set_foreground(self.whiteColour)
		self.drawingAreaGC.set_function(gtk.gdk.XOR)
		self.drawingArea.window.draw_rectangle(self.drawingAreaGC, True, x, y, w, h)
		self.drawingAreaGC.set_function(gtk.gdk.COPY)

	
	def redrawStar(self):
		if (self.smartPlayMode):
			left, top, width, height = self.starRect.getLeftTopWidthHeight()
			self.offscreenPixmap.draw_pixbuf(self.offscreenGC, self.starPixbuf, 0, 0, left, top)
		else:
			self.offscreenGC.set_foreground(self.greenColour)
			left, top, width, height = self.starRect.getLeftTopWidthHeight()
			self.offscreenPixmap.draw_rectangle(self.offscreenGC, True, left, top, width, height)
		
	
	def redrawOffscreen(self):
		# Redraw the game board and all card stacks
		
		self.offscreenGC.set_foreground(self.greenColour)
		width, height = self.offscreenPixmap.get_size()
		self.offscreenPixmap.draw_rectangle(self.offscreenGC, True, 0, 0, width, height)
		
		for cardStack in self.acesStacks:
			cardStack.drawStack(self.offscreenPixmap, self.offscreenGC)
		
		for cardStack in self.freecellStacks:
			cardStack.drawStack(self.offscreenPixmap, self.offscreenGC)
		
		for cardStack in self.mainCardStacks:
			cardStack.drawStack(self.offscreenPixmap, self.offscreenGC)
		
		self.redrawStar()
		
	
	
	def configure_event_cb(self, widget, event):
		# Handle the window configuration event at startup or when changing to/from fullscreen
		
		logging.info("configure_event_cb")
		
		# Allocate a Pixbuf to serve as the offscreen buffer for drawing of the game board
		
		x, y, width, height = widget.get_allocation()
		self.offscreenPixmap = gtk.gdk.Pixmap(widget.window, width, height)
		self.offscreenGC = self.offscreenPixmap.new_gc()
		self.greenColour = self.offscreenGC.get_colormap().alloc_color(0x0000, 0x8000, 0x0000)
		self.redColour = self.offscreenGC.get_colormap().alloc_color(0xFFFF, 0x0000, 0x0000)
		self.blackColour = self.offscreenGC.get_colormap().alloc_color(0x0000, 0x0000, 0x0000)
		self.whiteColour = self.offscreenGC.get_colormap().alloc_color(0xFFFF, 0xFFFF, 0xFFFF)
		self.drawingAreaGC = self.drawingArea.window.new_gc()
		self.tmpPixmap = gtk.gdk.Pixmap(widget.window, width, height)
		self.tmpGC = self.tmpPixmap.new_gc()
		
		
		# Screen geometry has changed, so note new size, set CardStack locations, redraw screen
		self.windowWidth = width
		self.windowHeight = height
		
		logging.debug("configure_event_cb: self.windowWidth = %d, self.windowHeight = %d" % (self.windowWidth, self.windowHeight))
		
		# Resize has occurred, so set the card rects
		self.setCardRects()
		
		# Set the star rect
		left = (self.windowWidth - self.starPixbuf.get_width()) // 2
		top = 2 * VERT_SEPARATOR_WIDTH
		self.starRect = Rect(left, top, self.starPixbuf.get_width(), self.starPixbuf.get_height())
		
		# Redraw everything
		self.redrawOffscreen()
		
		return True


	def expose_event_cb(self, widget, event):
		# Draw game board by copying from offscreen Pixbuf to onscreen window
		# Gtk is apparently now double-buffered, so this is probably unnecessary
		
		x , y, width, height = event.area
		
		logging.info("expose_event_cb: x=%d, y=%d, w=%d, h=%d" % (x, y, width, height))
		
		if (self.offscreenPixmap != None):
			widget.window.draw_drawable(widget.get_style().fg_gc[gtk.STATE_NORMAL], self.offscreenPixmap, x, y, x, y, width, height)
		
		return False
	
	
	def clearCardSelection(self):
		# Clear the current card selection (drawn inverted)
		
		#logging.info("clearCardSelection: (%d,%d) %dx%d" %(self.selectedCardRect.getLeftTopWidthHeight()))
		if (self.selectedCardRect != None):
			self.updateRect(self.selectedCardRect)
		self.selectedCardRect = None
		self.selectedCardType = None
		self.selectedCardStack = None
	
	
	def setCardSelection(self, stackType, cardStack, cardRect):
		self.invertRect(cardRect)
		self.selectedCardRect = cardRect
		self.selectedCardType = stackType
		self.selectedCardStack = cardStack
	
	
	def animateCardMove(self, card, toX,toY):
		# Cutesy animation showing movement of a card from its current location to a new location
		
		fromX,fromY,cardWidth,cardHeight = card.getLeftTopWidthHeight()
		
		deltaX, deltaY = float(toX - fromX), float(toY - fromY)
		dist = math.sqrt(deltaX*deltaX + deltaY*deltaY)
		speed = 10.0
		numSteps = int(dist / speed)
		
		if (numSteps <= 0):
			return
		
		vx, vy = deltaX / numSteps, deltaY / numSteps
		
		updateWidth, updateHeight = cardWidth + int(abs(vx) + 0.5) + 1, cardHeight + int(abs(vy) + 0.5) + 1
		
		prevX, prevY = fromX, fromY
		
		for i in range(numSteps + 1):
			if (i == numSteps):
				# Avoid rounding issues
				x, y = int(toX), int(toY)
			else:
				x, y = int(fromX + vx * i), int(fromY + vy * i)
			
			left, top = min(x, prevX), min(y, prevY)
			
			self.tmpPixmap.draw_drawable(self.tmpGC, self.offscreenPixmap, left, top, left, top, updateWidth, updateHeight)
			card.drawCard(self.tmpPixmap, self.tmpGC, (x,y))
			self.drawingArea.window.draw_drawable(self.drawingArea.get_style().fg_gc[gtk.STATE_NORMAL], self.tmpPixmap, left, top, left, top, updateWidth, updateHeight)
			
			# Took me a long time to figure out that this forces screen updates and makes the animation work
			gtk.gdk.window_process_all_updates()
			
			prevX, prevY = x, y
			
			#time.sleep(0.1)
		
	
	def moveCard(self, srcStack, dstStack):
		# Move a card from one stack to another
		
		if (srcStack == dstStack):
			return
		
		srcCardVal, srcSuit, srcSuitColour = srcStack.getCardValueSuitColour(-1)
		dstCardVal, dstSuit, dstSuitColour = dstStack.getCardValueSuitColour(-1)
		
		logging.info("moveCard: move %s %d to %s %d" % (SUITNAMES[srcSuit], srcCardVal, SUITNAMES[dstSuit], dstCardVal))
		self.undoStack.append((srcStack, dstStack))
		
		x, y, w, h = srcStack.getTopCardRect().getLeftTopWidthHeight()
		self.offscreenGC.set_foreground(self.greenColour)
		self.offscreenPixmap.draw_rectangle(self.offscreenGC, True, x, y, w, h)
		
		fromX, fromY = x, y
		toX, toY = dstStack.getNextTopCardLeftTop()
		
		card = srcStack.popCard()
		srcStack.drawTopCard(self.offscreenPixmap, self.offscreenGC)
		
		self.animateCardMove(card, toX, toY)
		
		dstStack.pushCard(card)
		dstStack.drawTopCard(self.offscreenPixmap, self.offscreenGC)
	
	
	def xyToCardStackInfo(self, x, y):
		# Determine the card/stack at a given (x,y); return the type, rect, cardStack of the target
		
		hitType = None
		hitRect = None
		hitStack = None
		
		if (self.freeCellsRect.enclosesXY(x, y)):
			for i in range(len(self.freecellStacks)):
				hitStack = self.freecellStacks[i]
				if (hitStack.enclosesXY(x, y)):
					hitRect = self.freecellStacks[i].getRect()
					hitType = FREECELL_TYPE
					break
		
		elif (self.acesRect.enclosesXY(x, y)):
			for i in range(len(self.acesStacks)):
				hitStack = self.acesStacks[i]
				if (hitStack.enclosesXY(x, y)):
					hitRect = self.acesStacks[i].getRect()
					hitType = ACE_TYPE
					break
		
		else:
			for i in range(len(self.mainCardStacks)):
				hitStack = self.mainCardStacks[i]
				if (hitStack.enclosesXY(x, y)):
					hitRect = self.mainCardStacks[i].getTopCardRect()
					hitType = REGULAR_TYPE
					
					break
		
		return (hitType, hitRect, hitStack)
	
	
	def button_press_event_cb(self, widget, event):
		# This is the big, ugly one-- all the gameplay rules are implemented here...
		
		x, y = event.x, event.y
		
		dstType, dstRect, dstStack = self.xyToCardStackInfo(x, y)
		
		if (dstType == None):
			# Didn't click on a valid target, so clear the previous click selection and bail
			self.clearCardSelection()
		
		elif (self.selectedCardType == None):
			# There was no previous selection, so select target (if valid) and bail
			if (dstStack.getNumCards() > 0):
				if (not self.smartPlayMode):
					self.setCardSelection(dstType, dstStack, dstRect)
				
				else:
					# Move the card to an ace stack, main stack, or free cell stack, if possible
					movedCard = False
					origDstType, origDstStack, origDstRect = dstType, dstStack, dstRect
					
					# Call it srcStack to make this clearer
					srcStack = dstStack
					srcCardVal, srcCardSuit, srcCardSuitColour = srcStack.getCardValueSuitColour(-1)
					
					# Try the aces stack first
					dstStack = self.acesStacks[srcCardSuit]
					dstCardVal, dstSuit, dstSuitColour = dstStack.getCardValueSuitColour(-1)
					
					if (dstCardVal == srcCardVal - 1):
						self.moveCard(srcStack, dstStack)
						self.clearCardSelection()
						movedCard = True
					
					if (movedCard == False):
						# Try a non-empty main stack
						for dstStack in self.mainCardStacks:
							dstCardVal, dstSuit, dstSuitColour = dstStack.getCardValueSuitColour(-1)
							if (dstCardVal >= 0 and dstCardVal == srcCardVal + 1 and dstSuitColour != srcCardSuitColour):
								self.moveCard(srcStack, dstStack)
								self.clearCardSelection()
								movedCard = True
								break
					
					if (movedCard == False):
						# Try an empty main stack or a freecell stack
						tmpStacks = self.mainCardStacks + self.freecellStacks
						for dstStack in tmpStacks:
							if (dstStack.getNumCards() <= 0):
								self.moveCard(srcStack, dstStack)
								self.clearCardSelection()
								movedCard = True
								break
					
					if (movedCard == False):
						self.setCardSelection(dstType, origDstStack, dstRect)
					
		
		else:
			# A card is currently selected, so see if it can be moved to the target
			srcStack = self.selectedCardStack
			srcNumCards = srcStack.getNumCards()
			srcCardVal, srcSuit, srcSuitColour = srcStack.getCardValueSuitColour(-1)
			dstNumCards = dstStack.getNumCards()
			dstCardVal, dstSuit, dstSuitColour = dstStack.getCardValueSuitColour(-1)
			dstSrcDelta = dstCardVal - srcCardVal
			logging.debug("srcSuit = %d, srcSuitColour = %d, srcCardVal = %d, srcNumCards = %d" % (srcSuit, srcSuitColour, srcCardVal, srcNumCards))
			logging.debug("dstSuit = %d, dstSuitColour = %d, dstCardVal = %d, dstSrcDelta = %d" % (dstSuit, dstSuitColour, dstCardVal, dstSrcDelta))
			
			numFreeCells = 0
			for cardStack in self.freecellStacks:
				if (cardStack.getNumCards() <= 0):
					numFreeCells += 1
			
			runLength = 0
			for i in range(srcNumCards):
				cardVal, cardSuit, cardSuitColour = srcStack.getCardValueSuitColour(srcNumCards - i - 1)
				logging.debug("card #%d: cardVal = %d, cardSuit = %d, cardSuitColour = %d" % (srcNumCards - i - 1, cardVal, cardSuit, cardSuitColour))
				if (cardVal == srcCardVal + i and cardSuitColour == (srcSuitColour + i) % 2):
					runLength += 1
				else:
					break
			
			suitColoursWork = (srcSuitColour == (dstSuitColour + dstSrcDelta) % 2)
			srcRunMeetsDst = dstSrcDelta >0 and runLength >= dstSrcDelta
			
			logging.info("dstSrcDelta = %d, numFreeCells = %d, runLength = %d, suitColoursWork = %s" % (dstSrcDelta, numFreeCells, runLength, suitColoursWork))
			
			
			if (dstType == FREECELL_TYPE):
				# Move selected card to a free cell, if it is open
				if (dstNumCards <= 0 or self.debugMode):
					self.moveCard(srcStack, dstStack)
			
			elif (dstType == ACE_TYPE):
				# Move selected card to an ace stack, if it matches suit and is in order
				logging.debug("srcSuit=%d, dstSuit=%d, dstNumCards=%d, srcCardVal=%d, dstCardVal=%d" % (srcSuit, dstSuit, dstNumCards, srcCardVal, dstCardVal))
				if srcSuit == dstSuit and srcCardVal == dstCardVal + 1:
					self.moveCard(srcStack, dstStack)
			
			elif (dstNumCards <= 0 and runLength <= 1):
				# Move a single card to an empty stack
				self.moveCard(srcStack, dstStack)
			
			elif (dstNumCards <= 0 and runLength > 1):
				dialogResult = MOVE_CARD_ID
				
				if (numFreeCells > 0):
					# Move a card or a column to an empty stack?
					self.cardOrColumnDialog.show()
					dialogResult = self.cardOrColumnDialog.run()
					self.cardOrColumnDialog.hide()
				
				# Repaint the mess made by the dialog box
				gtk.gdk.window_process_all_updates()
				x, y, w, h = 0, 0, self.windowWidth, self.windowHeight
				self.drawingArea.window.draw_drawable(self.drawingArea.get_style().fg_gc[gtk.STATE_NORMAL], self.offscreenPixmap, x, y, x, y, w, h)
				
				if (dialogResult == MOVE_CARD_ID):
					# Just move a single card after all
					self.moveCard(srcStack, dstStack)
				else:
					# Nope, move the run
					tempStacks = [ ]
					for i in range(min(numFreeCells, runLength - 1)):
						for j in range(NUMFREECELLS):
							if (self.freecellStacks[j].getNumCards() <= 0):
								self.moveCard(srcStack, self.freecellStacks[j])
								tempStacks.insert(0, self.freecellStacks[j])
								break
					self.moveCard(srcStack, dstStack)
					for s in tempStacks:
						self.moveCard(s, dstStack)
			
			elif (srcRunMeetsDst and suitColoursWork and numFreeCells >= dstSrcDelta - 1):
				# Move a column onto another card (column could be just a single card, really)
				logging.debug("Column move")
				tempStacks = [ ]
				for i in range(dstSrcDelta - 1):
					for j in range(NUMFREECELLS):
						if (self.freecellStacks[j].getNumCards() <= 0):
							self.moveCard(srcStack, self.freecellStacks[j])
							tempStacks.insert(0, self.freecellStacks[j])
							break
				self.moveCard(srcStack, dstStack)
				for s in tempStacks:
					self.moveCard(s, dstStack)
			
			
			# Clear selection
			self.clearCardSelection()
			
			logging.debug("-----------------------------------------------------------------------------")
		
		self.checkGameOver()
		
		return True

	
if __name__ == "__main__":
	logging.basicConfig(level=logging.INFO)
	
	freeCell = FreeCell()
	
	gtk.main()
