from sqlobject import sqlbuilder
from sqlobject import classregistry
from sqlobject.col import StringCol, ForeignKey
from sqlobject.main import sqlmeta, SQLObject, SelectResults, \
   makeProperties, unmakeProperties, getterName, setterName
from sqlobject import events
import iteration

try:
    set
except NameError: # Python 2.3
    from sets import Set, ImmutableSet
    set, frozenset = Set, ImmutableSet

def tablesUsedSet(obj, db):
    if hasattr(obj, "tablesUsedSet"):
        return obj.tablesUsedSet(db)
    elif isinstance(obj, (tuple, list, set, frozenset)):
        s = set()
        for component in obj:
            s.update(tablesUsedSet(component, db))
        return s
    else:
        return set()


class InheritableSelectResults(SelectResults):
    IterationClass = iteration.InheritableIteration

    def __init__(self, sourceClass, clause, clauseTables=None,
            inheritedTables=None, **ops):
        if clause is None or isinstance(clause, str) and clause == 'all':
            clause = sqlbuilder.SQLTrueClause

        dbName = (ops.get('connection',None) or sourceClass._connection).dbName

        tablesSet = tablesUsedSet(clause, dbName)
        tablesSet.add(str(sourceClass.sqlmeta.table))
        orderBy = ops.get('orderBy')
        if inheritedTables:
            for tableName in inheritedTables:
                tablesSet.add(str(tableName))
        if orderBy and not isinstance(orderBy, basestring):
            tablesSet.update(tablesUsedSet(orderBy, dbName))
        #DSM: if this class has a parent, we need to link it
        #DSM: and be sure the parent is in the table list.
        #DSM: The following code is before clauseTables
        #DSM: because if the user uses clauseTables
        #DSM: (and normal string SELECT), he must know what he wants
        #DSM: and will do himself the relationship between classes.
        if not isinstance(clause, str):
            tableRegistry = {}
            allClasses = classregistry.registry(
                sourceClass.sqlmeta.registry).allClasses()
            for registryClass in allClasses:
                if str(registryClass.sqlmeta.table) in tablesSet:
                    #DSM: By default, no parents are needed for the clauses
                    tableRegistry[registryClass] = registryClass
            tableRegistryCopy = tableRegistry.copy()
            for childClass in tableRegistryCopy:
                if childClass not in tableRegistry:
                    continue
                currentClass = childClass
                while currentClass:
                    if tableRegistryCopy.has_key(currentClass):
                        if currentClass in tableRegistry:
                            #DSM: Remove this class as it is a parent one
                            #DSM: of a needed children
                            del tableRegistry[currentClass]
                        #DSM: Must keep the last parent needed
                        #DSM: (to limit the number of join needed)
                        tableRegistry[childClass] = currentClass
                    currentClass = currentClass.sqlmeta.parentClass
            #DSM: Table registry contains only the last children
            #DSM: or standalone classes
            parentClause = []
            for (currentClass, minParentClass) in tableRegistry.items():
                while (currentClass != minParentClass) \
                and currentClass.sqlmeta.parentClass:
                    parentClass = currentClass.sqlmeta.parentClass
                    parentClause.append(currentClass.q.id == parentClass.q.id)
                    currentClass = parentClass
                    tablesSet.add(str(currentClass.sqlmeta.table))
            clause = reduce(sqlbuilder.AND, parentClause, clause)

        super(InheritableSelectResults, self).__init__(sourceClass,
            clause, clauseTables, **ops)

    def accumulateMany(self, *attributes, **kw):
        if kw.get("skipInherited"):
            return super(InheritableSelectResults, self).accumulateMany(*attributes)
        tables = []
        for func_name, attribute in attributes:
           if not isinstance(attribute, basestring):
                tables.append(attribute.tableName)
        clone = self.__class__(self.sourceClass, self.clause,
                          self.clauseTables, inheritedTables=tables, **self.ops)
        return clone.accumulateMany(skipInherited=True, *attributes)

class InheritableSQLMeta(sqlmeta):
    def addColumn(sqlmeta, columnDef, changeSchema=False, connection=None, childUpdate=False):
        soClass = sqlmeta.soClass
        #DSM: Try to add parent properties to the current class
        #DSM: Only do this once if possible at object creation and once for
        #DSM: each new dynamic column to refresh the current class
        if sqlmeta.parentClass:
            for col in sqlmeta.parentClass.sqlmeta.columnList:
                cname = col.name
                if cname == 'childName': continue
                if cname.endswith("ID"): cname = cname[:-2]
                setattr(soClass, getterName(cname), eval(
                    'lambda self: self._parent.%s' % cname))
                if not col.immutable:
                    def make_setfunc(cname):
                        def setfunc(self, val):
                            if not self.sqlmeta._creating and not getattr(self.sqlmeta, "row_update_sig_suppress", False):
                                self.sqlmeta.send(events.RowUpdateSignal, self, {cname : val})
                                
                            result = setattr(self._parent, cname, val)
                        return setfunc

                    setfunc = make_setfunc(cname)
                    setattr(soClass, setterName(cname), setfunc)
            if childUpdate:
                makeProperties(soClass)
                return

        if columnDef:
            super(InheritableSQLMeta, sqlmeta).addColumn(columnDef, changeSchema, connection)

        #DSM: Update each child class if needed and existing (only for new
        #DSM: dynamic column as no child classes exists at object creation)
        if columnDef and hasattr(soClass, "q"):
            q = getattr(soClass.q, columnDef.name, None)
        else:
            q = None
        for c in sqlmeta.childClasses.values():
            c.sqlmeta.addColumn(columnDef, connection=connection, childUpdate=True)
            if q: setattr(c.q, columnDef.name, q)

    addColumn = classmethod(addColumn)

    def delColumn(sqlmeta, column, changeSchema=False, connection=None, childUpdate=False):
        if childUpdate:
            soClass = sqlmeta.soClass
            unmakeProperties(soClass)
            makeProperties(soClass)

            if isinstance(column, str):
                name = column
            else:
                name = column.name
            delattr(soClass, name)
            delattr(soClass.q, name)
            return

        super(InheritableSQLMeta, sqlmeta).delColumn(column, changeSchema, connection)

        #DSM: Update each child class if needed
        #DSM: and delete properties for this column
        for c in sqlmeta.childClasses.values():
            c.sqlmeta.delColumn(column, changeSchema=changeSchema,
                connection=connection, childUpdate=True)

    delColumn = classmethod(delColumn)

    def addJoin(sqlmeta, joinDef, childUpdate=False):
        soClass = sqlmeta.soClass
        #DSM: Try to add parent properties to the current class
        #DSM: Only do this once if possible at object creation and once for
        #DSM: each new dynamic join to refresh the current class
        if sqlmeta.parentClass:
            for join in sqlmeta.parentClass.sqlmeta.joins:
                jname = join.joinMethodName
                jarn  = join.addRemoveName
                setattr(soClass, getterName(jname),
                    eval('lambda self: self._parent.%s' % jname))
                if hasattr(join, 'remove'):
                    setattr(soClass, 'remove' + jarn,
                        eval('lambda self,o: self._parent.remove%s(o)' % jarn))
                if hasattr(join, 'add'):
                    setattr(soClass, 'add' + jarn,
                        eval('lambda self,o: self._parent.add%s(o)' % jarn))
            if childUpdate:
                makeProperties(soClass)
                return

        if joinDef:
            super(InheritableSQLMeta, sqlmeta).addJoin(joinDef)

        #DSM: Update each child class if needed and existing (only for new
        #DSM: dynamic join as no child classes exists at object creation)
        for c in sqlmeta.childClasses.values():
            c.sqlmeta.addJoin(joinDef, childUpdate=True)

    addJoin = classmethod(addJoin)

    def delJoin(sqlmeta, joinDef, childUpdate=False):
        if childUpdate:
            soClass = sqlmeta.soClass
            unmakeProperties(soClass)
            makeProperties(soClass)
            return

        super(InheritableSQLMeta, sqlmeta).delJoin(joinDef)

        #DSM: Update each child class if needed
        #DSM: and delete properties for this join
        for c in sqlmeta.childClasses.values():
            c.sqlmeta.delJoin(joinDef, childUpdate=True)

    delJoin = classmethod(delJoin)

    def getAllColumns(sqlmeta):
        columns = sqlmeta.columns.copy()
        sm = sqlmeta
        while sm.parentClass:
            columns.update(sm.parentClass.sqlmeta.columns)
            sm = sm.parentClass.sqlmeta
        return columns

    def getColumns(sqlmeta):
        columns = sqlmeta.getAllColumns()
        if columns.has_key('childName'):
            del columns['childName']
        return columns


class InheritableSQLObject(SQLObject):

    sqlmeta = InheritableSQLMeta
    _inheritable = True
    SelectResultsClass = InheritableSelectResults

    def set(self, **kw):
        if self._parent:
            SQLObject.set(self, _suppress_set_sig=True, **kw)
        else:
            SQLObject.set(self, **kw)

    def __classinit__(cls, new_attrs):
        SQLObject.__classinit__(cls, new_attrs)
        # if we are a child class, add sqlbuilder fields from parents
        currentClass = cls.sqlmeta.parentClass
        while currentClass:
            for column in currentClass.sqlmeta.columnDefinitions.values():
                if column.name == 'childName':
                    continue
                if isinstance(column, ForeignKey):
                    continue
                setattr(cls.q, column.name,
                    getattr(currentClass.q, column.name))
            currentClass = currentClass.sqlmeta.parentClass

    # @classmethod
    def _SO_setupSqlmeta(cls, new_attrs, is_base):
        # Note: cannot use super(InheritableSQLObject, cls)._SO_setupSqlmeta -
        #       InheritableSQLObject is not defined when it's __classinit__
        #       is run.  Cannot use SQLObject._SO_setupSqlmeta, either:
        #       the method would be bound to wrong class.
        if cls.__name__ == "InheritableSQLObject":
            call_super = super(cls, cls)
        else:
            # InheritableSQLObject must be in globals yet
            call_super = super(InheritableSQLObject, cls)
        call_super._SO_setupSqlmeta(new_attrs, is_base)
        sqlmeta = cls.sqlmeta
        sqlmeta.childClasses = {}
        # locate parent class and register this class in it's children
        sqlmeta.parentClass = None
        for superclass in cls.__bases__:
            if getattr(superclass, '_inheritable', False) \
            and (superclass.__name__ != 'InheritableSQLObject'):
                if sqlmeta.parentClass:
                    # already have a parent class;
                    # cannot inherit from more than one
                    raise NotImplementedError(
                        "Multiple inheritance is not implemented")
                sqlmeta.parentClass = superclass
                superclass.sqlmeta.childClasses[cls.__name__] = cls
        if sqlmeta.parentClass:
            # remove inherited column definitions
            cls.sqlmeta.columns = {}
            cls.sqlmeta.columnList = []
            cls.sqlmeta.columnDefinitions = {}
            # default inheritance child name
            if not sqlmeta.childName:
                sqlmeta.childName = cls.__name__

    _SO_setupSqlmeta = classmethod(_SO_setupSqlmeta)

    def get(cls, id, connection=None, selectResults=None, childResults=None, childUpdate=False):

        val = super(InheritableSQLObject, cls).get(id, connection, selectResults)

        #DSM: If we are updating a child, we should never return a child...
        if childUpdate: return val
        #DSM: If this class has a child, return the child
        if 'childName' in cls.sqlmeta.columns:
            childName = val.childName
            if childName is not None:
                childClass = cls.sqlmeta.childClasses[childName]
                # If the class has no columns (which sometimes makes sense
                # and may be true for non-inheritable (leaf) classes only),
                # shunt the query to avoid almost meaningless SQL
                # like "SELECT NULL FROM child WHERE id=1".
                # This is based on assumption that child object exists
                # if parent object exists.  (If it doesn't your database
                # is broken and that is a job for database maintenance.)
                if not (childResults or childClass.sqlmeta.columns):
                    childResults = (None,)
                return childClass.get(id, connection=connection,
                    selectResults=childResults)
        #DSM: Now, we know we are alone or the last child in a family...
        #DSM: It's time to find our parents
        inst = val
        while inst.sqlmeta.parentClass and not inst._parent:
            inst._parent = inst.sqlmeta.parentClass.get(id,
                connection=connection, childUpdate=True)
            inst = inst._parent
        #DSM: We can now return ourself
        return val

    get = classmethod(get)

    def _notifyFinishClassCreation(cls):
        sqlmeta = cls.sqlmeta
        # verify names of added columns
        if sqlmeta.parentClass:
            # FIXME: this does not check for grandparent column overrides
            parentCols = sqlmeta.parentClass.sqlmeta.columns.keys()
            for column in sqlmeta.columnList:
                if column.name == 'childName':
                    raise AttributeError(
                        "The column name 'childName' is reserved")
                if column.name in parentCols:
                    raise AttributeError("The column '%s' is"
                        " already defined in an inheritable parent"
                        % column.name)
        # if this class is inheritable, add column for children distinction
        if cls._inheritable and (cls.__name__ != 'InheritableSQLObject'):
            sqlmeta.addColumn(StringCol(name='childName',
                # limit string length to get VARCHAR and not CLOB
                length=255, default=None))
        if not sqlmeta.columnList:
            # There are no columns - call addColumn to propagate columns
            # from parent classes to children
            sqlmeta.addColumn(None)
        if not sqlmeta.joins:
            # There are no joins - call addJoin to propagate joins
            # from parent classes to children
            sqlmeta.addJoin(None)
    _notifyFinishClassCreation = classmethod(_notifyFinishClassCreation)

    def _create(self, id, **kw):

        #DSM: If we were called by a children class,
        #DSM: we must retreive the properties dictionary.
        #DSM: Note: we can't use the ** call paremeter directly
        #DSM: as we must be able to delete items from the dictionary
        #DSM: (and our children must know that the items were removed!)
        if kw.has_key('kw'):
            kw = kw['kw']
        #DSM: If we are the children of an inheritable class,
        #DSM: we must first create our parent
        if self.sqlmeta.parentClass:
            parentClass = self.sqlmeta.parentClass
            new_kw = {}
            parent_kw = {}
            for (name, value) in kw.items():
                if (name != 'childName') and hasattr(parentClass, name):
                    parent_kw[name] = value
                else:
                    new_kw[name] = value
            kw = new_kw

            # Need to check that we have enough data to sucesfully
            # create the current subclass otherwise we will leave
            # the database in an inconsistent state.
            for col in self.sqlmeta.columnList:
                if (col._default == sqlbuilder.NoDefault) and \
                        (col.name not in kw) and (col.foreignName not in kw):
                    raise TypeError, "%s() did not get expected keyword argument %s" % (self.__class__.__name__, col.name)

            parent_kw['childName'] = self.sqlmeta.childName
            self._parent = parentClass(kw=parent_kw,
                connection=self._connection)

            id = self._parent.id

        super(InheritableSQLObject, self)._create(id, **kw)

    def _findAlternateID(cls, name, dbName, value, connection=None):
        result = list(cls.selectBy(connection, **{name: value}))
        if not result:
            return result, None
        obj = result[0]
        return [obj.id], obj
    _findAlternateID = classmethod(_findAlternateID)

    def select(cls, clause=None, *args, **kwargs):
        parentClass = cls.sqlmeta.parentClass
        childUpdate = kwargs.pop('childUpdate', None)
        # childUpdate may have one of three values:
        #   True:
        #       select was issued by parent class to create child objects.
        #       Execute select without modifications.
        #   None (default):
        #       select is run by application.  If this class is inheritance
        #       child, delegate query to the parent class to utilize
        #       InheritableIteration optimizations.  Selected records
        #       are restricted to this (child) class by adding childName
        #       filter to the where clause.
        #   False:
        #       select is delegated from inheritance child which is parent
        #       of another class.  Delegate the query to parent if possible,
        #       but don't add childName restriction: selected records
        #       will be filtered by join to the table filtered by childName.
        if (not childUpdate) and parentClass:
            if childUpdate is None:
                # this is the first parent in deep hierarchy
                addClause = parentClass.q.childName == cls.sqlmeta.childName
                # if the clause was one of TRUE varians, replace it
                if (clause is None) or (clause is sqlbuilder.SQLTrueClause) \
                or (isinstance(clause, basestring) and (clause == 'all')):
                    clause = addClause
                else:
                    # patch WHERE condition:
                    # change ID field of this class to ID of parent class
                    # XXX the clause is patched in place; it would be better
                    #     to build a new one if we have to replace field
                    clsID = cls.q.id
                    parentID = parentClass.q.id
                    def _get_patched(clause):
                        if isinstance(clause, sqlbuilder.SQLOp):
                            _patch_id_clause(clause)
                            return None
                        elif not isinstance(clause, sqlbuilder.Field):
                            return None
                        elif (clause.tableName == clsID.tableName) \
                        and (clause.fieldName == clsID.fieldName):
                            return parentID
                        else:
                            return None
                    def _patch_id_clause(clause):
                        if not isinstance(clause, sqlbuilder.SQLOp):
                            return
                        expr = _get_patched(clause.expr1)
                        if expr:
                            clause.expr1 = expr
                        expr = _get_patched(clause.expr2)
                        if expr:
                            clause.expr2 = expr
                    _patch_id_clause(clause)
                    # add childName filter
                    clause = sqlbuilder.AND(clause, addClause)
            return parentClass.select(clause, childUpdate=False,
                *args, **kwargs)
        else:
            return super(InheritableSQLObject, cls).select(
                clause, *args, **kwargs)
    select = classmethod(select)

    def selectBy(cls, connection=None, **kw):
        clause = []
        foreignColumns = {}
        currentClass = cls
        while currentClass:
            foreignColumns.update(dict([(column.foreignName, name)
                for (name, column) in currentClass.sqlmeta.columns.items()
                    if column.foreignKey
            ]))
            currentClass = currentClass.sqlmeta.parentClass
        for name, value in kw.items():
            if name in foreignColumns:
                name = foreignColumns[name] # translate "key" to "keyID"
                if isinstance(value, SQLObject):
                    value = value.id
            currentClass = cls
            while currentClass:
                try:
                    clause.append(getattr(currentClass.q, name) == value)
                    break
                except AttributeError, err:
                    pass
                currentClass = currentClass.sqlmeta.parentClass
            else:
                raise AttributeError("'%s' instance has no attribute '%s'"
                    % (cls.__name__, name))
        if clause:
            clause = reduce(sqlbuilder.AND, clause)
        else:
            clause = None # select all
        conn = connection or cls._connection
        return cls.SelectResultsClass(cls, clause, connection=conn)

    selectBy = classmethod(selectBy)

    def destroySelf(self):
        #DSM: If this object has parents, recursivly kill them
        if hasattr(self, '_parent') and self._parent:
            self._parent.destroySelf()
        super(InheritableSQLObject, self).destroySelf()

    def _reprItems(self):
        items = super(InheritableSQLObject, self)._reprItems()
        # add parent attributes (if any)
        if self.sqlmeta.parentClass:
            items.extend(self._parent._reprItems())
        # filter out our special column
        return [item for item in items if item[0] != 'childName']

__all__ = ['InheritableSQLObject']
