"""
parser.sql.objectadapter module (imdb.parser.sql package).

This module adapts the SQLObject ORM to the internal mechanism.

Copyright 2008-2010 Davide Alberani <da@erlug.linux.it>

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
"""

import sys
import logging

from sqlobject import *
from sqlobject.sqlbuilder import ISNULL, ISNOTNULL, AND, OR, IN, CONTAINSSTRING

from dbschema import *

_object_logger = logging.getLogger('imdbpy.parser.sql.object')


# Maps our placeholders to SQLAlchemy's column types.
MAP_COLS = {
        INTCOL: IntCol,
        UNICODECOL: UnicodeCol,
        STRINGCOL: StringCol
}


# Exception raised when Table.get(id) returns no value.
NotFoundError = SQLObjectNotFound


# class method to be added to the SQLObject class.
def addIndexes(cls, ifNotExists=True):
    """Create all required indexes."""
    for col in cls._imdbpySchema.cols:
        if col.index:
            idxName = col.index
            colToIdx = col.name
            if col.indexLen:
                colToIdx = {'column': col.name, 'length': col.indexLen}
            if idxName in [i.name for i in cls.sqlmeta.indexes]:
                # Check if the index is already present.
                continue
            idx = DatabaseIndex(colToIdx, name=idxName)
            cls.sqlmeta.addIndex(idx)
    try:
        cls.createIndexes(ifNotExists)
    except dberrors.OperationalError, e:
        _object_logger.warn('Skipping creation of the %s.%s index: %s' %
                            (cls.sqlmeta.table, col.name, e))
addIndexes = classmethod(addIndexes)


# Global repository for "fake" tables with Foreign Keys - need to
# prevent troubles if addForeignKeys is called more than one time.
FAKE_TABLES_REPOSITORY = {}

def _buildFakeFKTable(cls, fakeTableName):
    """Return a "fake" table, with foreign keys where needed."""
    countCols = 0
    attrs = {}
    for col in cls._imdbpySchema.cols:
        countCols += 1
        if col.name == 'id':
            continue
        if not col.foreignKey:
            # A non-foreign key column - add it as usual.
            attrs[col.name] = MAP_COLS[col.kind](**col.params)
            continue
        # XXX: Foreign Keys pointing to TableName.ColName not yet supported.
        thisColName = col.name
        if thisColName.endswith('ID'):
            thisColName = thisColName[:-2]

        fks = col.foreignKey.split('.', 1)
        foreignTableName = fks[0]
        if len(fks) == 2:
            foreignColName = fks[1]
        else:
            foreignColName = 'id'
        # Unused...
        #fkName = 'fk_%s_%s_%d' % (foreignTableName, foreignColName,
        #                        countCols)
        # Create a Foreign Key column, with the correct references.
        fk = ForeignKey(foreignTableName, name=thisColName, default=None)
        attrs[thisColName] = fk
    # Build a _NEW_ SQLObject subclass, with foreign keys, if needed.
    newcls = type(fakeTableName, (SQLObject,), attrs)
    return newcls

def addForeignKeys(cls, mapTables, ifNotExists=True):
    """Create all required foreign keys."""
    # Do not even try, if there are no FK, in this table.
    if not filter(None, [col.foreignKey for col in cls._imdbpySchema.cols]):
        return
    fakeTableName = 'myfaketable%s' % cls.sqlmeta.table
    if fakeTableName in FAKE_TABLES_REPOSITORY:
        newcls = FAKE_TABLES_REPOSITORY[fakeTableName]
    else:
        newcls = _buildFakeFKTable(cls, fakeTableName)
        FAKE_TABLES_REPOSITORY[fakeTableName] = newcls
    # Connect the class with foreign keys.
    newcls.setConnection(cls._connection)
    for col in cls._imdbpySchema.cols:
        if col.name == 'id':
            continue
        if not col.foreignKey:
            continue
        # Get the SQL that _WOULD BE_ run, if we had to create
        # this "fake" table.
        fkQuery = newcls._connection.createReferenceConstraint(newcls,
                                newcls.sqlmeta.columns[col.name])
        if not fkQuery:
            # Probably the db doesn't support foreign keys (SQLite).
            continue
        # Remove "myfaketable" to get references to _real_ tables.
        fkQuery = fkQuery.replace('myfaketable', '')
        # Execute the query.
        newcls._connection.query(fkQuery)
    # Disconnect it.
    newcls._connection.close()
addForeignKeys = classmethod(addForeignKeys)


# Module-level "cache" for SQLObject classes, to prevent
# "class TheClass is already in the registry" errors, when
# two or more connections to the database are made.
# XXX: is this the best way to act?
TABLES_REPOSITORY = {}

def getDBTables(uri=None):
    """Return a list of classes to be used to access the database
    through the SQLObject ORM.  The connection uri is optional, and
    can be used to tailor the db schema to specific needs."""
    DB_TABLES = []
    for table in DB_SCHEMA:
        if table.name in TABLES_REPOSITORY:
            DB_TABLES.append(TABLES_REPOSITORY[table.name])
            continue
        attrs = {'_imdbpyName': table.name, '_imdbpySchema': table,
                'addIndexes': addIndexes, 'addForeignKeys': addForeignKeys}
        for col in table.cols:
            if col.name == 'id':
                continue
            attrs[col.name] = MAP_COLS[col.kind](**col.params)
        # Create a subclass of SQLObject.
        # XXX: use a metaclass?  I can't see any advantage.
        cls = type(table.name, (SQLObject,), attrs)
        DB_TABLES.append(cls)
        TABLES_REPOSITORY[table.name] = cls
    return DB_TABLES


def toUTF8(s):
    """For some strange reason, sometimes SQLObject wants utf8 strings
    instead of unicode."""
    return s.encode('utf_8')


def setConnection(uri, tables, encoding='utf8', debug=False):
    """Set connection for every table."""
    kw = {}
    # FIXME: it's absolutely unclear what we should do to correctly
    #        support unicode in MySQL; with some versions of SQLObject,
    #        it seems that setting use_unicode=1 is the _wrong_ thing to do.
    _uriLower = uri.lower()
    if _uriLower.startswith('mysql'):
        kw['use_unicode'] = 1
        #kw['sqlobject_encoding'] = encoding
        kw['charset'] = encoding

        # On some server configurations, we will need to explictly enable
        # loading data from local files
        kw['local_infile'] = 1
    conn = connectionForURI(uri, **kw)
    conn.debug = debug
    # XXX: doesn't work and a work-around was put in imdbpy2sql.py;
    #      is there any way to modify the text_factory parameter of
    #      a SQLite connection?
    #if uri.startswith('sqlite'):
    #    major = sys.version_info[0]
    #    minor = sys.version_info[1]
    #    if major > 2 or (major == 2 and minor > 5):
    #        sqliteConn = conn.getConnection()
    #        sqliteConn.text_factory = str
    for table in tables:
        table.setConnection(conn)
        #table.sqlmeta.cacheValues = False
        # FIXME: is it safe to set table._cacheValue to False?  Looks like
        #        we can't retrieve correct values after an update (I think
        #        it's never needed, but...)  Anyway, these are set to False
        #        for performance reason at insert time (see imdbpy2sql.py).
        table._cacheValue = False
    # Required by imdbpy2sql.py.
    conn.paramstyle = conn.module.paramstyle
    return conn

