#
# -*- coding: utf-8 -*-

''' TBD '''

############################################################################################################
#
#   IMPORTS
#
#===========================================================================================================
import string
from types import *
import logging
# debugging to file
log = logging.getLogger("webapp.querytool")


############################################################################################################
#
#   CLASSES
#
#===========================================================================================================
class UnaryOperatorError(Exception):
    def __init__(self):
        pass


        
class OperatorBaseClass:
    '''An abstract base class for any filter operator.'''
    _encoding = 'utf-8'
    # ------------------------------------------------------------------------------------
    def __init__(self):
        pass
    
    # ------------------------------------------------------------------------------------
    def __unicode__(self):
        return self._repr()
               
    # ------------------------------------------------------------------------------------
    def __repr__(self):
        return self.__unicode__().encode( self.encoding() )
               
    # ------------------------------------------------------------------------------------
    def __str__(self):
        return self.__unicode__().encode(self.encoding())
               
    # ------------------------------------------------------------------------------------
    def getProtocolXML(self, digir=False):
        return self._reprXML(digir=digir)
               
    # ------------------------------------------------------------------------------------
    def __add__(self, filterObj):
        '''Return a new logical AND operator with 2 operands.'''
        return AndClass( [self,filterObj] )

    # ------------------------------------------------------------------------------------
    def __sub__(self, filterObj):
        '''Return a new logical OR operator with 2 operands.'''
        return OrClass( [self,filterObj] )

    # ------------------------------------------------------------------------------------
    def encoding(self):
        '''Return the filter base class encoding string.'''
        return OperatorBaseClass._encoding
               
    # ------------------------------------------------------------------------------------
    def negate(self):
        '''Return a new logical NOT operator with this operand.'''
        return NotClass(self)
               
    # ------------------------------------------------------------------------------------
    def optimize(self):
        '''Try to remove unnecessary operators like nested ANDs.'''
        return self
               
    # ------------------------------------------------------------------------------------
    def _getIndention(self, indent=0):
        '''Try to remove unnecessary operators like nested ANDs.'''
        return "  "*indent
    

class BinaryCOPBaseClass(OperatorBaseClass):
    '''A base class for any comparison operator.'''
    # ------------------------------------------------------------------------------------
    def __init__(self, conceptObj, val):
        OperatorBaseClass.__init__(self)
        self.conceptObj = conceptObj
        if type(val) is not UnicodeType:
            val = unicode(val, self.encoding() )
        self.val     = val
    
    # ------------------------------------------------------------------------------------
    def _repr(self):
        return "%s%s%s"%( escape(unicode(self.conceptObj.label)), self.__class__.symbol, escape(self.val) )

    # ------------------------------------------------------------------------------------
    def _reprXML(self, indent=0, digir=False):
        if digir:
            return '\n%s<%s> <darwin:%s>%s</darwin:%s> </%s>'%(self._getIndention(indent), self.__class__.symXML, self.conceptObj.path, escapeXML(self.val), self.conceptObj.path, self.__class__.symXML )
        else:
            return '\n%s<%s path="%s">%s</%s>'%(self._getIndention(indent), self.__class__.symXML, self.conceptObj.path, escapeXML(self.val), self.__class__.symXML )
        
    # ------------------------------------------------------------------------------------
    def __len__(self):
        return len(self.val)

class LikeClass(BinaryCOPBaseClass):
    '''The LIKE comparison operator.'''
    symbol = u'~'
    symXML = u'like'
    # ------------------------------------------------------------------------------------
    def __init__(self, conceptObj, val):
        BinaryCOPBaseClass.__init__(self, conceptObj, val)
    
                
class EqualsClass(BinaryCOPBaseClass):
    '''The = comparison operator.'''
    symbol = u'='
    symXML = u'equals'
    # ------------------------------------------------------------------------------------
    def __init__(self, conceptObj, val):
        BinaryCOPBaseClass.__init__(self, conceptObj, val)
        
                
class GreaterClass(BinaryCOPBaseClass):
    '''The > comparison operator.'''
    symbol = u'>'
    symXML = u'greaterThan'
    # ------------------------------------------------------------------------------------
    def __init__(self, conceptObj, val):
        BinaryCOPBaseClass.__init__(self, conceptObj, val)
    
                
class LesserClass(BinaryCOPBaseClass):
    '''The < comparison operator.'''
    symbol = u'<'
    symXML = u'lessThan'
    # ------------------------------------------------------------------------------------
    def __init__(self, conceptObj, val):
        BinaryCOPBaseClass.__init__(self, conceptObj, val)
    
                
    
class LOPBaseClass(OperatorBaseClass):
    '''A base class for any logical operator.'''
    # ------------------------------------------------------------------------------------
    def __init__(self):
        OperatorBaseClass.__init__(self)
        self.args = []
        self.iterIndex = 0               # the next iterator index

    # ------------------------------------------------------------------------------------
    def _repr(self):
        if len(self) > 1:
            return '(%s)'%string.join([arg._repr() for arg in self.args], ')%s('%self.__class__.symbol)
        elif len(self) == 1:
            return self.args[0]._repr()
        else:
            return ''
               
    # ------------------------------------------------------------------------------------
    def _reprXML(self, indent=0, digir=False):
        return '\n%s<%s>%s\n%s</%s>'%(self._getIndention(indent), self.__class__.symXML, string.join([arg._reprXML(indent+1,digir) for arg in self.args], ''), self._getIndention(indent), self.__class__.symXML )

    # ------------------------------------------------------------------------------------
    def addOperand(self, filterObj):
        '''Add another operand to this logical operator'''
        if filterObj is not None:
            self.args += [filterObj]

    # ------------------------------------------------------------------------------------
    def __len__(self):
        return len(self.args)

    # ------------------------------------------------------------------------------------
    def __getitem__(self, index):
        return self.args[index]

    # ------------------------------------------------------------------------------------
    def __setitem__(self, index, value):
        self.args[index] = value

    # ------------------------------------------------------------------------------------
    def __iter__(self):
        self.iterIndex = 0               # the next iterator index
        return self                 # simplest iterator creation
        
    # ------------------------------------------------------------------------------------
    def next(self):
        if self.iterIndex < len(self):
            self.iterIndex += 1
            return self.__getitem__(self.iterIndex-1)
        else:
            raise StopIteration

    # ------------------------------------------------------------------------------------
    def optimize(self):
        '''Try to remove double negations as operands. 
        Cannot be handled by the NotClass cause it would lead to the destruction of the NOT instance.
        Also check in the main filter class for double nots at root filter level.'''
        filterObj = self
        # check for double NOTs as child + grantchild
        args = []
        for arg in filterObj:
            if isinstance(arg, NotClass) and isinstance(arg.args, NotClass):
                log.info("  nested NOT removed.")
                args.append(arg.args.args)
            else:
                args.append(arg)
        filterObj = filterObj.__class__(args)
        # look for similar nested unbounded LOPs. flatten them
        if isinstance(filterObj, LOPBaseClass) and not isinstance(filterObj, NotClass):
            args=[]
            log.info("  potential nested LOP detected.")
            for arg in filterObj:
                if isinstance(arg, filterObj.__class__):
                    # found the same LOP operator as a child. flatten
                    log.info("  nested LOP flattened.")
                    args += arg.args
                else:
                    args.append(arg)
            # create a new object with this new operands
            filterObj = filterObj.__class__(args)
            log.info("  new un-nested filter=%s"%unicode(filterObj))
        # look for unbounded LOPs with only 1 argument and remove them.
        if isinstance(filterObj, LOPBaseClass) and not isinstance(filterObj, NotClass) and len(filterObj) < 2:
            if len(filterObj) == 1:
                filterObj = filterObj[0]
                log.info("  LOP with 1 arg removed. New filter=%s"%(unicode(filterObj)))
            else:
                filterObj = None
                log.info("  LOP with 0 args removed. New filter=%s"%(unicode(filterObj)))
        # now check the children again...
        if isinstance(filterObj, LOPBaseClass):
            for arg in filterObj:
                arg.optimize()
        # return final object
        return filterObj

                
class AndClass(LOPBaseClass):
    '''The logical AND operator.'''
    symbol = u'AND'
    symXML = u'and'
    # ------------------------------------------------------------------------------------
    def __init__(self, args=[]):
        LOPBaseClass.__init__(self)
        self.args = [a for a in args if a is not None]
    

class OrClass(LOPBaseClass):
    '''The logical OR operator.'''
    symbol = u'OR'
    symXML = u'or'
    # ------------------------------------------------------------------------------------
    def __init__(self, args=[]):
        LOPBaseClass.__init__(self)
        self.args = [a for a in args if a is not None]
    

class NotClass(LOPBaseClass):
    '''The logical NOT operator.'''
    symbol = u'NOT'
    symXML = u'not'
    # ------------------------------------------------------------------------------------
    def __init__(self, arg=None):
        LOPBaseClass.__init__(self)
        self.args = arg
    
    # ------------------------------------------------------------------------------------
    def addOperand(self, filterObj):
        '''Adding another operand to this logical NOT operator is not possible'''
        if self.args is None:
            self.args=filterObj
        else:
            raise UnaryOperatorError

    # ------------------------------------------------------------------------------------
    def _repr(self):
        if self.args is None:
            return '%s(None)'%self.__class__.symbol
        else:
            return '%s(%s)'%(self.__class__.symbol, self.args._repr() )
               
    # ------------------------------------------------------------------------------------
    def _reprXML(self, indent=0, digir=False):
        if self.args is None:
            return '\n%s<%s>%s\n%s</%s>'%(self._getIndention(indent), self.__class__.symXML, 'None', self._getIndention(indent), self.__class__.symXML )
        else:
            return '\n%s<%s>%s\n%s</%s>'%(self._getIndention(indent), self.__class__.symXML, self.args._reprXML(indent+1, digir), self._getIndention(indent), self.__class__.symXML )
    
    
# ------------------------------------------------------------------------------------
class Concept:
    '''A single concept.'''
    def __init__(self, NS, path, label=None):
        self.label = label
        self.NS    = NS
        self.path  = path

    def __repr__(self):
        return '''Concept: label=%s, ns=%s, path=%s'''%(self.label, self.NS, self.path)

    
class Literal(OperatorBaseClass):
    # ------------------------------------------------------------------------------------
    def __init__(self, arg):
        if type(arg) is not UnicodeType:
            arg = unicode(arg, self.encoding() )
        self.args = arg

    # ------------------------------------------------------------------------------------
    def __len__(self):
        return len(self.args)

    # ------------------------------------------------------------------------------------
    def _repr(self):
        return escape(self.args)
               
    # ------------------------------------------------------------------------------------
    def _reprXML(self, indent=0, digir=False):
        if digir:
            return '%s'%(self.args)
        else:
            return '\n%s<literal value="%s">'%(self._getIndention(indent), self.args )



############################################################################################################
#
#   FUNCTIONS
#
#===========================================================================================================

def escapeXML(x):
    '''Escape basic XML entities <>& .'''
    if type(x) in (StringType, UnicodeType):
        x = x.replace('&','&amp;')
        x = x.replace('>','&gt;')
        x = x.replace('<','&lt;')
    return x
    
# ------------------------------------------------------------------------------------
def escape(x):
    '''Escape the characters <=>~()\ with a backslash in front.'''
    if type(x) in (StringType, UnicodeType):
        x = x.replace('\\','\\\\')
        x = x.replace('<','\<')
        x = x.replace('=','\=')
        x = x.replace('>','\>')
        x = x.replace('~','\~')
        x = x.replace('(','\(')
        x = x.replace(')','\)')
    return x

# ------------------------------------------------------------------------------------
def unescape(x):
    '''Escape the characters <=>~() with a backslash.'''
    if type(x) in (StringType, UnicodeType):
        x = x.replace('\\\\','\\')
        x = x.replace('\<','<')
        x = x.replace('\=','=')
        x = x.replace('\>','>')
        x = x.replace('\~','~')
        x = x.replace('\(','(')
        x = x.replace('\)',')')
    return x

# ------------------------------------------------------------------------------------
def getOptimizedFilterObj(filterObj):
    try:
        log.info("FUNC OPTIMIZING FILTER:  %s"%unicode(filterObj))
    except:
        log.info("FUNC OPTIMIZING FILTER:  ASCII ERROR")
    filterObj = filterObj.optimize()
    log.info("  FILTER OBJ (self.optimized):  %s"%unicode(filterObj))
    # finally check for double NOTs at root level
    if isinstance(filterObj, NotClass) and isinstance(filterObj.args, NotClass):
        filterObj = filterObj.args.args
    # is the root a logical operator with at least 2 args?
    if isinstance(filterObj, LOPBaseClass) and not isinstance(filterObj, NotClass) and len(filterObj) < 2:
        filterObj = filterObj[0]
    log.info("  FILTER OBJ (func optimized):  %s"%unicode(filterObj))
    return filterObj
                       

# ------------------------------------------------------------------------------------
class node:
    def __init__(self):
        self.children = []
        self.parent = None
        #print "\nnew node created."
    def __len__(self):
        return len(self.children)
    def __repr__(self):
        return str(self.children)
    def _repr(self, indent=0):
        r = []
        pre = '\n' + '  '*indent
        for c in self.children:
            if isinstance(c, node):
                r.append( c._repr(indent+1) )
            else:
                r.append( "%s'%s'" %(pre,c) )
        return string.join( r )
    def addChild(self):
        child = node()
        child.parent=self
        self.children.append(child)
        return child
    def addData(self, data):
        self.children.append(data)
        #print "data added: %s"%data
    def getParent(self):
        return self.parent
    def getFilter(self, schemaObj=None, indent=1):
        filter = None
        i = 0
        for c in self.children:
            i += 1
            if isinstance(c, node):
                if filter is None:
                    filter = c.getFilter(schemaObj, indent+1)
                else:
                    filter.addOperand( c.getFilter(schemaObj, indent+1) )
            elif c is None:
                filter = None
            elif c.lower() == 'and':
                filter = AndClass( [filter] )
            elif c.lower() == 'or':
                filter = OrClass( [filter] )
            elif c.lower() == 'not':
                filter = NotClass(  )
            elif c.find('~') > -1 and c[c.find('~')-1] <> '\\':
                filter = self.getFilterWithCOPAdded(filter, c, LikeClass, schemaObj)
            elif c.find('=') > -1 and c[c.find('=')-1] <> '\\':
                filter = self.getFilterWithCOPAdded(filter, c, EqualsClass, schemaObj)
            elif c.find('<') > -1 and c[c.find('<')-1] <> '\\':
                filter = self.getFilterWithCOPAdded(filter, c, LesserClass, schemaObj)
            elif c.find('>') > -1 and c[c.find('>')-1] <> '\\':
                filter = self.getFilterWithCOPAdded(filter, c, GreaterClass, schemaObj)
            else:
                raise "unknown filter compound: %s   self=%s"%(c,unicode(self))
        if filter is not None:
            filter.optimize()
        return filter
    def getFilterWithCOPAdded(self, filter, COPstring, COPClass, schemaObj):
        # find the (escaped) label and value of the COPstring
        if type(COPstring) is not UnicodeType:
            COPstring = unicode(COPstring, 'utf-8')
        pos = COPstring.find(COPClass.symbol)
        while COPstring[pos-1] == '\\':
            pos = COPstring.find(COPClass.symbol)
        label = COPstring[:pos]
        val   = COPstring[pos+1:]
        # first unescape the strings (label & val)
        label = unescape(label)
        val   = unescape(val)
        # create/get concept object
        if schemaObj is not None and schemaObj.knowsLabel(label):
            con = schemaObj.getConcept(label)
        else:
            raise "Concept Label Unknown!"
            con = Concept(label=label)
        # create COP object
        newFilter = COPClass(con, val)
        # integrate in existing filter
        if filter is None:
            return newFilter
        else:
            return filter.addOperand(newFilter)


# ------------------------------------------------------------------------------------
def binaryCopObjectFactory(CopClass, conceptObj, val, multiValLOPClass=OrClass):
    '''Factory to create COP objects from a concept object and values. 
    If multiple values exist for the same COP, then create a LOP with multiple binary COPs.
    This LOP is by default an OrClass'''
    if type(val) in (ListType,TupleType) and len(val) > 1:
        # create multiple binary COPs with LOPs combining them
        filterObj = multiValLOPClass()
        for v in val:
            filterObj.addOperand( CopClass(conceptObj, v) )
    else:
        if type(val) in (ListType,TupleType):
            val = val[0]
        filterObj = CopClass(conceptObj, val)
    return filterObj

# ------------------------------------------------------------------------------------
def createFilterObjFromString(filterString, schemaObj=None):
    '''Deserialize filter string:
    Creates a filter object from a simple human string representation of a filter
    like the one you get when printing a filter object.'''
    # first take the string and create a tree for all brackets.
    tree = node()
    curr = tree
    tmp = ''
    escape = False
    for c in filterString:
        #print c.encode('utf-8'),
        if c == '\\':
            escape = True
        elif c is None:
            pass
        else:
            if c == "(" and not escape:
                if len(tmp) > 0:
                    curr.addData(tmp)
                    tmp = ''
                curr = curr.addChild()
            elif c == ")" and not escape:
                if len(tmp) > 0:
                    curr.addData(tmp)
                    tmp = ''
                curr = curr.getParent()
            else:
                pass
                tmp += c
            escape = False
        pass
    if len(tmp) > 0:
        tree.addData(tmp)
    filterObj = tree.getFilter(schemaObj)
    if filterObj is None:
        filterObj = AndClass()
    return filterObj




# ------------------------------------------------------------------------------------
if __name__ == '__main__':
    import os, cgi

    # ***** include the biocase.lib directory in the python sys path for module importing *****
    execfile( os.path.join(r'D:\workspace\bps2\lib\biocase', 'adjustpath.py' ) )

    from biocase.tools.generators import *
    a = AndClass(  [ x for x in cop_generator(3)] )

    print "Thats 'A': %s" %a
    print "Each argument seperately:"
    for op in a:
        print op
        
    print "-"*40
    #    b = OrClass( [ x for x in cop_generator(2)] )
    #    c = NotClass(b)
    #    d = Literal('Peter')
    #
    #    x = a+b+c
    #    print
    #    print " --- original ---"
    #    print x._reprXML()
    #
    #    print
    #    print " --- optimized ---"
    #    x.optimize()
    #    print x._reprXML()
    #    
    #    print
    #    print " --- Filter Strings ---"
    #    print x

    print
    print " --- string 1 ---"
    fstring = u'(Scientific_Name~d*)AND(Scientific_Name=Dactylorhiza eléta \(Poiret\) Soo)'
    print fstring.encode('utf-8')
    print
    print " --- Filter from string 1: unicode().encode() ---"
    filter = createFilterObjFromString( fstring )
    print unicode(filter).encode('utf-8')
    print
    print " --- Filter from string 1: str() ---"
    print str(filter)
    print
    print " --- Filter from string 1: _reprXML() ---"
    print filter._reprXML().encode('utf-8')

    print '-'*40
    print
    print " --- string 2 ---"
    fstring = u'(Scientific_Name~d*)AND(Scientific_Name=Dactylorhiza \(Poiret\) Soo)'
    print fstring.encode('utf-8')
    print
    print " --- Filter from string 2 ---"
    filter = createFilterObjFromString( fstring )
    print filter
    
