#
# database.py
#
# by Joern Thyssen <jth@gnubg.org>, 2004
#
# This file contains the functions for adding matches to a relational
# database.
#
# The modules use the DB API V2 python modules. 
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#
# 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
#
# $Id: database.py,v 1.4 2004/04/23 20:20:37 thyssen Exp $
#
                                                                                  

import gnubg
import sys

class relational:

   def __init__(self):

      self.conn = None

   #
   # Return next free "id" from the given table
   # Paramters: conn (the database connection, DB API v2)
   #            tablename (the tablename)
   # Returns: next free "id"
   #

   def __next_id(self,tablename):

      # fetch next_id from control table

      q = "SELECT next_id FROM gnubg.control WHERE tablename = '%s'" % (tablename)

      c = self.conn.cursor()
      c.execute(q)

      row = c.fetchone()

      c.close()

      if row:
     
         next_id = row[0]

         # update control data with new next id
         
         q = "UPDATE gnubg.control SET next_id = %d WHERE tablename = '%s'" \
             % (next_id+1, tablename)
         
         c = self.conn.cursor()
         c.execute(q)
         c.close()
     
      else:
         next_id = 0

         # insert next id

         q = "INSERT INTO gnubg.control (tablename,next_id) VALUES ('%s',%d)" \
             % (tablename,next_id+1)
         
         c = self.conn.cursor()
         c.execute(q)
         c.close()
      
      return next_id

   #
   # Add players to database
   #
   # Parameters: conn (the database connection, DB API v2)
   #             name (the name of the player)
   #             env_id (the env. of the match)
   #
   # Returns: 0 on success, non-zero otherwise
   #
   
   def __addPlayer(self,name,env_id):
      
      # check if player exists
      
      query = "SELECT per.person_id FROM gnubg.player pla INNER JOIN gnubg.person per ON pla.person_id = per.person_id  WHERE per.name = '%s'" % ( name )
      
      cursor = self.conn.cursor()
      cursor.execute(query)
      
      row = cursor.fetchone()
      cursor.close()
      
      if row:
         # player already exists
         return row[0]
      else:
         # player does not exist
         
         # Insert person
         
         person_id = self.__next_id("person")
         
         query = "INSERT INTO gnubg.person (person_id,name,notes) VALUES (%d,'%s','')" % ( person_id, name )
         cursor = self.conn.cursor()
         cursor.execute(query)
         cursor.close()
         
         # Insert player
         
         query = "INSERT INTO gnubg.player (person_id,env_id) VALUES (%d,%d)" % (person_id, env_id)
         cursor = self.conn.cursor()
         cursor.execute(query)
         cursor.close()
         
         return person_id

   #
   # getKey: return key if found in dict, otherwise return empty string
   #
   #

   def __getKey(self,d,key):
      
      if d.has_key(key):
         return d[key]
      else:
         return ''

   #
   # Calculate a/b, but return 0 for b=0
   #

   def __calc_rate(self,a,b):
      if b == 0:
         return 0.0
      else:
         return a/b

   #
   # Add statistics
   # Parameters: conn (the database connection, DB API v2)
   #             person_id (the person id for this statistics)
   #             gs (the game/match statistics)
   #
   
   def __addStat(self,match_id,person_id,gs,gs_other):
      
      # identification
      s1 = "%d,%d," % (match_id, person_id )
      
      # chequer play statistics
      chs = gs[ 'moves' ]
      m = chs[ 'marked' ]
      s2 = "%d,%d,%d,%d,%d,%d,%d,%f,%f,%f,%f,%d," % \
           ( chs[ 'total-moves' ], \
             chs[ 'unforced-moves' ], \
             m[ 'unmarked' ], \
             m[ 'good' ], \
             m[ 'doubtful' ], \
             m[ 'bad' ], \
             m[ 'very bad' ], \
             chs[ 'error-skill' ], \
             chs[ 'error-cost' ], \
             self.__calc_rate( chs[ 'error-skill' ], chs[ 'unforced-moves' ] ), \
             self.__calc_rate( chs[ 'error-cost' ], chs[ 'unforced-moves' ] ), \
             gnubg.errorrating( self.__calc_rate( chs[ 'error-skill' ],
                                             chs[ 'unforced-moves' ] ) ) ) 


      # luck statistics

      lus = gs[ 'dice' ]
      m = lus[ 'marked-rolls' ]
      s3 = "%d,%d,%d,%d,%d,%f,%f,%f,%f,%d," % \
           ( m[ 'verygood' ], \
             m[ 'good' ], \
             m[ 'unmarked' ], \
             m[ 'bad' ], \
             m[ 'verybad' ], \
             lus[ 'luck' ], \
             lus[ 'luck-cost' ], \
             self.__calc_rate( lus[ 'luck' ], chs[ 'total-moves' ] ), \
             self.__calc_rate( lus[ 'luck-cost' ], chs[ 'total-moves' ] ), \
             gnubg.luckrating( self.__calc_rate( lus[ 'luck' ],
                                            chs[ 'total-moves' ] ) ) ) 

      # cube statistics
   
      cus = gs[ 'cube' ]
      s4 = "%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%d," % \
           ( cus[ 'total-cube' ], \
             cus[ 'close-cube' ], \
             cus[ 'n-doubles' ], \
             cus[ 'n-takes' ], \
             cus[ 'n-drops' ], \
             cus[ 'missed-double-below-cp' ], \
             cus[ 'missed-double-below-cp' ], \
             cus[ 'wrong-double-below-dp' ], \
             cus[ 'wrong-double-above-tg' ], \
             cus[ 'wrong-take' ], \
             cus[ 'wrong-drop' ], \
             cus[ 'err-missed-double-below-cp-skill' ], \
             cus[ 'err-missed-double-above-cp-skill' ], \
             cus[ 'err-wrong-double-below-dp-skill' ], \
             cus[ 'err-wrong-double-above-tg-skill' ], \
             cus[ 'err-wrong-take-skill' ], \
             cus[ 'err-wrong-drop-skill' ], \
             cus[ 'err-missed-double-below-cp-cost' ], \
             cus[ 'err-missed-double-above-cp-cost' ], \
             cus[ 'err-wrong-double-below-dp-cost' ], \
             cus[ 'err-wrong-double-above-tg-cost' ], \
             cus[ 'err-wrong-take-cost' ], \
             cus[ 'err-wrong-drop-cost' ], \
             cus[ 'error-skill' ], \
             cus[ 'error-cost' ], \
             self.__calc_rate( cus[ 'error-skill' ], cus[ 'close-cube' ] ), \
             self.__calc_rate( cus[ 'error-cost' ], cus[ 'close-cube' ] ), \
             gnubg.errorrating( self.__calc_rate( cus[ 'error-skill' ],
                                             cus[ 'close-cube' ] ) ) ) 
      
      # overall
      
      s5 = "%f,%f,%f,%f,%d,%f,%f,%f," % \
           ( cus[ 'error-skill' ] + chs[ 'error-skill' ], \
             cus[ 'error-cost' ] + chs[ 'error-cost' ], \
             self.__calc_rate( cus[ 'error-skill' ] + chs[ 'error-skill' ], \
                          cus[ 'close-cube' ] + chs[ 'unforced-moves' ] ),
             self.__calc_rate( cus[ 'error-cost' ] + chs[ 'error-cost' ], \
                          cus[ 'close-cube' ] + chs[ 'unforced-moves' ] ),
             
             gnubg.errorrating( self.__calc_rate( cus[ 'error-skill' ] +
                                             chs[ 'error-skill' ], \
                                             cus[ 'close-cube' ] +
                                             chs[ 'unforced-moves' ] ) ),
             lus[ 'actual-result' ],
             lus[ 'luck-adjusted-result' ],
             self.__calc_rate( cus[ 'error-skill' ] + chs[ 'error-skill' ], \
                          chs[ 'total-moves' ] +
                          gs_other[ 'moves' ][ 'total-moves' ] ) )


      # for matches only
      if lus.has_key( 'fibs-rating-difference' ):
         s6 = '%f,' % ( lus[ 'fibs-rating-difference' ] )
      else:
         s6 = 'NULL,'

      if gs.has_key( 'error-based-fibs-rating' ):
         s = gs[ 'error-based-fibs-rating' ]
         if s.has_key( 'total' ):
            s6 = s6 + "%f," % ( s[ 'total' ] )
         else:
            s6 = s6 + "NULL,"
         if s.has_key( 'chequer' ):
            s6 = s6 + "%f," % ( s[ 'chequer' ] )
         else:
            s6 = s6 + "NULL,"
         if s.has_key( 'cube' ):
            s6 = s6 + "%f," % ( s[ 'cube' ] )
         else:
            s6 = s6 + "NULL,"
      else:
         s6 = s6 + "NULL,NULL,NULL,"

      # for money sessions only
      if gs.has_key( 'ppg-advantage' ):
         s = gs[ 'ppg-advantage' ]
         s7 = "%f,%f,%f,%f," % ( \
         s[ 'actual' ], \
         s[ 'actual-ci' ], \
         s[ 'luck-adjusted' ], \
         s[ 'luck-adjusted-ci' ] )
      else:
         s7 = "NULL,NULL,NULL,NULL,"

      # time penalties
      s8 = "%d,%f,%f" % \
           ( gs[ 'time' ][ 'time-penalty' ], \
             gs[ 'time' ][ 'time-penalty-skill' ], \
             gs[ 'time' ][ 'time-penalty-cost' ] )
      
      query = "INSERT INTO gnubg.matchstat VALUES (" + \
              s1 + s2 + s3 + s4 + s5 + s6 + s7 + s8 + ");"
      cursor = self.conn.cursor()
      cursor.execute(query)
      cursor.close()
      
      return 0

   #
   # Add match
   # Parameters: conn (the database connection, DB API v2)
   #             env_id (the env)
   #             person_id0 (person_id of player 0)
   #             person_id1 (person_id of player 1)
   #             m (the gnubg match)             
   
   def __addMatch(self,env_id,person_id0,person_id1,m):

      # add match

      match_id = self.__next_id("match")
      
      mi = m[ 'match-info' ]
      
      if mi.has_key( 'date' ):
         date = "'%04d-%02d-%02d'" % ( mi[ 'date' ][ 2 ],
                                       mi[ 'date' ][ 1 ],
                                       mi[ 'date' ][ 0 ] )
      else:
         date = "NULL"
         
      query = "INSERT INTO gnubg.match (match_id,env_id0,person_id0,env_id1,person_id1,result,length,added,rating0,rating1,event,round,place,annotator,comment,date) VALUES (%d,%d,%d,%d,%d,%d,%d,CURRENT_TIMESTAMP,'%s','%s','%s','%s','%s','%s','%s',%s) " % \
              (match_id,env_id,person_id0,env_id,person_id1, \
               -1,mi[ 'match-length' ],
               self.__getKey( mi[ 'X' ], 'rating' )[0:80],
               self.__getKey( mi[ 'O' ], 'rating' )[0:80],
               self.__getKey( mi, 'event' )[0:80],
               self.__getKey( mi, 'round' )[0:80],
               self.__getKey( mi, 'place' )[0:80],
               self.__getKey( mi, 'annotator' )[0:80],
               self.__getKey( mi, 'comment' )[0:80], date )
      
      cursor = self.conn.cursor()
      cursor.execute(query)
      cursor.close()

      # add match statistics for both players

      if self.__addStat(match_id,person_id0,m[ 'stats' ][ 'X' ],
                   m [ 'stats' ][ 'O' ]) == None:
         print "Error adding player 0's stat to database."
         return None

      if self.__addStat(match_id,person_id1,m[ 'stats' ][ 'O' ],
                   m [ 'stats' ][ 'X' ]) == None:
         print "Error adding player 1's stat to database."
         return None
      
      return match_id
         
   
   #
   # Add match to database
   # Parameters: conn (the database connection, DB API v2)
   #             m (the match as obtained from gnubg)
   #             env_id (the environment of the match)
   # Returns: n/a
   #
   
   def __add_match(self,m,env_id):

      # add players

      mi = m[ 'match-info' ]
      
      person_id0 = self.__addPlayer( mi[ 'X' ][ 'name' ], env_id)
      if person_id0 == None:
         return None
      
      person_id1 = self.__addPlayer( mi[ 'O' ][ 'name' ], env_id)
      if person_id1 == None:
         return None
      
      # add match

      match_id = self.__addMatch( env_id, person_id0, person_id1, m)
      if match_id == None:
         return None

      return match_id

   #
   # Check for existing match
   #
   #

   def is_existing(self,m,env_id):

      return 0

   #
   # Main logic
   #

   def connect(self):

      # Import the DB API v2 postgresql module
      import pgdb
      import _pg
      try:
         self.conn = pgdb.connect(dsn=':gnubg')
         return self.conn
      except _pg.error, e:
         print e
      except:
         print "Unhandled..."

      return None

   def disconnect(self):
         
      if self.conn == None:
         return

      self.conn.close()

   def addmatch(self,env_id=0,force=0):

      if self.conn == None:
         return -3

      m = gnubg.match(0,0,1,0)
      if m:
         if force or self.is_existing(m,env_id) == 0:
            match_id = self.__add_match(m,env_id)
            if match_id <> None:
               self.conn.commit()
               # Match successfully added to database
               return 0
            else:
               # Error add match to database
               return -1
         else:
            # Match is already in the database
            return -3
      else:
         # No match
         return -2

   def test(self):

      # connection test 

      self.connect()
      if self.conn == None:
         return -1

      # verify that the main table is present

      rc = 0

      cursor = self.conn.cursor()
      try:
         cursor.execute("SELECT COUNT(*) from gnubg.match;")
      except:
         rc = -2

      cursor.close()

      # disconnect

      self.disconnect()

      return rc

   def list_environments(self):

      if self.conn == None:
         return None

      cursor = self.conn.cursor()
      try:
         cursor.execute( "SELECT env_id, place "
                         "FROM gnubg.env "
                         "ORDER BY env_id" );
      except:
         cursor.close()
         return None
         
      all = cursor.fetchall()

      cursor.close()

      return all
