
# Engine delegates checking to the evaluators in evaluate.py depending
# on loaded ConstraintModels and checked element type.

# The Evaluator implementation is found by reading the path from the
# Meta-model Constraint element.

# Results are created if apply in the Evaluator returns False for some
# element. SearchApply returns a list of Results so these are simply
# appended to the ConstraintModel.

# These are the most frequently used functions:
#
#  Evaluates a specific constraint on all the elements owned by targetModel
#   def checkSingleConstraint(constraint, targetModel):
#
#  Evaluates all constraints in the ConstraintModel CM on CM.target
#   def reEvaluateAll(CM):
#
#  Evaluates loaded constraints on elem. Checks which ConstraintModels that
#  apply to the model containing elem and selects the Constraints that apply
#  to elem.
#   def evaluate(elem):
    
import coral
from evaluate import *
import time
from coral.modeler import action
from coral.modeler.extension import getFactory

from coral.modeler.coralmodeler import CoralModeler,ContinuousModelObserver
import sys

#__all__ = ["loadModel", "printConstraints", "getConstraintsByType", "getConstraintsByName", "loadTarget", "__engine__"]
L = coral.lang.CONSTRAINT10
TM= coral.getTransactionManager()
import time        

class Engine:
    def __init__(self):
        self.constraints = {}
        self.inActiveConstraintModels = coral.Set()
        self.activeConstraintModels = coral.Set()
        self.constraintObjects={}
        self.targetProjects = []
        self.evaluateQueue=[]
        self.resultBuffer=[]
        self.type2constraintObjects={}
        
    def addConstraint(self, constr):
        assert constr.oclIsKindOf(L.Constraint)
        typeName = constr.__type__().getName()
        if self.constraints.has_key(typeName):
            self.constraints[typeName].append(constr)
        else:
            self.constraints[typeName] = [constr]

    def addConstraintModel(self, constrM):
        assert constrM.oclIsKindOf(L.ConstraintModel)
        self.inActiveConstraintModels.append(constrM)

    def addTargetProject(self, prj):
        self.targetProjects.append(prj)

    def checkElement(self,element):
        evaluate(element)

    def activate(self,CM):
        if not CM in self.inActiveConstraintModels:
            return
        self.type2constraintObjects={}
        self.constraintObjects={}
        self.activeConstraintModels.append(CM)
        self.inActiveConstraintModels.drop(CM)

    def deActivate(self,CM):
        if not CM in self.activeConstraintModels:
            return
        self.activeConstraintModels.drop(CM)
        self.inActiveConstraintModels.append(CM)

    def getConstraintObject(self,constr):
        assert constr.oclIsKindOf(L.Constraint)
        constrObject=self.constraintObjects.get(constr,None)
        if constrObject:
            return constrObject
        
        if constr.oclIsKindOf(L.PythonConstraint):
            if not constr.kind:
                raise Exception("PythonConstraint without kind")
            
        ##pcObj=PythonConstraintEvaluator(constr)
        c = getFactory(constr.getImplementationPath()) 
        #consPath = constr.getImplementationPath()
        pcObj = c(constr)
        self.constraintObjects[constr]=pcObj
        return pcObj

##        if constr.oclIsKindOf(L.QueryScriptConstraint):
##            qscObj=QueryScriptConstraintEvaluator(constr)
##            self.constraintObjects[constr]=qscObj
##            return qscObj
            

    def getConstraints(self,cm,elem):
        """Returns already created objects""" 
        if self.type2constraintObjects.has_key( (elem.__type__(),cm) ):
            return self.type2constraintObjects[(elem.__type__(),cm)]
        
        objects=[]
        constraint_set=coral.Set()
        for element in cm.constraint:
            constraint_set.unionWith(element.__getAllParts__().select(lambda p: p.oclIsKindOf(L.Constraint)))

        TM.beginModifyCmd()
        try:
            for constr in constraint_set: 
                try:
                    obj=self.getConstraintObject(constr)
                except Exception, exce:
                    r=L.Result(message=str(exce), name=constr.name, constraint=constr)
                    # cm.result.append( r )
                    addResult(r, cm)
                    continue

                objects.append(obj)

        finally:
            TM.endModifyCmd()


        constraintList=filter(lambda o:o.appliesTo(elem) ,objects)
        self.type2constraintObjects[(elem.__type__(),cm)]=constraintList
        return constraintList

    def addBufferedResultsToModel(self):
        """Adds the buffered results to the result slot in the constraint model"""

        TM.beginModifyCmd()
        try:
            for res,cm in self.resultBuffer:
                addResult(res,cm)
            self.resultBuffer=[]
        finally:
            TM.endModifyCmd()

def updateResults():
    __engine__.addBufferedResultsToModel()

def isEvaluating():
    return not len(__engine__.evaluateQueue)==0
        
class ContinuousEvaluator(ContinuousModelObserver):
    def __init__(self):
        self.next_elem=0
        self.allConstraintTabs = []
    
    def doIt(self):
        """Runs the evaluate() for every element in which a slot has changed in an transaction."""
        queue = __engine__.evaluateQueue

        if self.next_elem == 0:
            self.allConstraintTabs = self.findConstraintTabs()
            for ct in self.allConstraintTabs:
                ct.setProgressPictureOn()
        if len(queue) <= self.next_elem:
            CoralModeler.getInstance().removeContinuousObserver(self)
            __engine__.addBufferedResultsToModel()
            __engine__.evaluateQueue=[]
            self.next_elem=0
            for ct in self.allConstraintTabs:
                ct.setProgressPictureOff()
            return

        MAX_EVAL=1

        while self.next_elem < len(queue) and MAX_EVAL > 0:
            MAX_EVAL-=1
            evaluate(queue[self.next_elem])
            self.next_elem+=1

    def findConstraintTabs(self):
        """Looks for the Constraint Tab from the set of editors of the current handler"""
        tabs = []
        self.window = action.getCurrentHandler("modeler")
        for e in self.window.editorSet.getEditors():
            if isinstance(e,coral.addins.constraint.constraintTab.ConstraintTab):
                tabs.append(e)
        return tabs


            
__engine__ = Engine()
__ce__ = ContinuousEvaluator()
        
def loadModelFromFile(fileName):
    global __engine__
    prj = coral.loadDocument(fileName)
    for elem in prj.contents():
        for part in elem.__getAllParts__():
            if part.oclIsKindOf(L.Constraint):
                __engine__.addConstraint(part)
            elif part.oclIsKindOf(L.ConstraintModel):
                __engine__.addConstraintModel(part)

def loadConstraintModel(CM):
    global __engine__
    if CM.oclIsKindOf(L.ConstraintModel):
        __engine__.addConstraintModel(CM)
        
def activateCM(CM):
    """Activates checking for ConstraintModel"""
    global __engine__

    # Clear the object cache for all constraints in CM
    for constraint in CM.constraint:
        if __engine__.constraintObjects.has_key(constraint):
            __engine__.constraintObjects.pop(constraint)
    
    reEvaluateAll(CM)
    
    __engine__.activate(CM)
    
def deActivateCM(CM):
    """Deactivates checking for ConstraintModel"""
    global __engine__
    __engine__.deActivate(CM)

def isActive(CM):
    """Returns true if ConstraintModel is being checked"""
    global __engine__
    return CM in __engine__.activeConstraintModels

def checkSingleConstraint(constraint, targetModel):
    """Checks sigle constraint against a specific target"""
    global __engine__
#    print constraint,
#    start=time.time()
    try:
        constraintObject=__engine__.getConstraintObject(constraint)
#        print constraint,
#        sys.stdout.flush()
        results = constraintObject.searchApply(targetModel)
#        print str(len(results))
    except Exception, exce:
        r=L.Result(message=str(exce),constraint=constraint,name=constraint.name)
        return [r]
#    end=time.time()
#    print end-start," s"
    return results

def onModelChanged(tr):
    if len(__engine__.activeConstraintModels)==0:
        return

    changedElements = coral.Set()

    tr=TM.getCurrentTransaction()

    for c in tr:
        if isinstance(c,coral.ElementChangeSlotCommand):
            s = c.getSlot()

            changedElement = s.getElement()
            offendingSlot=None
            if changedElement.oclIsKindOf(coral.lang.CORE01.AnyAbstractElement):
                offendingOppositeSlot=changedElement.__getSlotByOppositeName__(L.Result,"offending")
                
                for result in offendingOppositeSlot:
                    for offending in result.offending:
                        if offending == changedElement:
                            continue
                        changedElements.append(offending)
                        
                    if result.constraintmodel and isActive(result.constraintmodel):
                        result.__resetAttrs__()
                        
            if changedElement.oclIsKindOf(L.Result):
                if s.getProperty().getName() == "offending":
                    
                    if len(changedElement.offending) == 0:
                        
                        # This is NOT good
                        # The observers should not use begin- and end-ModifyCmd since they
                        # trigger the observers to run.
                        #TM.beginModifyCmd()
                        #try:
                        changedElement.__resetAttrs__()
                        #finally:
                        #    TM.endModifyCmd()
            
            elif not s.getElement() in __engine__.evaluateQueue:
                changedElements.append(s.getElement())

    if changedElements and not isEvaluating():
        CoralModeler.getInstance().addContinuousObserver(__ce__)
        
    for new_elem in changedElements:
        __engine__.evaluateQueue.append(new_elem)

#     else:
#         for elem in __engine__.evaluateQueue:
#             evaluate(elem)
#         __engine__.evaluateQueue=[]
#         __engine__.addBufferedResultsToModel()

def getConstraintsForType(t):
    constraints = []
    for constr in getConstraintByName("PythonConstraint"):
        kind_t=coral.AbstractElementDefinition_getAbstractElementDefinitionFromSMD(constr.kind)
        if kind_t == t:
            constraints.append(constr)
    return constraints
    
def removeResults(e, constr, cm):
    """Removes results from cm, created when checking constr on element e"""
    oldResults = []

    for result in cm.result:
        if e in result.offending:
            if constr == result.constraint:
                oldResults.append(result)

    for old_result in oldResults:
        old_result.__resetAttrs__()

def addResult(result, cm):
    """Appends a result to the corresponding Constraint Model"""
    new_offending = result.offending

    all_constraints=coral.Set()
    
    for constr in cm.constraint:
        all_constraints.unionWith(constr.__getAllParts__().select(lambda p: p.oclIsKindOf(coral.lang.CONSTRAINT10.Constraint)))

    if result.constraint:
        assert result.constraint in all_constraints


    for old_result in cm.result:
        if result.constraint == old_result.constraint:

            old_offending = old_result.offending

            if sub_set(new_offending, old_offending):
                return
            
            if sub_set(old_offending, new_offending):
                old_result.__resetAttrs__()

    cm.result.append(result)

def sub_set(subset, bigset):
    # The sets have to be sorted
    if len(bigset) < len(subset):
        return False

    for elem in subset:
        if not elem in bigset:
            return False

    return True
    
def reEvaluateAll(CM):
    """Checks for any existing conflicts"""
    if CM.oclIsKindOf(L.ConstraintModel):

        TM.beginModifyCmd()
        try:
            CM.result.resetSlot()

            if not CM.target:
                r=L.Result(name=CM.name, message="ConstraintModel has no target!")
                addResult(r, CM)
                deActivateCM(CM)
                return
        
            constraint_set=coral.Set()
            for element in CM.constraint:
                constraint_set.unionWith(element.__getAllParts__().select(lambda p: p.oclIsKindOf(L.Constraint)))

                
            for constr in constraint_set: 
            
                for res in checkSingleConstraint(constr, CM.target):
                    addResult(res, CM)

        finally:
            TM.endModifyCmd()

def evaluate(elem):
    """Evaluates the element against all the ConstraintModels that apply to some of the element's parents"""
    global __engine__

    if not elem.oclIsKindOf(coral.lang.CORE01.AnyAbstractElement):
        return
    
#    if elem.__type__().__language__() == coral.lang.XMIDI20:
#        return

    if elem.__type__().__language__() == L:
        return

#    print "evaluate",elem

    newResultsBuffer=[]
    results2remove=[]
    for cm in __engine__.activeConstraintModels:
        if not cm.target:
            r=L.Result(name=cm.name, message="ConstraintModel has no target!")
            newResultsBuffer.append(([r],cm))
            deActivateCM(cm)
            continue
        
        if elem.__isPart__(cm.target):
            constraints = __engine__.getConstraints(cm, elem)

            for constrObj in constraints:
                try:
                    results = constrObj.apply(elem)
                except Exception, exept:
                    r=L.Result(message=str(exept),name=constrObj.elem.name,constraint=constrObj.elem)
                    r.offending.append(elem)
                    newResultsBuffer.append(([r],cm))
                    continue
               
                results2remove.append( (elem,constrObj.elem,cm) )
                newResultsBuffer.append((results,cm))

    # If inside TM the evaluation never ends
#     TM.beginModifyCmd()
#     try:
#         for elem,elemObj,cm in results2remove:
#             removeResults(elem,elemObj,cm)
#     finally:
#         TM.endModifyCmd()
        
    for results,cm in newResultsBuffer:    
        if results:
            for res in results:
                __engine__.resultBuffer.append( (res,cm) )

TM.addObserver(onModelChanged)


# Less frequently used functions

def getConstraintsByName(name):
    global __engine__
    return __engine__.constraints[name]

def getConstraintsByType(type):
    global __engine__
    return __engine__.constraints[type.getName()]

# def printConstraints():
#     #TODO Fix this function if it is needed
#     global __engine__
#     for k, v in __engine__.constraints.iteritems():
#         print k
#         for elem in v:
#             print elem.name

# def loadTarget(fileName):
#     #TODO Fix this function if needed
#     global __engine__
#     prj = coral.loadDocument(fileName)
#     __engine__.addTargetProject(prj)

