#
# dbapi.py An implementation of the Python database API 2.0 for MaxDB
#     contact: maxdb@lists.mysql.com.com
#
#
#    ========== licence begin  GPL
#    Copyright (c) 2001-2005 SAP AG
#
#    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.
#    ========== licence end

import string
from exceptions import StandardError
import time
import cStringIO
from types import TupleType

try:
    import thread
    _connectionLock = thread.allocate_lock ()
except ImportError:
    class _PseudoLock:
        def acquire (self, waitflag = 0):
            pass

        def release (self):
            pass

        def locked (self):
            return None

    _connectionLock = _PseudoLock ()

import sapdb.sql

apilevel = '2.0'

threadsafety = 1

paramstyle = 'qmark'

def wrapCall (call, *args):
    try:
        # print call, args
        result = apply (call, args)
        # print '==>', result
    except sapdb.sql.SQLError, err:
        raise ProgrammingError (err)
    except sapdb.sql.CommunicationError, err:
        raise OperationalError (err)
    return result

#
# data types
#
class _AbstractType:
    def __init__ (self, name, sqlNames):
        self.name = name
        self.sqlNames = sqlNames

    def __str__ (self):
        return self.name

    def __cmp__ (self, other):
        if other in self.sqlNames:
            return 0
        else:
            return -1

    def __eq__ (self, other):
        return (other in self.sqlNames)

    def __hash__ (self):
        return hash (self.name)


STRING = _AbstractType ('STRING',
    ['Char', 'Varchar', 'Long'])
BINARY = _AbstractType ('BINARY',
    ['Char Binary', 'Varchar Binary', 'Long Binary', 'Long byte_t'])
NUMBER = _AbstractType ('NUMBER',
    ['Integer', 'Smallint', 'Decimal', 'Fixed', 'Float'])
DATETIME = _AbstractType ('DATETIME',
    ['Date', 'Time', 'Timestamp'])
ROWID = None

#
# connection class
#
class Connection:
    def __init__ (self, user, password, database, host = '', **config):
        configList = map (lambda item: item [0] + '=' + item [1], config.items ())
        configString = string.join (['genericResults=true'] + configList, '&')
        try:
            _connectionLock.acquire ()
            self.__connection = wrapCall (sapdb.sql.connect, user, password,
                database, host, configString)
        finally:
            _connectionLock.release ()
        if host:
            self.id = user + '@' + host + ':' + database
        else:
            self.id = user + '@' + database
        self.typeTranslations = None

    def close (self):
        try:
            _connectionLock.acquire ()
            if self.__connection is not None:
                wrapCall (self.__connection.release)
        finally:
            _connectionLock.release ()

    __del__ = close
    def commit (self):
        wrapCall (self.__connection.commit)

    def rollback (self):
        wrapCall (self.__connection.rollback)

    def cursor (self):
        result = Cursor (self.__connection)
        if self.typeTranslations != None:
            result.setTypeTranslation (self.typeTranslations)
        return result

    def __str__ (self):
        return '<sapdbapi.Connection ' + self.id + '>'

    def setTypeTranslation (self, transDict):
        self.typeTranslations = transDict

connect = Connection

#
#  cursor class
#
class Cursor:
    def __init__ (self, rawConnection):
        self.__connection = rawConnection
        self.description = None
        self.rowcount = -1
        self.arraysize = 1
        self.__prepared = None
        self.__lastop = None
        self.__cursor = None
        self.__typeTranslation = {}

    def close (self):
        try:
            if self.__prepared:
                self.__prepared.close ()
                self.__prepared = None
        except:
            pass
        try:
            if self.__cursor:
                self.__cursor.close ()
                self.__cursor = None
        except:
            pass
        self.__connection = None

    def __handleResult (self, sqlResult):
        result = None
        #
        # last serial
        #
        lastSerial = sqlResult.getLastSerial ()
        if lastSerial is not None:
            self.lastrowid = lastSerial
        #
        # rows affected
        #
        rowsAffected = sqlResult.getRowsAffected ()
        if rowsAffected >= 0:
            result = rowsAffected
        #
        # result set
        #
        resultSet = sqlResult.getResultSet ()
        if resultSet is not None:
            self.__cursor = resultSet
            self.__setDescription (resultSet)
            if self.__typeTranslation:
                self.__instantiateTranslation ()
            if hasattr (self, 'fetchSize'):
                resultSet.setFetchSize (self.fetchSize)
            result = self
        else:
            self.__cursor = None
            self.description = None
        #
        # output parameter
        #
        outparms = sqlResult.getOutputParameters ()
        if outparms is not None:
            self.__cursor = SingleRowResultSet (outparms)
            description = self.description = []
            for name, typename, guess, size, frac, maybeNull, inout in self.__parsed.getDescription ():
                if inout in ['IN', 'IN/OUT']:
                    description.append ((name, typename, size, size, size, frac, maybeNull))
            result = outparms
        return result

    def __setDescription (self, cursor):
        self.description = description = []
        for i in xrange (cursor.columnCount ()):
            name, typename, size, frac = cursor.columnInfo (i)
            description.append ((name, typename, size, size, size, frac, 1))


    def __executeDirect (self, operation):
        result = self.__connection.sql (operation)
        return self.__handleResult (result)

    def __getParsed (self, operation):
        if operation and (self.__lastop != operation):
            self.__parsed = self.__connection.prepare (operation)
            self.__lastop = operation
        return self.__parsed

    def __executeParsed (self, parsed, arguments):
        return self.__handleResult (self.__parsed.execute (arguments))

    def execute (self, operation, parameters = None):
        if parameters == None:
            result = wrapCall (self.__executeDirect, operation)
        else:
            parsed = self.__getParsed (operation)
            result = wrapCall (self.__executeParsed, parsed, parameters)
        return result

    def __executemany (self, operation, seq_of_parameters):
        parsed = self.__getParsed (operation)
        for item in seq_of_parameters:
            parsed.execute (item)

    def executemany (self, operation, seq_of_parameters):
        wrapCall (self.__executemany, operation, seq_of_parameters)

    def __getCursor (self):
        result = self.__cursor
        if not result:
            exc = ProgrammingError ()
            exc.message = 'Not a cursor'
            raise exc
        return result

    def __fetchone (self):
        cursor = self.__getCursor ()
        return self.nextFromCursor (cursor)

    def fetchone (self):
        return wrapCall (self.__fetchone)

    def __fetchmany (self, arraysize):
        cursor = self.__getCursor ()
        result = []
        for i in xrange (arraysize):
            row = self.nextFromCursor (cursor)
            if not row:
                break
            result.append (row)
        return result

    def fetchmany (self, size = None):
        if size == None:
            size = self.arraysize
        return wrapCall (self.__fetchmany, size)

    def __fetchall (self):
        cursor = self.__getCursor ()
        result = []
        while 1:
            row = self.nextFromCursor (cursor)
            if not row:
                break
            result.append (row)
        return result

    def fetchall (self):
        return wrapCall (self.__fetchall)

    def setinputsizes (self, sizes):
        pass

    def setoutputsize (self, size, column = None):
        pass

    def setTranslation (self, translatorList):
        # TODO: make sure the count equals the number of output columns
        self.__translatorList = translatorList
        self.nextFromCursor = self.__nextTranslatedFromCursor

    def __nextTranslatedFromCursor (self, cursor):
        row = cursor.next ()
        if not row:
            return None
        result = [None] * len (row)
        for i in xrange (len (row)):
            translator = self.__translatorList [i]
            if translator is None:
                result [i] = row [i]
            else:
                result [i] = translator (row [i])
        return tuple (result)

    def __nextRawFromCursor (self, cursor):
        return cursor.next ()

    nextFromCursor = __nextRawFromCursor   # untranslated is the default

    def setTypeTranslation (self, transDict):
        self.__typeTranslation.update (transDict)
        if self.__cursor:
            self.__instantiateTranslation ()

    def __instantiateTranslation (self):
        cursor = self.__cursor
        colCount = cursor.columnCount ()
        translations = [None] * colCount
        transDict = self.__typeTranslation
        for i in xrange (colCount):
            typeName = cursor.columnInfo (i) [1]
            try:
                translations [i] = transDict [typeName]
            except KeyError:
                pass
        self.setTranslation (translations)

    def callproc (self, procname, parameters = None):
        if parameters:
            questionmarks = '?' + ', ?' * (len (parameters) - 1)
            parameters = parameters [:]
        procCmd = 'call ' + procname + '(' + questionmarks + ')'
        parsed = self.__getParsed (procCmd)
        resultValues = wrapCall (parsed.execute, inparameters)
        desc = parsed.getDescription ()
        resultIndex = 0
        for paramIndex in xrange (len (desc)):
            if desc [paramIndex] [-1] in ['OUT', 'IN/OUT']:
                parameters [paramIndex] = resultValues.getOutputParameters () [resultIndex]
                resultIndex = resultIndex + 1
        return parameters


class SingleRowResultSet:
    def __init__ (self, row):
        self.row = row

    def next (self):
        result = self.row
        self.row = None
        return result

    def __iter__ (self):
        return self


#
# exception classes
#

class Warning (StandardError):
    pass

class Error (StandardError):
    __internalException = None
    def __init__ (self, exception = None):
        StandardError.__init__ (self)
        if exception:
            self.__dict__.update (exception.__dict__)
            self.__internalException = exception

    def __str__ (self):
        if self.__internalException:
            return str (self.__internalException)
        else:
            return StandardError.__str__ (self)

class DatabaseError (Error):
    pass

class OperationalError (DatabaseError):
    pass

class ProgrammingError (DatabaseError):
    pass

class IntegrityError (DatabaseError):
    pass

class InterfaceError (Error):
    pass

class InternalError (Error):
    pass

class NotSupportedError (Error):
    pass

#
# input conversions
#

def Date (year, month, day):
    return "%04d%02d%02d" % (year, month, day)

def Time (hour, minute, second):
    return "%04d%02d%02d" % (hour, minute, second)

def Timestamp (year, month, day, hour, minute, second, micros = 0):
    return "%04d%02d%02d%02d%02d%02d%06d" % (year, month, day, hour, minute, second, micros)

def DateFromTicks (ticks):
    return apply (Date, time.localtime (ticks) [:3])

def TimeFromTicks (ticks):
    return apply (Time, time.localtime (ticks) [3:6])

def TimestampFromTicks (ticks):
    return apply (Timestamp, time.localtime (ticks) [:6])


def Binary (data):
    return cStringIO.StringIO (data)

#
# output conversions
#
# Date: YYYYMMDD
# Time: 00HHMMSS
# Timestamp: YYYYMMDDHHMMSSmmmmmm

def dateTuple (sapdbDate):
    if sapdbDate is None:
        return None
    return int (sapdbDate[:4]), int (sapdbDate[4:6]), int (sapdbDate[6:])

def timeTuple (sapdbTime):
    if sapdbTime is None:
        return None
    return int (sapdbTime[2:4]), int (sapdbTime[4:6]), int (sapdbTime[6:])

def timestampTuple (sapdbTimestamp):
    if sapdbTimestamp is None:
        return None
    return time.localtime (timestampVal (sapdbTimestamp))

tupleTranslation = {
    'Date': dateTuple,
    'Time': timeTuple,
    'Timestamp': timestampTuple,
}

def dateVal (sapdbDate):
    if sapdbDate is None:
        return None
    year, month, day = dateTuple (sapdbDate)
    return time.mktime ((year, month, day, 0, 0, 0, -1, -1, -1))

def timeVal (sapdbTime):
    if sapdbTime is None:
        return None
    hour, minute, second = timeTuple (sapdbTime)
    return time.mktime ((1970, 1, 2, hour, minute, second, -1, -1, -1))

def timestampVal (sapdbTimestamp):
    if sapdbTimestamp is None:
        return None
    print sapdbTimestamp
    year = int (sapdbTimestamp [:4])
    month = int (sapdbTimestamp [4:6])
    day = int (sapdbTimestamp [6:8])
    hour = int (sapdbTimestamp [8:10])
    minute = int (sapdbTimestamp [10:12])
    second = int (sapdbTimestamp [12:14])
    micros = int (sapdbTimestamp [14:])
    return time.mktime ((year, month, day, hour, minute, second, -1, -1, -1))


valTranslation = {
    'Date': dateVal,
    'Time': timeVal,
    'Timestamp': timestampVal,
}

class AbstractTimeFormat:
    def __init__ (self, fmt):
        self.fmt = fmt

    def __call__ (self, sapdbString):
        localtime = time.localtime ( self.toTime(sapdbString))
        return time.strftime (self.fmt, localtime)

class DateFormat (AbstractTimeFormat):
    def toTime (self, sapdbDate):
        return dateVal (sapdbDate)

class TimeFormat (AbstractTimeFormat):
    def toTime (self, sapdbTime):
        return timeVal (sapdbTime)

class TimestampFormat (AbstractTimeFormat):
    def toTime (self, sapdbTimestamp):
        return timestampVal (sapdbTimestamp)

