# -*- coding: utf-8 -*-
# Elisa - Home multimedia server
# Copyright (C) 2006-2008 Fluendo Embedded S.L. (www.fluendo.com).
# All rights reserved.
#
# This file is available under one of two license agreements.
#
# This file is licensed under the GPL version 3.
# See "LICENSE.GPL" in the root of this distribution including a special
# exception to use Elisa with Fluendo's plugins.
#
# The GPL part of Elisa is also available under a commercial licensing
# agreement from Fluendo.
# See "LICENSE.Elisa" in the root directory of this distribution package
# for details on that license.


__maintainer__ = 'Philippe Normand <philippe@fluendo.com>'


from elisa.core import log, media_uri
from elisa.core.utils import locale_helper
from elisa.extern import db_row, translation
import time
import sys, re, platform

# provide user friendly names for db adaptors
DB_BACKENDS={'sqlite': 'pysqlite2.dbapi2',
             'sqlite3': 'sqlite3.dbapi2'}

# increase cache size from 2000 to 66666 pages
BACKEND_SPECIFIC_SQL={'sqlite':"""\
PRAGMA default_synchronous = OFF;
pragma default_cache_size = 66666;
"""
                      }
TABLE_COLUMNS = {}

def table_names(schema):
    """
    Scan given db SQL schema and find all table names by looking for
    "CREATE TABLE" statements.

    @param schema: SQL schema definition
    @type schema:  string
    @rtype:        list of strings
    """
    names = []
    for line in schema.splitlines():
        result = re.search('CREATE TABLE (\w+)\s*', line)
        if result:
            names.append(result.groups()[0])
    return names

class DBBackendError(Exception):

    def __init__(self, message):
        self.message = message

    def __str__(self):
        return self.message

class DBBackend(log.Loggable):
    """
    Python DB API 2.0 backend support.

    In addition to DB API 2.0 support this class implements an SQL
    query filter, based on a set of rules (which remain to be
    defined...)

    Components can alter the database by adding new tables in the
    schema. These new tables can only be accessed via raw SQL queries.

    @todo: We need to define a set of rules that specify which
    component can alter/insert data in core_tables.

    """

    def __init__(self, **config):
        """ Connect to a db backend hosting the given database.

        @keyword username:     the user name to use to connect to the database
        @type username:      string or None if no username required by backend
        @keyword password:     the password to use to connect to the database
        @type password:      string or None if no password required by backend
        @keyword hostname:     the host name to connect to if the backend runs on
                             a specific machine
        @type hostname:      string or None if no hostname required by backend

        @raise DBBackendError : When no db backend has been specified or if it
                                cannot be used.
        """
        self.log_category = 'db_backend'
        log.Loggable.__init__(self)

        backend_name = ''
        database = ''

        self._config = config
        backend_name = config.get('db_backend')

        self._init_backend(backend_name)

    def _init_backend(self, backend_name):
        database = self._config.get('database')
        charset = locale_helper.system_encoding()
        database = database.decode(charset).encode('utf8')
        
        if not backend_name:
            raise DBBackendError("No db backend specified")

        mod_name = DB_BACKENDS.get(backend_name)
        self.info("Using %r database backend on %r database", mod_name, database)
        self.name = backend_name

        try:
            # import the DBM python package
            if mod_name.find('.') > -1:
                parts = mod_name.split('.')
                self._db_module = __import__(mod_name, globals(),
                                             locals(), [parts[-1],])
            else:
                self._db_module = __import__(mod_name, globals(), locals(),[])
        except ImportError, error:
            if backend_name == 'sqlite' and sys.version_info[:2] >= (2, 5):
                self._init_backend('sqlite3')
            else:
                raise DBBackendError(str(error))

        # TODO: remove SQLite specific options
        self._params = {'database': database,'check_same_thread': True}

        self.connect()

    def disconnect(self):
        """
        Commit changes to the database and disconnect.
        """
        self.save_changes()
        self._db.close()

    def connect(self):
        """
        Connect to the database, set L{_db} instance variable.
        """
        self._db = self._db_module.connect(**self._params)

        # TODO: remove these SQLite specific bits
        self._build_rows = False

    def reconnect(self):
        """
        Disconnect and reconnect to the database.
        """
        self.disconnect()
        self.connect()

    def save_changes(self):
        """
        Commit changes to the database

        @raise Exception: if the save has failed
        """

        try:
            self._db.commit()
            self.debug('Committed changes to DB')
        except Exception, ex:
            self.debug('Commit failed, %s' % ex)
            raise


    def insert(self, sql_query, *params):
        """ Execute an INSERT SQL query in the db backend
        and return the ID fo the row if AUTOINCREMENT is used

        @param sql_query: the SQL query data to execute
        @type sql_query:  string
        @rtype:           int
        """

        t0 = time.time()

        cursor = self._db.cursor()
        result = -1
        new_params = self._fix_params(params)

        debug_msg = u"%s params=%r" % (sql_query, new_params)
        debug_msg = u''.join(debug_msg.splitlines())
        self.debug(debug_msg)

        try:
            cursor.execute(sql_query, new_params)
        except Exception, exception:
            self.warning(exception)
            #FIXME: raise again ?
        else:
            result = cursor.lastrowid

        cursor.close()
        delta = time.time() - t0
        self.log("SQL insert took %s seconds" % delta)
        
        return result

    def sql_execute(self, sql_query, *params, **kw):
        """ Execute a SQL query in the db backend

        @param sql_query: the SQL query data to execute
        @type sql_query:  string
        @rtype:           L{elisa.extern.db_row.DBRow} list
        """
        t0 = time.time()
        result = self._query(sql_query, *params, **kw)

        delta = time.time() - t0
        self.log("SQL request took %s seconds" % delta)

        return result

    def _query(self, request, *params, **keywords):
        quiet = keywords.get('quiet', False)

        debug_msg = request
        if params:
            params = self._fix_params(params)
            debug_msg = u"%s params=%r" % (request, params)
        debug_msg = u''.join(debug_msg.splitlines())
        if debug_msg:
            self.debug('QUERY: %s', debug_msg)

        do_commit = keywords.get('do_commit', False)
        cursor = self._db.cursor()
        result = []
        new_params = self._fix_params(params)
        try:
            cursor.execute(request, new_params)
        except Exception, exception:
            if not quiet:
                self.warning(exception)
            #FIXME: raise again ?
        else:
            if cursor.description:
                all_rows = cursor.fetchall()
                if not self._build_rows:
                    # self.debug("Query result: %r", all_rows)
                    result = db_row.getdict(all_rows, cursor.description)
                else:
                    result = all_rows
        cursor.close()
        if do_commit:
            self.save_changes()
        return result

    def _fix_params(self, params):
        # For some unknown reasons escaping uris obtained from MediaUri.join()
        # doesn't work, so I need to check force repr() manually
        r = []
        for p in params:
            if isinstance(p, media_uri.MediaUri):
                r.append(unicode(p))
            elif isinstance(p, translation.TranslatableSingular):
                # FIXME: this doesn't really belong here.. should
                #        be in Translatable.__str__ (IMO)
                r.append(p.format % p.args)
            elif isinstance(p, translation.TranslatablePlural):
                # FIXME: this doesn't really belong here.. should
                #        be in Translatable.__str__ (IMO)
                if len(p.count) > 0:
                    r.append(p.plural % p.args)
                else:
                    r.append(p.singular % p.args)
            else:
                r.append(p)
        return tuple(r)

    def table_columns(self, table_name):
        """ Introspect given table and retrieve its column names in a cache

        The cache is global to the module, not DBBackend instance specific.

        @param table_name: name of the db table to introspect
        @type table_name:  string
        @rtype:            string list
        """
        global TABLE_COLUMNS
        if table_name not in TABLE_COLUMNS:
            result = []

            # FIXME: support more RDBMS here
            if self._backend_name == 'sqlite':
                result = self.sql_execute("PRAGMA table_info(%s)" % table_name)

            col_names = set([ r.name for r in result ])
            TABLE_COLUMNS[table_name] = col_names

        return TABLE_COLUMNS.get(table_name)
