import datetime
import logging

from SystemInterface import Session
from orangescape.model.server.wrapper.Model import DynamicModel
from orangescape.model.engine.exception.Error import InstanceNotFoundError
from orangescape.model.engine.execution.ExecutionTree import ExecutionTree
from Interface import Node
from orangescape.model.server.PerfMonitor import stopPerfMonitor, startSessionFormulaMonitor

class AbstractSession(Session):
    application = None
    updatedSheets = None
    deletedSheets = None
    user=None
    modifiedIn = None
#    totalGetListTime = datetime.datetime.now()-datetime.datetime.now()
#    totalGetListCall = 0
    applicationId = None

    def __init__(self, application):
        self.application = application
        self.updatedSheets = dict()
        self.deletedSheets = dict()
        self.attachmentCache = {'created':dict(), 'updated':dict(), 'deleted':dict()}
        self.attachmentVersionCache = {}
#        self.applicationId = self.application.getSheetId()

    def generateUUID(self):
        import uuid
        return uuid.uuid1()

    def createModel(self, metadata, keyName):
        metadataId = metadata.getSheetId()
        # keyName = str(uuid.uuid1())

        data = self.createDataObject(metadata, keyName)

        data.SheetMetadataId = metadataId
        model = DynamicModel(data, metadata, self)
        sheets = self.updatedSheets.get(metadataId)
        if sheets is None:
            sheets = dict()
            self.updatedSheets[metadataId] = sheets
        sheets[model.getSheetId()] = model
        return model

    def __generate_pk__(self, metadata):
        return metadata.getSheetName()[:2]+str(self.generateUUID()).replace('-', '_')

    def get(self, sheetId, metadata):
        pm = startSessionFormulaMonitor({'name':'Get','sheetName':metadata.getSheetName(),'type':'Session','inputValues':sheetId})
        model = self.getIfExists(sheetId, metadata)
        stopPerfMonitor(pm,model)
        if model is not None:
            return model
        logging.info("Sheet %s not found for metadata %s"%(sheetId, metadata.getSheetName()))
        raise InstanceNotFoundError("Could not find the sheet for the key '%s' for the sheet %s"%(sheetId, metadata.getSheetName()))

    def getIfExists(self, sheetId, metadata):
        if not sheetId:
            return None
        pm = startSessionFormulaMonitor({'name':'GetIfExists','sheetName':metadata.getSheetName(),'type':'Session','inputValues':sheetId})
        metadataId = metadata.getSheetId()

        sheets = self.updatedSheets.get(metadataId)
        if sheets and sheets.__contains__(sheetId):
            stopPerfMonitor(pm)
            return sheets[sheetId]
        if self.deletedSheets.__contains__(metadataId) and self.deletedSheets[metadataId].__contains__(sheetId):
            "Object is deleted from the session but not yet committed"
            stopPerfMonitor(pm)
            return None

        sheet = self.readFromDatastore(sheetId, metadata)
        if sheet is not None:
            model = DynamicModel(sheet, metadata, self)
            stopPerfMonitor(pm)
            return model
        stopPerfMonitor(pm)
        return None

    def readFromDatastore(sheetId, metadata):
        raise NotImplementedError

    def put(self, model):
        metadata = model.getSheetMetadata()
        metadataId = metadata.getSheetId()
        sheets = self.updatedSheets.get(metadataId)
        if sheets is None:
            sheets = dict()
            self.updatedSheets[metadataId] = sheets
        sheets[model.getSheetId()] = model

    def getList(self, model, referenceCell):
        pm = startSessionFormulaMonitor({'name':'GetList','sheetName':model.getSheetMetadata().getSheetName(),'type':'session','inputValues':model.getSheetId()})
        cacheData = self.getListFromDatastore(model, referenceCell)
        res = self.checkWithSessionCache(model, referenceCell, cacheData)
        stopPerfMonitor(pm)
        return res

    def checkWithSessionCache(self, model, referenceCell, cacheData):
        sessCacheRes = dict()
        metadata = referenceCell.getRowMetadata().getSheetMetadata()
        metadataId = metadata.getSheetId()
        if self.updatedSheets.__contains__(metadataId):
            sheets = self.updatedSheets[metadataId]
            for id, s in sheets.iteritems():
                if s.get(referenceCell) is not None and s.get(referenceCell) == model.getSheetId():
                     sessCacheRes[s.getSheetId()] = s
                else:
                    """If the foreign key is updated in the same transaction then the id should be removed from the cacheData"""
                    if cacheData.__contains__(id):
                        del cacheData[id]

        delCache = self.deletedSheets.get(metadataId)
        if len(sessCacheRes)==0 and (delCache is None or len(delCache)==0):
            results = [DynamicModel(data, metadata, self) for id, data in cacheData.iteritems()]
        elif len(sessCacheRes)>0 and (delCache is None or len(delCache)==0):
            results = [DynamicModel(data, metadata, self) for id, data in cacheData.iteritems() if (not sessCacheRes.__contains__(id))]
        elif len(sessCacheRes)==0 and delCache is not None and len(delCache)>0:
            results = [DynamicModel(data, metadata, self) for id, data in cacheData.iteritems() if not (delCache.__contains__(id))]
        else:
            results = [DynamicModel(data, metadata, self) for id, data in cacheData.iteritems() if not (delCache.__contains__(id)) and (not sessCacheRes.__contains__(id))]

        if len(results) > len(sessCacheRes):
            if len(sessCacheRes)>0:
                results.extend(sessCacheRes.values())
            return results
        if len(results)>0:
            tmp = sessCacheRes.values()
            tmp.extend(results)
            return tmp
        return sessCacheRes.values()

    def getListFromDatastore(self, model, referenceCell):
        raise NotImplementedError

    def filter(self, model, referenceCell, criteriaNode):
        et = ExecutionTree(self)
        return [child for child in self.getList(model, referenceCell) if et.executeNode(criteriaNode, child)]

    def query(self, metadata, limit=2000, baseQuery=None, criteriaNode=None, orderBy = None, isDescending = False, offset=0):
        import appconfig as config
        if config.env != config.SL and logging.getLogger().level == logging.DEBUG or config.DEBUGGER_ENABLED is not None:
            baseQueryStr = self.getQS(metadata, baseQuery, '')
            orderByStr = ''
            if orderBy:
                orderByStr = 'ORDER BY %s'%(metadata.getCellMetadataById(orderBy).getCellName())
                if isDescending:
                    orderByStr+=' desc'
            if config.env != config.SL and logging.getLogger().level == logging.DEBUG:
                logging.debug('Query: SELECT * FROM %s WHERE %s %s'%(metadata.getSheetName(), baseQueryStr, orderByStr))
            values = baseQueryStr + orderByStr
            pm = startSessionFormulaMonitor({'name':'Query','sheetName':metadata.getSheetName(),'type':'session','inputValues':values})
        cacheData = self.updatedSheets.get(metadata.getSheetId())
        delCache = self.deletedSheets.get(metadata.getSheetId())
        cache_qs_results = {}
        cache_deleted_records = {}
        if baseQuery and len(baseQuery)>0:
            if cacheData:
                qs = self.getQS(metadata, baseQuery, 'cacheData[id].getValue("')
                for id in cacheData:
                    if eval(qs):
                        cache_qs_results[id] = cacheData[id]

            if delCache:
                qs = self.getQS(metadata, baseQuery, 'delCache[id].getValue("')
                for id in delCache:
                    if eval(qs):
                        cache_deleted_records[id] = delCache[id]
#                cache_deleted_records = [delCache[id] for id in delCache if eval(qs)]
        else:
            if cacheData:
                cache_qs_results = cacheData
            else:
                cache_qs_results = {}

        if not baseQuery:
            baseQuery = dict()
        dbResults = [DynamicModel(data, metadata, self) for data in self.queryFromDataStore(metadata, baseQuery, limit, offset, orderBy, isDescending) if (not cache_qs_results.has_key(data.getSheetId()) and not cache_deleted_records.has_key(data.getSheetId()))]

        if(cache_qs_results):
            dbResults.extend(cache_qs_results.values())

        if criteriaNode is not None:
            et = ExecutionTree(self)
            dbResults = [model for model in dbResults if et.executeNode(criteriaNode, model)]

        if orderBy and cache_qs_results:
            cell = metadata.getCellMetadataById(orderBy)
            dbResults.sort(lambda x, y: cmp(x.get(cell), y.get(cell)), reverse=isDescending or False)
        if limit:
            dbResults = dbResults[:limit]

        if config.env != config.SL and logging.getLogger().level == logging.DEBUG or config.DEBUGGER_ENABLED is not None:
            stopPerfMonitor(pm,len(dbResults))
        return dbResults

    def advQuery(self, metadata, limit=2000, baseQuery=None, criteriaNode=None, orderBy = None, isDescending = False, offset=0):
        import appconfig as config
        if config.env != config.SL and logging.getLogger().level == logging.DEBUG :
            baseQueryStr = self.getAdvQS(metadata, baseQuery, '')
            orderByStr = ''
            if orderBy:
                orderByStr = 'ORDER BY %s'%(metadata.getCellMetadataById(orderBy).getCellName())
                if isDescending:
                    orderByStr+=' desc'
            if config.env != config.SL and logging.getLogger().level == logging.DEBUG:
                logging.debug('Query: SELECT * FROM %s WHERE %s %s'%(metadata.getSheetName(), baseQueryStr, orderByStr))
        cacheData = self.updatedSheets.get(metadata.getSheetId())
        delCache = self.deletedSheets.get(metadata.getSheetId())
        cache_qs_results = {}
        cache_deleted_records = {}
        if baseQuery and len(baseQuery)>0:
            if cacheData:
                qs = self.getAdvQS(metadata, baseQuery, 'cacheData[id].getValue("')
                for id in cacheData:
                    if eval(qs):
                        cache_qs_results[id] = cacheData[id]

            if delCache:
                qs = self.getAdvQS(metadata, baseQuery, 'delCache[id].getValue("')
                for id in delCache:
                    if eval(qs):
                        cache_deleted_records[id] = delCache[id]
#                cache_deleted_records = [delCache[id] for id in delCache if eval(qs)]
        else:
            if cacheData:
                cache_qs_results = cacheData
            else:
                cache_qs_results = {}

        if not baseQuery:
            baseQuery = dict()
        dbResults = [DynamicModel(data, metadata, self) for data in self.adv_query_from_ds(metadata, baseQuery, limit, offset, orderBy, isDescending) if (not cache_qs_results.has_key(data.getSheetId()) and not cache_deleted_records.has_key(data.getSheetId()))]

        if(cache_qs_results):
            dbResults.extend(cache_qs_results.values())

        if criteriaNode is not None:
            et = ExecutionTree(self)
            dbResults = [model for model in dbResults if et.executeNode(criteriaNode, model)]

        if orderBy and cache_qs_results:
            cell = metadata.getCellMetadataById(orderBy)
            dbResults.sort(lambda x, y: cmp(x.get(cell), y.get(cell)), reverse=isDescending or False)
        if limit:
            dbResults = dbResults[:limit]
        return dbResults

    def __mergeCache__(self, metadata, sessCacheRes, dbResults):
        delCache = self.deletedSheets.get(metadata.getSheetId())
        if delCache is None or len(delCache)==0:
            if sessCacheRes is None or len(sessCacheRes)==0:
                logging.debug('__mergeCache__: Data not available in session and del cache, loading data from db')
                results = [DynamicModel(data, metadata, self) for data in dbResults]
                return results
            else:
                logging.debug('__mergeCache__: del cache not available, loading data from session cache')
                results = [DynamicModel(data, metadata, self) for data in dbResults if not sessCacheRes.__contains__(data.getSheetId())]
                if len(results) > len(sessCacheRes):
                    results.extend(sessCacheRes.values())
                    return results
                else:
                    return sessCacheRes.values()
        else:
            if sessCacheRes is None or len(sessCacheRes)==0:
                logging.debug('__mergeCache__: session cache not available. Loading del cache data if exists')
                results = [DynamicModel(data, metadata, self) for data in dbResults if not (delCache.__contains__(data.getSheetId()))]
                return results
            else:
                logging.debug('__mergeCache__: checks both session & del cahce exists')
                results = [DynamicModel(data, metadata, self) for data in dbResults if not (delCache.__contains__(data.getSheetId())) and (not sessCacheRes.__contains__(data.getSheetId()))]
                if len(results) > len(sessCacheRes):
                    results.extend(sessCacheRes.values())
                    return results
                else:
                    return sessCacheRes.values()

    def construct_memoryfilter_param(self, metadata, lhs, operator, rhs, cacheStr):
        q = list()
        if operator.strip().lower()=='like' and cacheStr!='':
            q.append('(')
            q.append(cacheStr)
            q.append(metadata.getCellMetadataById(lhs).getCellName())
            q.append('") or "").lower().find("')
            q.append(str(rhs).lower())
            q.append('") > -1')
        else:
            q.append(cacheStr)
            q.append(metadata.getCellMetadataById(lhs).getCellName())
            if cacheStr!='':
                q.append('") ')
            else:
                q.append(' ')
            if len(lhs)>1:
                q.append(operator.strip())
                if operator.strip() =='=' and cacheStr!='':
                    q.append(operator)
            q.append(' ')
            if isinstance(rhs, str) or isinstance(rhs,unicode):
                q.append('"')
                q.append(rhs)
                q.append('"')
            else:
                q.append(str(rhs))
        return ''.join(q)

    def getAdvQS(self, metadata, baseQuery, cacheStr):
        bool_operator, filter_params = baseQuery
        qs = list()
        for tup in filter_params:
            if len(tup)==2:
                qs.append(self.getAdvQS(metadata, tup, cacheStr))
            else:
                lhs, operator, rhs = tup
                qs.append(self.construct_memoryfilter_param(metadata, lhs, operator, rhs, cacheStr))
        s = ' '+bool_operator+' '
        return '(' + s.join(qs) + ')'

    def getQS(self, metadata, base_query, cacheStr):
        qs = list()
        for lhs, rhs in base_query.iteritems():
            lhs, operator = lhs
            qs.append(self.construct_memoryfilter_param(metadata, lhs, operator, rhs, cacheStr))
        return ' and '.join(qs)

    def queryFromDataStore(self, metadata, baseQuery, limit, offset, orderBy, isDescending):
        raise NotImplementedError

    def delete(self, model):
        metadata = model.getSheetMetadata()
        metadataId = metadata.getSheetId()
        delSheets = self.deletedSheets.get(metadataId)
        if delSheets is None:
            delSheets = dict()
            self.deletedSheets[metadataId] = delSheets
        delSheets[model.getSheetId()] = model
        if self.updatedSheets.__contains__(metadataId) and self.updatedSheets[metadataId].__contains__(model.getSheetId()):
            del self.updatedSheets.get(metadataId)[model.getSheetId()]

    def getApplication(self):
        return self.application

    def getMetadata(self, metadataName):
        return self.getApplication().getSheetMetadata(metadataName)

    def getMetadataById(self, metadataId):
        return self.getApplication().getSheetMetadataById(metadataId)

    def getUser(self):
    #        return getCurrentUser(self)
        from orangescape.tests.mock.MockUser import User
        return User()

    def setUser(self,userins):
          self.user=userins

    def rollback(self):
        self.clearCache()
        self.rollback_datastore()

    def commit(self):
        self.commit_datastore()
        self.clearCache()

    def isReferenceChanged(self, model, refCell):
        oldData = model.getOriginalData()
        if oldData.get(refCell.getSheetId()) is None and model.get(refCell) is None:
            return False
        if oldData.get(refCell.getSheetId()) is None and model.get(refCell) is not None:
            return True
        if model.get(refCell) is None and oldData.get(refCell.getSheetId()) is not None:
            return True
        return not model.get(refCell) == oldData.get(refCell.getSheetId())

    def clearCache(self):
        self.updatedSheets.clear()
        self.deletedSheets.clear()

    def setModifiedIn(self, activityDefinitionId):
        self.modifiedIn = activityDefinitionId

    def getModifiedIn(self):
        return self.modifiedIn

    def getBySheetName(self, sheetName, metadata):
        sheetList = self.query(metadata, 10, {(metadata.getPublicCellMetadata('SheetName').getSheetId(), '='):sheetName})
        if len(sheetList)>0:
            return sheetList[0]
        return None

    def getByCellValue(self, value, metadata, cell):
        sheetList = self.query(metadata, 1, {(cell.getSheetId(), '=') : value})
        if len(sheetList)>0:
            return sheetList[0]
        return None

    def updateAttachment(self, id, file, tag=False):
        compressed = self.__compress__(file)

        if self.attachmentCache['created'].has_key(id):
            self.attachmentCache['created'][id] = compressed
        elif self.attachmentCache['updated'].has_key(id):
            self.attachmentCache['updated'][id] = compressed
        else:
            attachment = self.getAttachmentRecord(id)
            if attachment is None:
                self.attachmentCache['created'][id] = compressed
            else:
                self.attachmentCache['updated'][id] = compressed
        if tag:
            self.attachmentVersionCache[id] = {'comments':tag, 'content': compressed}

    def copyAttachment(self, srcId, tarId):
        dbData = self.getAttachmentFromDS(srcId)
        if dbData:
            self.attachmentCache['created'][tarId] = dbData

    def getAttachment(self, id):
        attachment = self.attachmentCache['created'].get(id) or self.attachmentCache['updated'].get(id)
        if attachment:
            return self.__decompress__(attachment)
        if self.attachmentCache['created'].has_key(id):
            return None
        dbData = self.getAttachmentFromDS(id)
        if dbData is not None:
            return self.__decompress__(dbData)
        return dbData

    def deleteAttachment(self, id):
        if self.attachmentCache['created'].has_key(id):
            del self.attachmentCache['created'][id]
        else:
            if self.attachmentCache['updated'].has_key(id):
                del self.attachmentCache['updated'][id]

            self.attachmentCache['deleted'][id] = id

    def getAttachmentList(self, parentSheet, referenceCell):
        raise NotImplementedError

    def clearAttachmentCache(self):
        self.attachmentCache['created'].clear()
        self.attachmentCache['updated'].clear()
        self.attachmentCache['deleted'].clear()
        self.attachmentVersionCache.clear()

    def getMultiCache(self, metadataIdDict, rsDict):
        cacheMissedIdDict = {}
        for id, metadataId in metadataIdDict.iteritems():
            if id:
                if self.updatedSheets.has_key(metadataId) and self.updatedSheets[metadataId].has_key(id):
                    rsDict[id] = self.updatedSheets[metadataId][id]
                elif self.deletedSheets.has_key(metadataId) and self.deletedSheets[metadataId].has_key(id):
                    """Object is deleted but not yet committed"""
                else:
                    cacheMissedIdDict[id] = metadataId
        return cacheMissedIdDict

    def getMulti(self, metadataIdDict):
        pm = startSessionFormulaMonitor({'name':'GetMulti','sheetName':str(metadataIdDict.values()),'type':'session','inputValues':metadataIdDict})
        rsDict = {}
        cacheMissedIdDict = self.getMultiCache(metadataIdDict, rsDict)
        dataDict = self.getMultiFromDatastore(cacheMissedIdDict)
        for id, dataObject in dataDict.iteritems():
            if dataObject is None:
                rsDict[id] = None
            else:
                rsDict[id] = DynamicModel(dataObject, self.getApplication().getSheetMetadataById(dataObject.getSheetMetadataId()), self)
        stopPerfMonitor(pm,len(rsDict))
        return rsDict

    def getMultiFromDatastore(self, metadataIdDict):
        raise NotImplementedError

    def __compress__(self, data):
        import zlib
        if type(data) in (str,unicode):
            return zlib.compress(data)
        else:
            logging.warn('Casting object type %s into string. While retrieving the object it will be string only'%(type(data)))
            return zlib.compress(str(data))

    def __decompress__(self, compressed):
        import zlib
        try:
            return zlib.decompress(compressed)
        except zlib.error, e:
            logging.warn('Invalid compression. %s'%(e))
            return compressed

    def getPrivate(self, sheetId, metadata):
        sheet = self.getIfExists(sheetId, metadata)
        if sheet is not None:
            return getPrivateFromDS(sheet.getDataObject())
        return None

    def executeAggregateFunction(self, et, sheet, referenceCell, aggNode, fnNode, criteria, orderBy, reverse):
        return et.evaluateAggrigate3DFunction(sheet, referenceCell, aggNode, fnNode, criteria, orderBy, reverse)

    def getVersion(self, key):
        version = self.getVersionFromDS(key)
        if version:
            content = version.content
            if content:
                return self.__decompress__(content)
        return None

    def get_by_keys(self, metadata, keys):
        return [DynamicModel(entity, metadata, self) for entity in self.get_by_keys_from_ds(metadata, keys)]

class QueryNode(Node):
    node = None
    str = False
    nodeValue = None
    nodeType = None
    formula = None
    nodeList = None
    publicRef = False
    argNo = float()
    cellMetadata = None

    def __init__(self):
        self.nodeList = list()

    def isString(self):
        return self.str

    def getNodeValue(self):
        return self.nodeValue

    def getFormula(self):
        return self.formula

    def getNode(self):
        return self.node

    def getNodeList(self):
        self.nodeList.sort(lambda x, y: cmp(x.argNo,y.argNo))
        return self.nodeList

    def isRootNode(self):
        return self.formula is not None

    def getValueType(self, sheet):
        raise NotImplementedError

    def isPublicRef(self):
        return self.publicRef

    def getArgNo(self):
        return self.argNo

    def getNodeType(self):
        return self.nodeType

    def getCellMetadata(self):
        return self.cellMetadata
