"""
Oracle pooled connection database backend for Django.
Requires cx_Oracle: http://www.python.net/crew/atuining/cx_Oracle/
"""

import os, sys
from django.db.backends import BaseDatabaseWrapper, BaseDatabaseValidation, util

# Makes it explicit where the default oracle versions fo these components are used
from django.db.backends.oracle.base import OracleParam
from django.db.backends.oracle.base import DatabaseFeatures as OracleDatabaseFeatures
from django.db.backends.oracle.base import DatabaseOperations as OracleDatabaseOperations
from django.db.backends.oracle.client import DatabaseClient as OracleDatabaseClient
from django.db.backends.oracle.introspection import DatabaseIntrospection as OracleDatabaseIntrospection
from django.db.backends.oracle.base import FormatStylePlaceholderCursor as OracleFormatStylePlaceholderCursor

from oraclepool.creation import DatabaseCreation
from django.conf import settings

try:
    import cx_Oracle as Database
except ImportError, e:
    from django.core.exceptions import ImproperlyConfigured
    raise ImproperlyConfigured("Error loading cx_Oracle module: %s" % e)

from django.utils.encoding import smart_str, force_unicode
# Check whether cx_Oracle was compiled with the WITH_UNICODE option.  This will
# also be True in Python 3.0.
if int(Database.version.split('.', 1)[0]) >= 5 and not hasattr(Database, 'UNICODE'):
    convert_unicode = force_unicode
else:
    convert_unicode = smart_str

DatabaseError = Database.Error
IntegrityError = Database.IntegrityError

# Oracle takes client-side character set encoding from the environment.
os.environ['NLS_LANG'] = '.UTF8'

def get_extras(database='default'):
    """ NB: Oracle already has OPTIONS specific to cx_Oracle.connection() use
        This adds extra pool and sql logging attributes to the settings 
    """
    DEFAULT_EXTRAS = {'min':4,         # start number of connections
                      'max':8,         # max number of connections
                      'increment':1,   # increase by this amount when more are needed
                      'homogeneous':1, # 1 = single credentials, 0 = multiple credentials
                      'threaded':True, # server platform optimisation 
                      'timeout':600,   # connection timeout, 600 = 10 mins
                      'log':0,         # extra logging functionality
                      'logpath':'',    # file system path for oraclepool.log file
                      'existing':'',   # Type modifications if using existing database data
                      }
    if hasattr(settings, 'DATABASES'):
        db = settings.DATABASES.get(database,{})
        if db.has_key('EXTRAS'):
            return db['EXTRAS']
    elif hasattr(settings, 'DATABASE_EXTRAS'):
        return settings.DATABASE_EXTRAS
    else:
        return DEFAULT_EXTRAS

def get_logger(extras):
    """ Check whether logging is required
        If log level is more than zero then logging is performed
        If log level is DEBUG then logging is printed to screen
        If no logfile is specified then unless its DEBUG to screen its added here
        NB: Log levels are 10 DEBUG, 20 INFO, 30 WARNING, 40 ERROR, 50 CRITICAL
    """

    loglevel = int(extras.get('log', 0))
    if loglevel>0:
        import logging
        logfile = extras.get('logpath','')
        if logfile.endswith('.log'):
            (logfile, filename) = os.path.split(logfile)
        else:
            filename = 'oraclepool.log'
        if os.path.exists(logfile):
            logfile = os.path.join(logfile,filename)
        else:
            logfile = ''
        if not logfile and extras.get('log') > logging.DEBUG:
            logfile = '.'
        if logfile in ['.','..']:
            logfile = os.path.join(os.path.abspath(os.path.dirname(logfile)),filename)
        # if log file is writable do it
        if not logfile:
            print 'Log path %s not found' % extras.get('logpath','')
            logger = None
        else:
            try:
                logging.basicConfig(filename=logfile, level=loglevel)
                logger = logging.getLogger("oracle_pool")
                logger.setLevel(loglevel)
                ch = logging.StreamHandler()
                ch.setLevel(loglevel)
                formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
                ch.setFormatter(formatter)
                logger.addHandler(ch)
                from datetime import datetime
                msg = '%s #### Started django-oraclepool SQL logging at level %s ####' % (datetime.now(),
                                                                                             loglevel)
                print 'Log started at %s' % logfile
                logger.info(msg)
            except:
                logger = None
    else:
        logger = None

    # Add sql logging for all requests if DEBUG level
    if extras.get('log') == 10 or settings.DEBUG:
        # Add middleware if needed
        MIDDLEWARE_CLASSES = list(settings.MIDDLEWARE_CLASSES) 
        MIDDLEWARE_CLASSES.append('oraclepool.log_sql.SQLLogMiddleware')
        settings.MIDDLEWARE_CLASSES = tuple(MIDDLEWARE_CLASSES)

DATABASE_EXTRAS = get_extras()
logger = get_logger(DATABASE_EXTRAS)
    
class DatabaseFeatures(OracleDatabaseFeatures):
    """ Add extra options from default Oracle ones
        Plus switch off save points and id return
    """
    uses_savepoints = False
    can_return_id_from_insert = False
    
    allows_group_by_ordinal = False
    supports_tablespaces = True
    uses_case_insensitive_names = True
    time_field_needs_date = True
    date_field_supports_time_value = False

class DatabasePool:
    """ Core connection pool """
    def __get__(self):
        if hasattr(self,'_pool'):
            return self._pool
        else:
            if len(settings.DATABASE_HOST.strip()) == 0:
                settings.DATABASE_HOST = 'localhost'
            if len(settings.DATABASE_PORT.strip()) != 0:
                dsn = Database.makedsn(settings.DATABASE_HOST, int(settings.DATABASE_PORT), settings.DATABASE_NAME)
            else:
                dsn = settings.DATABASE_NAME
            self._pool = cx_Oracle.SessionPool(settings.DATABASE_USER, 
                                     settings.DATABASE_PASSWORD, 
                                     dsn, 
                                     DATABASE_EXTRAS['min'], 
                                     DATABASE_EXTRAS['max'], 
                                     DATABASE_EXTRAS['increment'],
                                     threaded = DATABASE_EXTRAS['threaded'], 
                                     homogeneous = DATABASE_EXTRAS['homogeneous'])
            if DATABASE_EXTRAS['timeout']:
                self._pool.timeout = DATABASE_EXTRAS['timeout']
            return self._pool


class DatabaseWrapper(BaseDatabaseWrapper):
    """ This provides the core connection object wrapper
        for cx_Oracle's pool handling.
        The code is mostly taken from
        http://code.djangoproject.com/ticket/7732 by halturin
    """

    poolprops = {'homogeneous':'',
                 'increment':'',
                 'max':'',
                 'min':'',
                 'busy':'',
                 'opened':'',
                 'name':'',
                 'timeout':'',
                 'tnsentry':''
                }
    operators = {
        'exact': '= %s',
        'iexact': '= UPPER(%s)',
        'contains': "LIKEC %s ESCAPE '\\'",
        'icontains': "LIKEC UPPER(%s) ESCAPE '\\'",
        'gt': '> %s',
        'gte': '>= %s',
        'lt': '< %s',
        'lte': '<= %s',
        'startswith': "LIKEC %s ESCAPE '\\'",
        'endswith': "LIKEC %s ESCAPE '\\'",
        'istartswith': "LIKEC UPPER(%s) ESCAPE '\\'",
        'iendswith': "LIKEC UPPER(%s) ESCAPE '\\'",
    }
    oracle_version = None

    def __init__(self, *args, **kwargs):
        """ Set up the various database components
            Oracle prefixed classes use the standard Oracle
            version - tested with both 1.0 and 1.2
        """
        super(DatabaseWrapper, self).__init__(*args, **kwargs)

        self.features = DatabaseFeatures()
        self.ops = OracleDatabaseOperations()
        self.client = OracleDatabaseClient(self)
        self.creation = DatabaseCreation(self)
        self.introspection = OracleDatabaseIntrospection(self)
        try:
            self.validation = BaseDatabaseValidation(self)
        except:
            # pre django 1.2
            self.validation = BaseDatabaseValidation()            

    def get_config(self):
        """ Report the oracle connection and pool data see 
            http://cx-oracle.sourceforge.net/html/session_pool.html#sesspool
        """
        pool = self._get_pool()
        if pool:
            for key in self.poolprops.keys():
                try:
                    self.poolprops[key] = getattr(pool, key, '')
                except:
                    pass
        else:
            self.poolprops['name'] = 'Session pool not found'
        return self.poolprops

    def _get_pool (self):
        """ Get the connection pool or create it if it doesnt exist """
        if not hasattr (self.__class__, '_pool'):
            if DATABASE_EXTRAS['threaded']:
                Database.OPT_Threading = 1
            else:
                Database.OPT_Threading = 0
            if len(settings.DATABASE_HOST.strip()) == 0:
                settings.DATABASE_HOST = 'localhost'
            if len(settings.DATABASE_PORT.strip()) != 0:
                dsn = Database.makedsn(settings.DATABASE_HOST, int(settings.DATABASE_PORT), settings.DATABASE_NAME)
            else:
                dsn = settings.DATABASE_NAME
            try:
                p = Database.SessionPool(settings.DATABASE_USER, 
                                         settings.DATABASE_PASSWORD, 
                                         dsn, 
                                         DATABASE_EXTRAS['min'], 
                                         DATABASE_EXTRAS['max'], 
                                         DATABASE_EXTRAS['increment'],
                                         threaded = DATABASE_EXTRAS['threaded'], 
                                         homogeneous = DATABASE_EXTRAS['homogeneous'])
            except:
                print "##### Database '%s' login failed or database not found #####" % settings.DATABASE_NAME
                print "dsn = %s" % dsn
                print 'Django start up cancelled'
                sys.exit(1)
            if DATABASE_EXTRAS['timeout']:
                p.timeout = DATABASE_EXTRAS['timeout']
            setattr(self.__class__, '_pool', p)

        return getattr(self.__class__, '_pool')
        
    pool = property (_get_pool)
	
    def _cursor(self, settings=None):
        """ Get a cursor from the connection pool """
        cursor = None
        if self.pool is not None:
            if self.connection is None:
                self.connection = self.pool.acquire()
                if logger:
                    logger.info("Acquire pooled connection \n%s\n" % self.connection.dsn)
        	
            cursor = FormatStylePlaceholderCursor(self.connection)
            # Set oracle date to ansi date format.  This only needs to execute
            # once when we create a new connection.
            cursor.execute("ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD' "  
                           "NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SS.FF'")
            try:
                self.oracle_version = int(self.connection.version.split('.')[0])
                # There's no way for the DatabaseOperations class to know the
                # currently active Oracle version, so we do some setups here.
                # TODO: Multi-db support will need a better solution (a way to
                # communicate the current version).
                if self.oracle_version <= 9:
                    self.ops.regex_lookup = self.ops.regex_lookup_9
                else:
                    self.ops.regex_lookup = self.ops.regex_lookup_10
            except ValueError, err:
                if logger:
                    logger.warn(str(err))
            try:
                self.connection.stmtcachesize = 20
            except:
                # Django docs specify cx_Oracle version 4.3.1 or higher, but
                # stmtcachesize is available only in 4.3.2 and up.
                pass
        else:
            if logger:
                logger.critical('Pool couldnt be created')
            else:
                raise u'Pool couldnt be created'
            
        if not cursor:
            cursor = FormatStylePlaceholderCursor(self.connection)
        # Default arraysize of 1 is highly sub-optimal.
        cursor.arraysize = 100
        return cursor

    def close(self):
        """ Releases connection back to pool """
        if self.connection is not None:
            if logger:
                logger.debug("Release pooled connection\n%s\n" % self.connection.dsn)
            self.pool.release(self.connection)
            self.connection = None

class FormatStylePlaceholderCursor(OracleFormatStylePlaceholderCursor):
    """ Added just to allow use of % for like queries without params
        and use of logger if present.
    """

    def cleanquery(self, query, args=None):
        """ cx_Oracle wants no trailing ';' for SQL statements.  For PL/SQL, it
            it does want a trailing ';' but not a trailing '/'.  However, these
            characters must be included in the original query in case the query
            is being passed to SQL*Plus.

            Split out this as a function and allowed for no args so
            % signs can be used in the query without requiring parameterization
        """
        if query.endswith(';') or query.endswith('/'):
            query = query[:-1]
        if not args:
            return convert_unicode(query, self.charset)
        else:
            try:
                return convert_unicode(query % tuple(args), self.charset)
            except TypeError, e:
                err = 'Parameter parsing failed due to error ' + str(e) + ' for query:' + query
                if logger:
                    logger.critical(err)
                else:
                    raise unicode(err)
                
    def execute(self, query, params=[]):
        if params is None:
            args = None
        else:
            params = self._format_params(params)
            args = [(':arg%d' % i) for i in range(len(params))]
        query = self.cleanquery(query, args)
        self._guess_input_sizes([params])
        try:
            return self.cursor.execute(query, self._param_generator(params))
        except DatabaseError, e:
            # cx_Oracle <= 4.4.0 wrongly raises a DatabaseError for ORA-01400.
            if e.args[0].code == 1400 and not isinstance(e, IntegrityError):
                e = IntegrityError(e.args[0])
            err = '%s due to query:%s' % (e, query)
            if logger:
                logger.critical(err)
            else:
                raise unicode(err)

    def executemany(self, query, params=[]):
        try:
            args = [(':arg%d' % i) for i in range(len(params[0]))]
        except (IndexError, TypeError):
            # No params given, nothing to do
            return None
        query = self.cleanquery(query, args)        
        formatted = [self._format_params(i) for i in params]
        self._guess_input_sizes(formatted)
        try:
            return self.cursor.executemany(query,
                                [self._param_generator(p) for p in formatted])
        except DatabaseError, e:
            # cx_Oracle <= 4.4.0 wrongly raises a DatabaseError for ORA-01400.
            if e.args[0].code == 1400 and not isinstance(e, IntegrityError):
                e = IntegrityError(e.args[0])
            if logger:
                logger.critical('%s due to query:%s' % (e, query))                
            else:
                raise unicode(e)
