#    Jools -- a graphical puzzle game in the Tetris tradition
#    
#    Copyright (C) 2002-2003 Paul Pelzl
#
#    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 2 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, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#

# grid.py
# Implements game logic, jool position handling, etc.

import pygame, math
from jool import *
from timer import *
from initialize import *
import options


# FIXME: this should really be refactored into a class that manages the
#        game screen (and handles most input), along with a "grid" class
#        that does nothing more than display jools and handle jool movements.

# Class that manages positions and movement of all 
# the various jools.
class Grid:
   def __init__(self):
      # the grid is initially blank.
      self.rows = []
      self.mousePressed = 0
      self.mousePressedPoint = pygame.mouse.get_pos()
      self.hasSwaps = 0
      self.hasFalseSwaps = 0
      self.rotatingPos = (-1, -1)
      self.pointsToAdd = 0
      self.totalPoints = 0
      self.chainTotal  = 0
      self.totalJools  = 0
      self.joolsThisLevel = 0
      self.timer = Timer()
      self.oldKeyTuple = pygame.key.get_pressed()
      self.spacePressed = 0
      self.joolsRect = pygame.Rect(SCREENW-8*JOOLSIZE, SCREENH-8*JOOLSIZE,
         8*JOOLSIZE, 8*JOOLSIZE)
      self.hasSelection1 = 0
      self.oldMousePos = (0,0)
      self.oldJoolPos = (-1,-1)
      self.oldMousePressed = 0
      self.usingKeyboard = 0

   # create a brand new grid of jools.
   def new(self, timeTrial):
      self.pointsToAdd = 0
      self.chainTotal = 0

      self.jools = []
      self.availableMoves = []
      
      for i in range(8):  # iterate through eight rows
         rows = []
         for j in range(8):  # iterate through eight jools per row
            tempJool = Jool(random.randint(0,6))
            tempJool.setPos((280 + JOOLSIZE*j, 60*i - 500))
            rows.append(tempJool) 
         self.jools.append(rows)

      # iterate through all jools again, to set the collision rects
      self.screenBottom = pygame.Rect(0, SCREENH, SCREENW, 500)
      for i in range(8):
         for j in range(8):
            if i == 7:
               # If the jool is in the bottom row, then the bottom of the
               # screen is the collision point
               self.jools[i][j].rect_below = self.screenBottom
            else:
               # For all other jools, the jool lying below determines the
               # collision rect
               self.jools[i][j].rect_below = self.jools[i+1][j].rect

      if timeTrial:
         self.timer.new(255, 125)
         self.timer.draw_border(screen)
         pygame.display.update(self.timer.draw(screen))

      self.oldSelection1 = self.jools[0][0]
      self.oldSelection2 = self.jools[0][0]
      self.oldSelPos1 = (0,0)

   
   # update the positions of all jools
   # (returns 1 if some jools were moved, else 0)
   # FIXME: Maintain a list of all jools that need to be updated at any
   #    point in time.  Don't need to waste time searching every jool.
   def updateAll(self, updateType):
      updatedSome = 0
      spritesToRender.empty()
      renderList = []
      self.cleanup_rects = []
      irange = range(8)
      irange.reverse()    # We need to move the lower jools before
                          #    the higher ones in order to avoid
                          #    collisions
      for i in irange:
         for j in range(8):
            # (deep copy; don't want this to change during update() )
            old_rect = pygame.Rect(self.jools[i][j].rect)   

            # If there was an update, we need to erase the old sprite and
            # draw the new one
            if self.jools[i][j].update(updateType):
               updatedSome = 1
               renderList.append(self.jools[i][j])
               self.cleanup_rects.append(old_rect)

      if updateType == UPDATEALL or updateType == UPDATESWAPS:
         self.hasSwaps = 0
         self.hasFalseSwaps = 0
      self.hasRotation = 0
      # Do a (sorta) Z-buffer sort so that the "top" jools get drawn last
      renderList.sort(joolZCmp)
      renderList.reverse()
      for jool in renderList:
         spritesToRender.add(jool)

      return updatedSome


   # clean the positions of sprites that just moved
   def cleanUpdated(self):
      for r in self.cleanup_rects:
         screen.blit(background, r, r)


   # draw sprites in the render list
   def renderUpdated(self):
      pygame.display.update(spritesToRender.draw(screen))
               

   # remove some jools, and drop in replacements
   # (takes a list of position tuples (row, col) as arguments)
   def removeJools(self, positions, tallyJools):
      positions.sort(posHeightCmp)  # Higher jools get removed first
      # FIXME: is this what we want to do?  I guess it would be better
      #        to clean the matches out of new grids before the jools
      #        even fall...
      if tallyJools:
         if self.chainTotal == 1:
            point1.play()
         elif self.chainTotal == 2:
            point2.play()
         elif self.chainTotal == 3:
            point3.play()
         elif self.chainTotal == 4:
            point4.play()
         elif self.chainTotal == 5:
            point5.play()
         else:
            point6.play()

      for pos in positions:
         if tallyJools:
            self.totalJools += 1
            self.joolsThisLevel += 1

         # the jools that are removed will have to be erased
         # (invoking deep copy)
         self.cleanup_rects.append(pygame.Rect(self.jools[pos[0]][pos[1]].rect))

         # the jool above the removed one gets a different collision rect now
         if pos[0] > 0:
            self.jools[pos[0]-1][pos[1]].rect_below = self.jools[pos[0]][pos[1]].rect_below

         # loop over all jools that lie above the removed one
         rowsToDrop = range(pos[0])
         rowsToDrop.reverse()
         for row in rowsToDrop:
            # those jools can move now
            self.jools[row][pos[1]].isFixed = 0
            # they need to be "renumbered"
            self.jools[row+1][pos[1]] = self.jools[row][pos[1]]

         tempJool = Jool(random.randint(0,6))
         tempJool.rect.top  = self.jools[0][pos[1]].rect.top - JOOLSIZE
         tempJool.rect.left = self.jools[0][pos[1]].rect.left
         tempJool.rect_below = self.jools[1][pos[1]].rect
         self.jools[0][pos[1]] = tempJool
      self.cleanUpdated()


   # Search the grid for matched jools in rows of
   # three or more.  Returns a list of jool positions
   # to remove, and the corresponding number of points to
   # add to the score.
   #
   # FIXME: plenty of room for optimization if necessary. (Doesn't look
   #        like it is necessary, however...)
   # 1) If we have done an exhaustive search previously, then we only
   #    need to search near areas that have changed.  (This one at least
   #    should be implemented.)
   # 2) If there is a match with a window of size 'n', then of course all
   #    smaller windows will match.  (Difficult to do cleanly.)
   # 3) If we *really* need to optimize, look for a completely new
   #    algorithm that starts by finding matched pairs, then expanding into
   #    rows of 3, etc.
   def getMatches(self, TallyPoints):
      # Slide a window of length 'windowLen' (which increments between 3
      # and 8) over the entire grid in both horizontal and vertical
      # orientations.  If all jools in the window match, add their
      # positions to a total match list.  
      matchedList = []
      points = 0   # Number of points scored for this set of matches ONLY,
                   # not counting chains.
      windowRange = range(3, 6)
      windowRange.reverse()
      for windowLen in windowRange:
         # Horizontal orientation
         for x in range(8-windowLen+1):
            for y in range(8):
               matched = 1
               joolType = self.jools[y][x].type
               for i in range(windowLen-1):
                  if self.jools[y][x+i+1].type != joolType:
                     matched = 0
                     break
               if matched:
                  newPos = 0
                  for i in range(windowLen):
                     tempPos = (y, x+i)
                     # Append only positions that are not already in the
                     # matchedList
                     if tempPos not in matchedList:
                        newPos = 1
                        matchedList.append(tempPos)
                  # Now worry about getting the points right.
                  if newPos:
                     # OK, this set of jools is not entirely contained in
                     # another set (e.g. 3 of a kind contained in 5 of
                     # a kind), so we can score it.
                     if windowLen == 3:
                        if points > 0:
                           points *= 2
                        else:
                           points = 10
                     elif windowLen == 4:
                        if points > 0:
                           points *= 3
                        else:
                           points = 30
                     elif windowLen == 5:
                        if points > 0:
                           points *= 4
                        else:
                           points = 80

         # Vertical orientation
         for y in range(8-windowLen+1):
            for x in range(8):
               matched = 1
               joolType = self.jools[y][x].type
               for i in range(windowLen-1):
                  if self.jools[y+i+1][x].type != joolType:
                     matched = 0
                     break
               if matched:
                  newPos = 0
                  for i in range(windowLen):
                     tempPos = (y+i, x)
                     # Append only positions that are not already in the
                     # matchedList
                     if tempPos not in matchedList:
                        newPos = 1
                        matchedList.append(tempPos)
                  # Now worry about getting the points right.
                  if newPos:
                     # OK, this set of jools is not entirely contained in
                     # another set (e.g. 3 of a kind contained in 5 of
                     # a kind), so we can score it.
                     if windowLen == 3:
                        if points > 0:
                           points *= 2
                        else:
                           points = 10
                     elif windowLen == 4:
                        if points > 0:
                           points *= 3
                        else:
                           points = 30
                     elif windowLen == 5:
                        if points > 0:
                           points *= 4
                        else:
                           points = 80

      # If this is a chain, there's an extra points bonus
      if points > 0:
         if self.pointsToAdd > 0:
            self.pointsToAdd += points
            self.pointsToAdd *= 2
            self.chainTotal += 1
         else:
            self.pointsToAdd = points
            self.chainTotal = 1
      else:
         self.chainTotal = 1
         self.pointsToAdd = 0

      # Only add the points if this grid is not new
      if TallyPoints:
         self.totalPoints += self.pointsToAdd

      return matchedList


   # Find all possible moves.
   # Save each move as a pair of positions to swap, with the left/top
   # position first.
   #
   # FIXME: Again, lots of room for optimization here.  Primarily, if most
   # of the screen is unchanged then we don't need to scan it.
   def updateMoves(self):
      # Slide masks across the grid.
      # If all jools in those masks match, then there is a move with an
      # associated tuple.  Add the move to the list if it isn't in there
      # already.

      moveList = []
      # Using the masks:
      #
      # XX X    X XX
      #
      for i in range(5):
         for j in range(8):
            # Horizontal orientation
            temp = self.jools[j][i].type
            if self.jools[j][i+3].type == temp:
               if self.jools[j][i+1].type == temp:
                  move = ( (j,i+2), (j,i+3) )
                  if move not in moveList:
                     moveList.append(move)
               if self.jools[j][i+2].type == temp:
                  move = ( (j,i), (j,i+1) )
                  if move not in moveList:
                     moveList.append(move)
            # Vertical orientation
            temp = self.jools[i][j].type
            if self.jools[i+3][j].type == temp:
               if self.jools[i+1][j].type == temp:
                  move = ( (i+2,j), (i+3,j) )
                  if move not in moveList:
                     moveList.append(move)
               if self.jools[i+2][j].type == temp:
                  move = ( (i,j), (i+1,j) )
                  if move not in moveList:
                     moveList.append(move)

      # using the masks:
      #
      # XX    X      XX     X     X     X X                               
      #   X    XX   X     XX     X X     X                                   
      #
      for i in range(6):
         for j in range(7):
            # Horizontal orientation
            # first two
            temp = self.jools[j][i].type
            if self.jools[j+1][i+2].type == temp:
               if self.jools[j][i+1].type == temp:
                  move = ( (j,i+2), (j+1,i+2) )
                  if move not in moveList:
                     moveList.append(move)
               if self.jools[j+1][i+1].type == temp:
                  move = ( (j,i), (j+1,i) )
                  if move not in moveList:
                     moveList.append(move)
            # middle two
            temp = self.jools[j+1][i].type
            if self.jools[j][i+2].type == temp:
               if self.jools[j][i+1].type == temp:
                  move = ( (j,i), (j+1,i) )
                  if move not in moveList:
                     moveList.append(move)
               if self.jools[j+1][i+1].type == temp:
                  move = ( (j,i+2), (j+1,i+2) )
                  if move not in moveList:
                     moveList.append(move)
            # last two
            temp = self.jools[j+1][i].type
            if (self.jools[j][i+1].type == temp and
               self.jools[j+1][i+2].type == temp):
               move = ( (j,i+1), (j+1,i+1) )
               if move not in moveList:
                  moveList.append(move)
            temp = self.jools[j][i].type
            if (self.jools[j+1][i+1].type == temp and
               self.jools[j][i+2].type == temp):
               move = ( (j,i+1), (j+1,i+1) )
               if move not in moveList:
                  moveList.append(move)

            # Vertical orientation
            # first two
            temp = self.jools[i][j].type
            if self.jools[i+2][j+1].type == temp:
               if self.jools[i+1][j].type == temp:
                  move = ( (i+2,j), (i+2,j+1) )
                  if move not in moveList:
                     moveList.append(move)
               if self.jools[i+1][j+1].type == temp:
                  move = ( (i,j), (i,j+1) )
                  if move not in moveList:
                     moveList.append(move)
            # middle two
            temp = self.jools[i][j+1].type
            if self.jools[i+2][j].type == temp:
               if self.jools[i+1][j].type == temp:
                  move = ( (i,j), (i,j+1) )
                  if move not in moveList:
                     moveList.append(move)
               if self.jools[i+1][j+1].type == temp:
                  move = ( (i+2,j), (i+2,j+1) )
                  if move not in moveList:
                     moveList.append(move)
            # last two
            temp = self.jools[i][j+1].type
            if (self.jools[i+1][j].type == temp and
               self.jools[i+2][j+1].type == temp):
               move = ( (i+1,j), (i+1,j+1) )
               if move not in moveList:
                  moveList.append(move)
            temp = self.jools[i][j].type
            if (self.jools[i+1][j+1].type == temp and
               self.jools[i+2][j].type == temp):
               move = ( (i+1,j), (i+1,j+1) )
               if move not in moveList:
                  moveList.append(move)

      self.availableMoves = moveList




   # control remains in this loop while pause is active
   def pauseLoop(self):
      screen.blit(background, (640-(8*JOOLSIZE), 480-(8*JOOLSIZE)))
      screen.blit(pausedImage, (SCREENW-(8*JOOLSIZE) + ((8*JOOLSIZE)-pausedRect.width)/2, 
         (SCREENH-(8*JOOLSIZE) + ((8*JOOLSIZE)-pausedRect.height)/2)))
      pygame.display.update()

      if audio.Capable and audio.musicEnabled:
         pygame.mixer.music.pause()

      # loop while paused
      while 1:
         pygame.time.wait(READ_INPUT_DELAY)
         pygame.event.pump()
         if pygame.event.peek(QUIT):
            options.saveOptions()
            pygame.quit()
            sys.exit("Exiting Jools...")
         keyTuple = pygame.key.get_pressed()
         if not self.oldKeyTuple[pygame.K_p] and keyTuple[pygame.K_p]:
            # djk: unpause music
            if audio.Capable and audio.musicEnabled:
               pygame.mixer.music.unpause()
            # unpaused... r edraw all jools.
            screen.blit(background, (640-(8*JOOLSIZE), 480-(8*JOOLSIZE)))
            self.updateAll(UPDATEREDRAW)
            self.renderUpdated()
            pygame.display.flip()
            self.oldKeyTuple = keyTuple
            break
         elif not self.oldKeyTuple[pygame.K_q] and keyTuple[pygame.K_q]:
            screen.blit(background, (640-(8*JOOLSIZE), 480-(8*JOOLSIZE)))
            self.updateAll(UPDATEREDRAW)
            self.renderUpdated()
            pygame.display.flip()
            self.oldKeyTuple = keyTuple
            return 1
         self.oldKeyTuple = keyTuple


   # enable and disable sound effects
   def toggleSound(self):
      if audio.soundEnabled == 1:
         audio.soundEnabled = 0
         audioDisplay.draw()
      elif audio.Capable:
         audio.soundEnabled = 1
         audioDisplay.draw()

   # enable and disable music
   def toggleMusic(self):
      if audio.musicEnabled == 1:
         audio.musicEnabled = 0
         audioDisplay.draw()
         if audio.Capable:
            pygame.mixer.music.stop()
      elif audio.Capable:
         audio.musicEnabled = 1
         audioDisplay.draw()
         try:
            pygame.mixer.music.play(-1)
         except:
            print "Warning: no music loaded."




   # Check for (and handle) mouse clicks, drags, etc.
   # FIXME: this is badly in need of refactoring, now that there are
   #        more types of input to scan for.
   def processInput(self):
      # djk: handle minimization during gameplay
      if pygame.event.peek(ACTIVEEVENT):
         evtList = pygame.event.get(ACTIVEEVENT)
         #evt = evtList.pop()
         for evt in evtList:
            #print( evt.dict )
            if( (evt.state == 6) and (evt.gain == 0) ):
               self.pauseLoop()

      keyTuple = pygame.key.get_pressed()

      # process keypresses for pause, quit, sound toggling
      if not self.oldKeyTuple[pygame.K_p] and keyTuple[pygame.K_p]:
         self.oldKeyTuple = keyTuple
         self.pauseLoop()
      elif not self.oldKeyTuple[pygame.K_q] and keyTuple[pygame.K_q]:
         return 1
      elif not self.oldKeyTuple[pygame.K_s] and keyTuple[pygame.K_s]:
         self.toggleSound()
      elif not self.oldKeyTuple[pygame.K_m] and keyTuple[pygame.K_m]:
         self.toggleMusic()


      # check whether the user clicked on the audio status icons
      mousePos = pygame.mouse.get_pos()
      mousePressed = pygame.mouse.get_pressed()[0]
      if self.oldMousePressed:
         if not mousePressed:
            # check whether the user clicked on the audio status icons
            if audioDisplay.notesRect.collidepoint(mousePos):
               self.toggleMusic()
            elif audioDisplay.megaRect.collidepoint(mousePos):
               self.toggleSound()


      joolPos = self.oldJoolPos

      # jool selection and swapping
      # first, check for mouse movement
      if self.joolsRect.collidepoint(mousePos):
         if mousePos != self.oldMousePos or not self.usingKeyboard:
            self.usingKeyboard = 0
            pygame.mouse.set_visible(1)
            joolPos = ((mousePos[1] - (SCREENH-8*JOOLSIZE))/JOOLSIZE,
                       (mousePos[0] - (SCREENW-8*JOOLSIZE))/JOOLSIZE)
            if not self.hasSelection1:
               self.selectNewSecondary(joolPos, self.oldJoolPos)
            else:
               if (abs(self.sel1Pos[0]-joolPos[0]) +
                   abs(self.sel1Pos[1]-joolPos[1]) == 1):
                  if self.oldJoolPos != self.sel1Pos:
                     self.selectNewSecondary(joolPos, self.oldJoolPos)
                  else:
                     # careful not to erase the primary selection
                     self.hasRotation = 1
                     self.jools[joolPos[0]][joolPos[1]].isRotating = 1
                     self.jools[joolPos[0]][joolPos[1]].selectionStatus = SEL_SECONDARY 
                     self.jools[joolPos[0]][joolPos[1]].drawSelection()
                     self.jools[self.oldJoolPos[0]][self.oldJoolPos[1]].needsRotateCleanup = 1
               else:
                  self.hasRotation = 1
                  self.jools[joolPos[0]][joolPos[1]].isRotating = 1
                  if self.oldJoolPos != self.sel1Pos:
                     self.jools[self.oldJoolPos[0]][self.oldJoolPos[1]].selectionStatus = SEL_UNSELECTED
                     self.jools[self.oldJoolPos[0]][self.oldJoolPos[1]].needsRotateCleanup = 1
      else:
         if not self.usingKeyboard:
            self.jools[self.oldJoolPos[0]][self.oldJoolPos[1]].needsRotateCleanup = 1

      # make mouse pointer visible if there is any mouse activity
      if mousePos != self.oldMousePos or mousePressed:
         self.usingKeyboard = 0
         pygame.mouse.set_visible(1)



      # check for arrow key movement
      if ((keyTuple[pygame.K_UP] and not self.oldKeyTuple[pygame.K_UP]) or
          (keyTuple[pygame.K_k] and not self.oldKeyTuple[pygame.K_k])):
         self.usingKeyboard = 1
         if self.oldJoolPos == (-1,-1):
            joolPos = (0,0)
         elif self.oldJoolPos[0] > 0:
            joolPos = (self.oldJoolPos[0] - 1, self.oldJoolPos[1])
         else:
            joolPos = (7, self.oldJoolPos[1])
         self.doKeyboardSelection(joolPos)
      elif ((keyTuple[pygame.K_DOWN] and not self.oldKeyTuple[pygame.K_DOWN]) or
          (keyTuple[pygame.K_j] and not self.oldKeyTuple[pygame.K_j])):
         self.usingKeyboard = 1
         if self.oldJoolPos == (-1,-1):
            joolPos = (0,0)
         elif self.oldJoolPos[0] < 7:
            joolPos = (self.oldJoolPos[0] + 1, self.oldJoolPos[1])
         else:
            joolPos = (0, self.oldJoolPos[1])
         self.doKeyboardSelection(joolPos)
      elif ((keyTuple[pygame.K_LEFT] and not self.oldKeyTuple[pygame.K_LEFT]) or
          (keyTuple[pygame.K_h] and not self.oldKeyTuple[pygame.K_h])):
         self.usingKeyboard = 1
         if self.oldJoolPos == (-1,-1):
            joolPos = (0,0)
         elif self.oldJoolPos[1] > 0:
            joolPos = (self.oldJoolPos[0], self.oldJoolPos[1] - 1)
         else:
            joolPos = (self.oldJoolPos[0], 7)
         self.doKeyboardSelection(joolPos)
      elif ((keyTuple[pygame.K_RIGHT] and not self.oldKeyTuple[pygame.K_RIGHT]) or
          (keyTuple[pygame.K_l] and not self.oldKeyTuple[pygame.K_l])):
         self.usingKeyboard = 1
         if self.oldJoolPos == (-1,-1):
            joolPos = (0,0)
         elif self.oldJoolPos[1] < 7:
            joolPos = (self.oldJoolPos[0], self.oldJoolPos[1] + 1)
         else:
            joolPos = (self.oldJoolPos[0], 0)
         self.doKeyboardSelection(joolPos)


      # catch a few items that don't fall under the above
      if self.usingKeyboard:
         pygame.mouse.set_visible(0)
         self.hasRotation = 1
         self.jools[joolPos[0]][joolPos[1]].isRotating = 1
         if not self.hasSelection1:
            self.selectNewSecondary(joolPos, joolPos)



      # check for mouse button activity
      if self.joolsRect.collidepoint(mousePos):
         if mousePressed:
            self.usingKeyboard = 0
            if not self.hasSelection1:
               if not self.oldMousePressed:
                  joolPos = ((mousePos[1] - (SCREENH-8*JOOLSIZE))/JOOLSIZE,
                             (mousePos[0] - (SCREENW-8*JOOLSIZE))/JOOLSIZE)
                  self.selectNewSecondary(joolPos, self.oldJoolPos)
                  self.selectPrimary(joolPos) 
            else:
               if not self.oldMousePressed:
                  joolPos = ((mousePos[1] - (SCREENH-8*JOOLSIZE))/JOOLSIZE,
                             (mousePos[0] - (SCREENW-8*JOOLSIZE))/JOOLSIZE)
                  if joolPos == self.sel1Pos:
                     self.deselectPrimary()
                  elif (abs(self.sel1Pos[0]-joolPos[0]) +
                        abs(self.sel1Pos[1]-joolPos[1]) == 1):
                     temp = self.sel1Pos
                     self.deselectPrimary()
                     self.jools[joolPos[0]][joolPos[1]].selectionStatus = SEL_UNSELECTED
                     self.jools[joolPos[0]][joolPos[1]].needsRotateCleanup = 1
                     self.trySwap(temp, joolPos)
         else:
            if self.hasSelection1:
               if self.oldMousePressed:
                  if (abs(self.sel1Pos[0]-joolPos[0]) +
                        abs(self.sel1Pos[1]-joolPos[1]) == 1):
                     temp = self.sel1Pos
                     self.deselectPrimary()
                     self.jools[joolPos[0]][joolPos[1]].selectionStatus = SEL_UNSELECTED
                     self.jools[joolPos[0]][joolPos[1]].needsRotateCleanup = 1
                     self.trySwap(temp, joolPos)
                  elif joolPos != self.sel1Pos:
                     self.deselectPrimary()

                
      # check for keyboard selection activity
      if keyTuple[pygame.K_SPACE]:
         self.usingKeyboard = 1
         if not self.oldKeyTuple[pygame.K_SPACE]:
            if not self.hasSelection1:
               self.hasSelection1 = 1
               self.selectPrimary(joolPos) 
            else:
               if joolPos == self.sel1Pos:
                  self.deselectPrimary()
               elif (abs(self.sel1Pos[0]-joolPos[0]) +
                     abs(self.sel1Pos[1]-joolPos[1]) == 1):
                  temp = self.sel1Pos
                  self.deselectPrimary()
                  self.jools[joolPos[0]][joolPos[1]].selectionStatus = SEL_UNSELECTED
                  self.jools[joolPos[0]][joolPos[1]].needsRotateCleanup = 1
                  self.trySwap(temp, joolPos)
      else:
         if self.hasSelection1:
            if self.oldKeyTuple[pygame.K_SPACE]:
               if (abs(self.sel1Pos[0]-joolPos[0]) +
                     abs(self.sel1Pos[1]-joolPos[1]) == 1):
                  temp = self.sel1Pos
                  self.deselectPrimary()
                  self.jools[joolPos[0]][joolPos[1]].selectionStatus = SEL_UNSELECTED
                  self.jools[joolPos[0]][joolPos[1]].needsRotateCleanup = 1
                  self.trySwap(temp, joolPos)
               elif joolPos != self.sel1Pos:
                  self.deselectPrimary()

      # prepare for next iteration of processInput()
      self.oldMousePos     = mousePos
      self.oldMousePressed = mousePressed
      self.oldKeyTuple     = keyTuple
      self.oldJoolPos      = joolPos




   # the following functions are used for highlighting jools in
   # various colored boxes
   def selectNewSecondary(self, newPos, oldPos):
      self.hasRotation = 1
      self.jools[newPos[0]][newPos[1]].isRotating = 1
      self.jools[newPos[0]][newPos[1]].selectionStatus = SEL_SECONDARY 
      self.jools[newPos[0]][newPos[1]].drawSelection()
      if newPos != oldPos:
         self.jools[oldPos[0]][oldPos[1]].selectionStatus = SEL_UNSELECTED
         self.jools[oldPos[0]][oldPos[1]].needsRotateCleanup = 1

   def selectNewTertiary(self, newPos, oldPos):
      self.hasRotation = 1
      self.jools[newPos[0]][newPos[1]].isRotating = 1
      self.jools[newPos[0]][newPos[1]].selectionStatus = SEL_TERTIARY
      self.jools[newPos[0]][newPos[1]].drawSelection()
      if newPos != oldPos:
         if oldPos != self.sel1Pos:
            self.jools[oldPos[0]][oldPos[1]].selectionStatus = SEL_UNSELECTED
         self.jools[oldPos[0]][oldPos[1]].needsRotateCleanup = 1

   def selectPrimary(self, pos):
      self.hasSelection1 = 1
      self.sel1Pos = pos
      self.jools[pos[0]][pos[1]].selectionStatus = SEL_PRIMARY
      self.jools[pos[0]][pos[1]].drawSelection()

   def deselectPrimary(self):
      self.hasSelection1 = 0
      self.jools[self.sel1Pos[0]][self.sel1Pos[1]].selectionStatus = SEL_UNSELECTED
      self.jools[self.sel1Pos[0]][self.sel1Pos[1]].needsRotateCleanup = 1
      self.sel1Pos = (-1,-1)
      


   # this is called to select new jools when the arrow keys are pressed
   def doKeyboardSelection(self, newPos):
      if not self.hasSelection1:
         self.selectNewSecondary(newPos, self.oldJoolPos)
      else:
         if (abs(self.sel1Pos[0]-newPos[0]) +
             abs(self.sel1Pos[1]-newPos[1]) == 1):
            if self.oldJoolPos != self.sel1Pos:
               self.selectNewSecondary(newPos, self.oldJoolPos)
            else:
               # careful not to erase the primary selection
               self.hasRotation = 1
               self.jools[newPos[0]][newPos[1]].isRotating = 1
               self.jools[newPos[0]][newPos[1]].selectionStatus = SEL_SECONDARY 
               self.jools[newPos[0]][newPos[1]].drawSelection()
               self.jools[self.oldJoolPos[0]][self.oldJoolPos[1]].needsRotateCleanup = 1
         else:
            self.hasRotation = 1
            self.jools[newPos[0]][newPos[1]].isRotating = 1
            if newPos != self.sel1Pos:
               self.selectNewTertiary(newPos, self.oldJoolPos)
            else:
               self.jools[self.oldJoolPos[0]][self.oldJoolPos[1]].selectionStatus = SEL_UNSELECTED
               self.jools[self.oldJoolPos[0]][self.oldJoolPos[1]].needsRotateCleanup = 1

   





   # Swap two jools, given the grid positions of the two to swap
   def swapByPosition(self, pos1, pos2):
      m1, n1 = pos1
      m2, n2 = pos2
      if ((pos1, pos2) in self.availableMoves or 
         (pos2, pos1) in self.availableMoves):
         # Swap the objects in memory
         tempJool = self.jools[m1][n1]
         self.jools[m1][n1] = self.jools[m2][n2]
         self.jools[m2][n2] = tempJool
         # Mark those jools as swapped, so they will be caught
         # in the next graphics update
         self.jools[m1][n1].isSwapped = 1
         self.jools[m2][n2].isSwapped = 1
         self.jools[m1][n1].isOnTop = 1      # the selected jool is drawn last
         if n2 > n1:
            self.jools[m1][n1].swapDir = 3   # moving left
            self.jools[m2][n2].swapDir = 1   # moving right
         elif n2 < n1:
            self.jools[m1][n1].swapDir = 1   # moving right
            self.jools[m2][n2].swapDir = 3   # moving left
         elif m2 > m1:
            self.jools[m1][n1].swapDir = 4   # moving up
            self.jools[m2][n2].swapDir = 2   # moving down
         elif m2 < m1:
            self.jools[m1][n1].swapDir = 2   # moving down
            self.jools[m2][n2].swapDir = 4   # moving up

         # Update the collision rects
         # The jools above the swapped jools need to have new 
         #    collision rects
         if m1 > 0:
            self.jools[m1-1][n1].rect_below = self.jools[m1][n1].rect
         if m2 > 0:
            self.jools[m2-1][n2].rect_below = self.jools[m2][n2].rect
         # The swapped jools themselves need to have new collision
         #    rects
         if m1 < 7:
            self.jools[m1][n1].rect_below = self.jools[m1+1][n1].rect
         else:
            self.jools[m1][n1].rect_below = self.screenBottom
         if m2 < 7:
            self.jools[m2][n2].rect_below = self.jools[m2+1][n2].rect
         else:
            self.jools[m2][n2].rect_below = self.screenBottom
            
         # The swapped jools may need a rotation cleanup animation.
         if self.jools[m1][n1].imageNum > 0:
            self.jools[m1][n1].needsRotateCleanup = 1
         if self.jools[m2][n2].imageNum > 0:
            self.jools[m2][n2].needsRotateCleanup = 1

      # Illegal move; enter false swap animation.
      else:
         # Mark those jools as swapped, so they will be caught
         # in the next graphics update
         self.hasFalseSwaps = 1
         self.jools[m1][n1].isFalseSwapped = 1
         self.jools[m2][n2].isFalseSwapped = 1
         self.jools[m2][n2].isOnTop = 1      # the selected jool is drawn last
         if n2 > n1:
            self.jools[m1][n1].swapDir = 1   # moving left
            self.jools[m2][n2].swapDir = 3   # moving right
         elif n2 < n1:
            self.jools[m1][n1].swapDir = 3   # moving right
            self.jools[m2][n2].swapDir = 1   # moving left
         elif m2 > m1:
            self.jools[m1][n1].swapDir = 2   # moving up
            self.jools[m2][n2].swapDir = 4   # moving down
         elif m2 < m1:
            self.jools[m1][n1].swapDir = 4   # moving down
            self.jools[m2][n2].swapDir = 2   # moving up

         # The swapped jools may need a rotation cleanup animation.
         if self.jools[m1][n1].imageNum > 0:
            self.jools[m1][n1].needsRotateCleanup = 1
         if self.jools[m2][n2].imageNum > 0:
            self.jools[m2][n2].needsRotateCleanup = 1



   # Try swapping the positions of two jools.  Checks whether the two jools
   # are adjacent.  If the jools are adjacent but a swap will not create a
   # match, then the call to self.swapByPosition() will do a "false swap".
   def trySwap(self, joolPos1, joolPos2):
      if ((joolPos2 != joolPos1) and
             (abs(joolPos2[0]-joolPos1[0]) +
              abs(joolPos2[1]-joolPos1[1]) == 1)):
         self.swapByPosition(joolPos1, joolPos2)
         self.hasSwaps = 1



# arch-tag: jool grid manager class

