# -*- coding: utf8 -*-
'''
SQL OPERATOR CLASSES
**************************
***  SQL OPERATOR CLASSES
**************************
$RCSfile: operators.py,v $
$Revision: 875 $
$Author: markus $
$Date: 2008-04-24 02:23:32 +0200 (Do, 24. Apr 2008) $

Classes representing logical and comparison operators.
They know how to convert themselves to SQL strings.
'''

from types                          import *
from string                         import join
from biocase.wrapper.errorclasses   import *
from biocase.wrapper.sql.base       import sqlBase
from biocase.wrapper.typeconverter  import DBAttributeClass, DBValueClass
from biocase.tools.xmlutils         import element, subelement

import logging
log=logging.getLogger("pywrapper.sql")



# ************************
# ***  FILTER CLASS
# ************************
'''
Missing wrapper class for all filters using LOP & COP.
Should define methods to evaluate filters
'''


# ---------------------------------------------------------------------------       
def compressOperator(opObj):
    '''Function that returns the completely compressed operator object.
    Resolving unnecessary nested logical hierarchies for OR & AND.
    Converts NOT(NOT(x)) into x.
    Removes None type objects.
    Removes binary logical operators containing a None object.
    Evaluates BooleanValue objects in logical operator context (might return a single false).
    '''
    # If the root object is invalid or needs to be replaced by a different class, do it.
    if opObj.__class__ == logicalOperator:
        if opObj.type in sqlBase.unaryLOP.keys():
            if opObj.args == None:
                return None
            # unary operator with None type as argument is NO OPERATOR
            elif opObj.args[0].type == None:
                return None
            elif opObj.type == 'not' and opObj.args[0].type == 'not':
                # NOT NOTs are removed
                return compressOperator(opObj.args[0].args)
            elif opObj.type == 'not' and isinstance(opObj.args[0], BooleanValue):
                # return inversed BooleanValue object
                return opObj.args[0].inverse()
            else:
                opObj.args = compressOperator(opObj.args)
                return opObj
        elif opObj.type in sqlBase.binaryLOP.keys():
            newArgs = []
            for i in range(len(opObj.args)):
                # check children first
                child = compressOperator(opObj.args[i])
                # check BooleanValues
                if isinstance(child, BooleanValue):
                    if opObj.type.lower() == 'and':
                        # replace the entire LOP with FALSE if one of the AND args is FALSE already
                        if child:
                            child = None
                        else:
                            # replace LOP with FALSE and break out of arg-loop:
                            newArgs = [BooleanValue(0)]
                            break
                    if opObj.type.lower() == 'or':
                        # replace the entire LOP with TRUE if one of the OR args is TRUE already
                        if child:
                            # replace LOP with TRUE and break out of arg-loop:
                            newArgs = [BooleanValue(1)]
                            break
                        else:
                            child = None
                # remove None childs
                if child == None:
                    pass
                # look for nested identical operators
                elif opObj.type == child.type:
                    newArgs += child.args
                else:
                    newArgs.append(child)
            # check if there is more than 1 argument left
            if len(newArgs) > 1:
                opObj.args = newArgs
                return opObj
            elif len(newArgs) == 1:
                # replace LOP by its single argument. Maybe a BooleanValue object.
                return newArgs[0]
            else:
                return None
        else:
            return None
    if opObj.__class__ == comparisonOperator:
        if opObj.type == None:
            opObj = None
    return opObj
    
        
        
# ---------------------------------------------------------------------------       
class logicalOperator(sqlBase):
    '''logical operator for AND, OR, NOT. Always use lists of arguments.'''
    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def __init__(self, LOPtype, args=None):
        self.type = None
        self.args = []
        try:
            LOPtype=LOPtype.lower()
            if not ((LOPtype in sqlBase.unaryLOP.keys()) or (LOPtype in sqlBase.binaryLOP.keys()) ):
                self.type = None
            else:
                self.type = LOPtype # and, or, not
                # also use arg lists for NOT, although there can be only 1 argument
                self.args = args
        except:
            self.type = None
            
    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def __repr__(self):
        '''the real output function for the use in SQL.'''
        (sql, paras) = self.getSQLTuple()
        sql = unicode(sql)
        while sql.find('?') > 0:
            idx = sql.find('?')
            sql = sql[:idx] + unicode(paras.pop(0).value) + sql[idx+1:]
        return sql
        
    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def createET(self):
        '''Creates an ElementTree representation as defined by the protocol of this object.
        Still to be done !'''
        return None

    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def getSQLTuple(self):
        '''Creates a sql string replacing all values with "?" and returning them seperately as a list of tuples (value/type).'''
        if self.type in sqlBase.unaryLOP.keys():
            (sql, paras) = self.args[0].getSQLTuple()
            return (self.group_statement(sqlBase.unaryLOP[self.type] +' '+ sql), paras)
        elif self.type in sqlBase.binaryLOP.keys():
            # decompress children
            sql = []
            paras = []
            for arg in self.args:
                (s, p) = arg.getSQLTuple()
                sql.append(s)
                paras += p
            return (self.group_statement( (' '+sqlBase.binaryLOP[self.type]+' ').join(sql) ), paras)


# ---------------------------------------------------------------------------       
class comparisonOperator(sqlBase):
    '''operators for comparing two attributes(DBAttrObjs in arg1/arg2)
    , one attribute with NULL (DBAttrObj in arg1, None in arg2)
    or attribute with value (DBAttrObj in arg1, value in arg2).'''
    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def getDBValObjs(self):
        '''Return a list of all DBVal objects inside this filter.'''
        dbvals = []
        if isinstance(self.arg1, DBAttributeClass):
            dbvals.append(self.arg1)
        if isinstance(self.arg2, DBAttributeClass):
            dbvals.append(self.arg2)
        return dbvals
            
    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def __init__(self, COPtype, DBAttrObj1, DBAttrObj2=None):
        COPtype=COPtype.lower()
        self.type = COPtype
        self.arg1 = None
        self.arg2 = None
        if COPtype in sqlBase.unaryCOP.keys():
            # A unary Operator
            self.arg1 = DBAttrObj1
            self.arg2 = None
        elif COPtype in sqlBase.binaryCOP.keys():
            # A binary Operator
            self.arg1 = DBAttrObj1
            self.arg2 = DBAttrObj2
        elif COPtype in sqlBase.multipleCOP.keys():
            # A multiple Operator
            self.arg1 = DBAttrObj1
            if type(DBAttrObj2) == ListType:
                self.arg2 = DBAttrObj2
            elif type(DBAttrObj2) == TupleType:
                self.arg2 = [x for x in DBAttrObj2]
        else:
            # unknown operator
            self.type = None
            
    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def __repr__(self):
        _replString = u"-§§$%&§$%&-"
        (sql, paras) = self.getSQLTuple()
        # escape ? in paras to \?
        paras = [unicode(p.value).replace('?','\\?') for p in paras]
        sql = unicode(sql)
        while sql.find('?') > 0:
            idx = sql.find('?')
            if sql[idx-1] <> "\\":
                sql = sql[:idx] + paras.pop(0) + sql[idx+1:]
            else:
                sql = sql[:idx-1] + unicode(_replString) + sql[idx+1:]
        return sql.replace(_replString, '?')

    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def createET(self):
        '''Creates a ElementTree representation as defined by the protocol of this object.'''
        if self.type in sqlBase.unaryCOP.keys():
            # unary comparison to one attribute args only. eg isNULL
            cop = None
        elif self.type in sqlBase.binaryCOP.keys():
            if type(self.arg1) == InstanceType and type(self.arg2) == InstanceType:
                # two attributes are being compared
                arg2 = self.joinDBAttrObj(self.arg2)
            else:
                # attribute compared to value
                arg2 = self.arg2
            cop = element(sqlBase.COPout[self.type], arg2, tablealias=self.arg1.table, attribute=self.arg1.attr, type=self.arg1.type)
        elif self.type in sqlBase.multipleCOP.keys():
            cop = element(sqlBase.COPout[self.type], tablealias=self.arg1.table, attribute=self.arg1.attr, type=self.arg1.type)
            for val in self.arg2:
                if val is not None and len(val)>0:
                    subelement(cop, "value", val)
        else:
            cop = None
        return cop

    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def getSQLTuple(self):
        '''Creates a sql string replacing all values with "?"
        and returning them seperately as a list of SQLvalue objects (see types module).
        '''
        typeDict = {}
        if self.type in sqlBase.unaryCOP.keys():
            # unary comparison to one attribute args only. eg isNULL
            result = self.joinDBAttrObj(self.arg1) +' ' +sqlBase.unaryCOP[self.type]
            return (self.group_statement(result), [])
        elif self.type in sqlBase.binaryCOP.keys():
            if type(self.arg1) == InstanceType and type(self.arg2) == InstanceType:
                # two attributes are being compared
                arg1 = self.joinDBAttrObj(self.arg1)
                arg2 = self.joinDBAttrObj(self.arg2)
                if sqlBase.caseAlteringFunctionForLike is None or self.type <> 'like':
                    result = '%s %s %s' % (str(arg1), sqlBase.binaryCOP[self.type], str(arg2) )
                else:
                    result = '%s(%s) %s %s(%s)' % (sqlBase.caseAlteringFunctionForLike, str(arg1), sqlBase.binaryCOP[self.type], sqlBase.caseAlteringFunctionForLike, str(arg2) )
                return (self.group_statement(result), [])
            else:
                # attribute & value are being compared
                target = self.joinDBAttrObj(self.arg1)
                value = DBValueClass(self.arg2, self.arg1.type)
                if sqlBase.caseAlteringFunctionForLike is None or self.type <> 'like':
                    result = '%s %s ? ' % (target, sqlBase.binaryCOP[self.type] )
                else:
                    result = '%s(%s) %s %s( ? )' % (sqlBase.caseAlteringFunctionForLike, target, sqlBase.binaryCOP[self.type], sqlBase.caseAlteringFunctionForLike )
                return (self.group_statement(result), [value])
        elif self.type in sqlBase.multipleCOP.keys():
            target = self.joinDBAttrObj(self.arg1)
            if self.type == 'in':
                if sqlBase.multipleCOP['in'] != None:
                    # IN operator supported by DB
                    values = [DBValueClass(val, self.arg1.type) for val in self.arg2]
                    argList = self.group_statement( "?, "*(len(values)-1)+"?" )
                    result = target +' '+ sqlBase.multipleCOP[self.type] +' '+ argList
                    return (self.group_statement(result), values)
                else:
                    if len(self.arg2) == 1:
                        # only one argument for in. this is like an equals clause
                        value = DBValueClass(self.arg2[0], self.arg1.type)
                        result = target +' '+ sqlBase.binaryCOP['equals'] +' ?'
                        return (self.group_statement(result), [value])
                    elif len(self.arg2) > 1:
                        # more than one argument. translate it into OR statements
                        values = [DBValueClass(v,self.arg1.type) for v in self.arg2]
                        arg = target +' '+ sqlBase.binaryCOP['equals'] +' ?'
                        xxx = arg
                        for val in values[1:]:
                            xxx  += ' ' +sqlBase.binaryLOP['or'] +' '+ arg
                        return (self.group_statement(xxx), values)
                    else:
                        # invalid arguments
                        return ("None", [])
            else:
                return ("None", [])
        else:
            return ("None", [])


# ---------------------------------------------------------------------------       
class BooleanValue(sqlBase):
    '''Boolean value TRUE or FALSE.'''
    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def __init__(self, boolType):
        if boolType or boolType == 'true':
            self.type = 1
        else:
            self.type = 0

    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def __nonzero__(self):
        '''Evaluate for expressions.'''
        if self.type:
            return 1
        else:
            return 0

    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def __repr__(self):
        if self.type:
            return "True"
        else:
            return "False"
        
    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def isFalse(self):
        '''Return True is the filter object is False in its evaluation.
        All filters return False except the boolean False.'''
        if self.type:
            return False
        else:
            return True

    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def inverse(self):
        '''Inverse meaning of value. TRUE becomes FALSE and vice versa.'''
        if self.type:
            self.type = 0
        else:
            self.type = 1
            
    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def getSQLTuple(self):
        '''Creates a sql string replacing all values with "?" and returning them seperately as a list of tuples (value/type).'''
        return (str(self), [])

    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def createET(self):
        '''Creates an ElementTree representation as defined by the protocol of this object.
        Still to be done !'''
        return None


# ---------------------------------------------------------------------------       
def getWhereObjTables(whereObj):
    if type(whereObj) != InstanceType:
        return []
    elif whereObj.__class__ == logicalOperator:
        result = []
        for arg in whereObj.args:
            result += getWhereObjTables(arg)
        return result
    elif whereObj.__class__ == comparisonOperator:
        tables = [whereObj.arg1.table]
        if type(whereObj.arg2) == InstanceType:
            # two tables involved
            tables += [whereObj.arg2.table]
        return tables
    else:
        return []
            
    

    
    
    
    
# ---------------------------------------------------------------------------       
if __name__ ==  "__main__":
    dbattr1 = DBAttributeClass()
    dbattr1.table = 'testtable'
    dbattr1.attr= 'testattribute'
    dbattr2 = DBAttributeClass()
    dbattr2.table = 'people'
    dbattr2.attr= 'hirni'
    
    cop = comparisonOperator('equals',dbattr1 ,"Harald")
    copN = comparisonOperator('equalsw12',dbattr2,"Harald")
    cop2 = comparisonOperator('in',dbattr1,[2,3,4,5,6,7,8])
    bool = BooleanValue(0)
    lop = logicalOperator('not',[copN])
    lop = logicalOperator('and',[lop,copN,bool])
    lop = logicalOperator('and',[lop,cop,bool])
    lop = logicalOperator('or',[lop,cop2])

    print "BEFORE: " +str(lop)
    print "AFTER : " +str(compressOperator(lop))

