import logging
from orangescape.model.engine.engineconstants import WORKFLOW, AND_PROCESS, OR_PROCESS, REAL_OR_PROCESS, PI_CELLNAME, STATUS_CELLNAME, CONST,ACTION
from orangescape.model.engine.execution.ProcessUtil import getContextBranch, getRootBranch, getProcessNode, getProcessCell_PrefixName
from orangescape.model.engine.execution.ExecutionTree import ExecutionTree
from orangescape.model.server.util.metadatautil import getCellByName

from orangescape.model.engine.util.NumericUtil import is_a_number
import appconfig as config
from orangescape.model.server.PerfMonitor import startPerfMonitor,stopPerfMonitor

process_module_pkg = 'orangescape.model.engine.execution.ProcessEngine.%sProcessNode'
action_module_pkg = 'orangescape.model.engine.execution.actions.%sCommand.%sProcessNode'


class ProcessNode():

    def __init__(self, component, sheet, activityDefinition):
        self.sheet = sheet
        self.activityDefinition = activityDefinition
        self.component = component
        self.session = self.component.getSession()
        self.args = dict()

    def execute(self):
        logging.info('Executing %s (%s) for the sheet %s'%(self.activityDefinition.getNodeType(), self.activityDefinition.getSheetName(), self.sheet.getSheetId()))
        if config.DEBUGGER_ENABLED:
            pm = startPerfMonitor(self.sheet.getSheetMetadata().getSheetName(),'Command',self.activityDefinition.getNodeType(),sheetid=self.sheet.getSheetId(),inputSheet=self.sheet)
        self.resolve()
        if config.DEBUGGER_ENABLED and pm is not None:
            pm.inputMetrics['InputValues'] = str(self.args)

        self.start()
        if config.DEBUGGER_ENABLED:
            stopPerfMonitor(pm, self.sheet)
        return self.nextProcess()

    def start(self):
        self.enter()

    def update(self, params):
        updateParams = {}
        for opPort in self.activityDefinition.getOutputportMetadataList():
            if opPort.getCellMetadata():
                if params.has_key(opPort.getParamName()) and opPort.getCellMetadata():
                    updateParams[opPort.getCellMetadata().getCellName()] = params[opPort.getParamName()]
                else:
                    logging.warn('output params mismatching, %s not found in params for the cell %s or the value might be None'%(opPort.getParamName(), opPort.getCellMetadata().getCellName()))
            else:
                logging.warn('LHS Ouput Params value on Update might be None')
        if updateParams:
            if config.env != config.SL and logging.getLogger().level == logging.DEBUG:
                logging.debug('Updating values %s for the model %s(%s) for %s (%s)'%(updateParams, self.sheet.getSheetMetadata().getSheetName(), self.sheet.getSheetId(), self.activityDefinition.getNodeType(), self.activityDefinition.getSheetName()))
            self.component.update(self.sheet, updateParams)

    def enter(self):
        pass

    def getSheetMetadata(self, sheetName):
        return self.session.getApplication().getSheetMetadata(sheetName)

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

    def resolve(self):
        ipPortList = self.activityDefinition.getInputportMetadataList()
        for ipPort in ipPortList:
            value = None
            if ipPort.getParamName()=='Search-Criteria':
                if ipPort.getFormulaList():
                    logging.info('Resolving search-criteria')
                    et = ExecutionTree(self.session)
                    rootNode = ipPort.getFormulaList()[0].getNodeList()[0]
                    if rootNode.getNodeValue()=='CRITERIA':
                        rootNode = rootNode.getNodeList()[0]
                    criteriaNode = et.getCriteriaNode(rootNode, self.sheet)
                    criteriaNode.formula = ipPort.getFormulaList()[0]
                    value = criteriaNode
                else:
                    logging.warn('Search-criteria dont have formula')

            elif ipPort.getCellMetadata():
                value = ipPort.getCellMetadata()
            elif ipPort.getBranch():
                value = ipPort.getBranch().getSheetId()
            elif ipPort.getActivityDefinitionRef():
                value = ipPort.getActivityDefinitionRef().getSheetId()
            elif ipPort.containsFormula():
                if ipPort.getParamName()=='Child-Search-Criteria':
                    value = ipPort.getFormulaList()[0].getNodeList()[0]
                else:
                    value = self.executeFormula(ipPort.getFormulaList(), self.sheet)
            elif ipPort.getCellMetadata():
                value = self.sheet.get(ipPort.getCellMetadata())
            else:
                if ipPort.isStr():
                    value = ipPort.getStringValue()
                else:
                    value = ipPort.getFloatValue()
            self.args[ipPort.getParamName()] = value

    def __end_AND_branch__(self, branch):

        rootBranch = getRootBranch(self.activityDefinition)
        workflow_end = rootBranch.getActivityDefinitionList()[-1]

        prefix_name = branch.getSheetId()+'_'
        assignment = self.__assign_activity__(prefix_name, workflow_end)
        assignment['InstanceAssignedTo'] = None
        self.__create_process_instance__(branch, assignment, prefix_name, workflow_end)

        all_branches_finished = True
        for peer_branch in branch.getActivityDefinition().getBranchList():
            if self.sheet.getValue('%s_Status'%(peer_branch.getSheetId())) != workflow_end.getSheetId():
                """Still some more branches needs to complete"""
                all_branches_finished = False
                break

        if all_branches_finished:
            nextActivity = self.__getNextActivity__(self.sheet, branch.getActivityDefinition().getBranch(), branch.getActivityDefinition())
            if nextActivity:
                activityId = self.__getProcessNode__(nextActivity).execute()
                while activityId:
                    activity = self.sheet.getSheetMetadata().getActivityDefinitionById(activityId)
                    processNode = getProcessNode(self.component, activity, self.sheet)
                    activityId = processNode.execute()

    def __end_branch_tree__(self, workflow_end, branch):
        if branch.getNodeType()==AND_PROCESS or branch.getNodeType() == REAL_OR_PROCESS:
            prefix_name = branch.getSheetId()+'_'

            if self.sheet.getValue(prefix_name+STATUS_CELLNAME) is not None and self.sheet.getValue(prefix_name+STATUS_CELLNAME) != workflow_end.getSheetId():
                assignment = self.__assign_activity__(prefix_name, workflow_end)
                assignment['InstanceAssignedTo'] = None
                self.__create_process_instance__(branch, assignment, prefix_name, workflow_end)

                for act in branch.getActivityDefinitionList():
                    for br in act.getBranchList():
                        self.__end_branch_tree__(workflow_end, br)
        else:
            for act in branch.getActivityDefinitionList():
                for br in act.getBranchList():
                    self.__end_branch_tree__(workflow_end, br)

    def __end_REAL_OR_branch__(self, branch):
        rootBranch = getRootBranch(self.activityDefinition)
        workflow_end = rootBranch.getActivityDefinitionList()[-1]

        for peer_branch in branch.getActivityDefinition().getBranchList():
            self.__end_branch_tree__(workflow_end, peer_branch)

        nextActivity = self.__getNextActivity__(self.sheet, branch.getActivityDefinition().getBranch(), branch.getActivityDefinition())
        if nextActivity:
            activityId = self.__getProcessNode__(nextActivity).execute()
            while activityId:
                activity = self.sheet.getSheetMetadata().getActivityDefinitionById(activityId)
                processNode = getProcessNode(self.component, activity, self.sheet)
                activityId = processNode.execute()

    def nextProcess(self):
        nextActivity = self.__getNextActivity__(self.sheet, self.activityDefinition.getBranch(), self.activityDefinition)
        if nextActivity:
            nextProcessNode = self.__getProcessNode__(nextActivity)
            return nextProcessNode.execute()

    def __getNextActivity__(self, sheet, branch, currentActivityDef):
        if currentActivityDef.getSequenceNo()<len(branch.getActivityDefinitionList()):
            return branch.getActivityDefinitionList()[int(currentActivityDef.getSequenceNo())]
        else:
            """Reached the last activity for this branch"""
            if branch.getNodeType()==OR_PROCESS:
                """Need to go to the next process of OR activity"""
                msg = "Need to go to the next process of OR activity %s(%s) in "%(branch.getActivityDefinition().getSheetName(), branch.getActivityDefinition().getSheetId())
                logging.info(msg)
                parentActivity = branch.getActivityDefinition()
                return self.__getNextActivity__(sheet, parentActivity.getBranch(), parentActivity)
            elif branch.getNodeType()==AND_PROCESS:
                self.__end_AND_branch__(branch)
            elif branch.getNodeType()==REAL_OR_PROCESS:
                msg = 'One of the OR branch %s is finished. Moving to the next activity of OR for the sheet %s'%(branch.getSheetName(), self.sheet.getSheetId())
                logging.info(msg)
                self.__end_REAL_OR_branch__(branch)
            else:
                return None

    def executeFormula(self, formulaList, sheet):
        et = ExecutionTree(self.session)
        return et.executeFormula(formulaList[0], sheet)

    def getNextActivityByResult(self, activityDef, result):
        branchList = activityDef.getBranchList()
        resultBranch = None

        if not result:
            resultBranch = branchList[1]
        elif not is_a_number(result):
            for branch in branchList:
                if branch.getSheetName()==str(result):
                    resultBranch = branch
            if not resultBranch:
                msg = 'Activity %s not found in any Branches for the sheet %s based on the formula in the activity %s(%s)'%(result, self.sheet.getSheetId(), self.activityDefinition.getSheetName(), self.activityDefinition.getSheetId())
                raise Exception(msg)
        else:
            result = float(result)
            if result:
                """If True get the first activity"""
                resultBranch = branchList[0]
            else:
                resultBranch = branchList[1]
                """If False get the second activity"""
        if len(resultBranch.getActivityDefinitionList())>0:
            return resultBranch.getActivityDefinitionList()[0]
        logging.info('IF: Branch %s based on the result %s doesn\'t contains any activity/command exiting from IF'%(resultBranch.getSheetName(), result))
        return self.__getNextActivity__(self.sheet, resultBranch.getActivityDefinition().getBranch(), resultBranch.getActivityDefinition())

    def __getProcessNode__(self, activitydef):
        return getProcessNode(self.component, activitydef, self.sheet)

    def __assign_activity__(self, prefix_name, activityDefinition):
        assignment = {'InstanceAssignedFor':activityDefinition.getAssignedFor()}
        assignment['InstanceAssignedTo'] = None
        if self.sheet.getSheetMetadata().getStartActivity().getSheetId()==activityDefinition.getSheetId():
            userIns = self.session.get(self.sheet.getValue('CreatedBy'), self.getSheetMetadata('User'))
            assignment['InstanceAssignedTo'] = userIns.getValue('UserId').split('@')[0]
        else:
            if activityDefinition.getAssignedFor()=='User':
                assignedTo = None
                if activityDefinition.containsFormula():
                    assignedTo = self.executeFormula(activityDefinition.getFormulaList(), self.sheet)
                """Direct user assignment"""
                if assignedTo:
                    userMetadata = self.session.getApplication().getSheetMetadata('User')
                    userIns = self.session.getIfExists(assignedTo, userMetadata)
                    if userIns:
                        assignment['InstanceAssignedTo'] = userIns.getValue('UserId').split('@')[0]
                    else:
                        if assignedTo.find('@')>=0:
                            raise Exception("The user assignment shouldn't contain the domain name.")
                        assignment['InstanceAssignedTo'] = assignedTo.lower()
                else:
                    assignment['InstanceAssignedTo'] = None
            else:
                """Here is the actual group assignment"""
                """If Activity is not sync take from ActivityDefinition metadata"""
                assignedTo = activityDefinition.getAssignedTo()
                logging.info('Assigning activity %s for the instance %s of the model %s to a group %s'%(activityDefinition.getSheetName(), self.sheet.getSheetId(), self.sheet.getSheetMetadata().getSheetName(), assignedTo))
                if not assignedTo:
                    assignment['InstanceAssignedTo'] = None
                else:
#                    appRoleInstance = self.session.getBySheetName(assignedTo, self.session.getApplication().getSheetMetadata('AppRole'))
#                    if not appRoleInstance:
#                        raise Exception("Could not find the app role %s. The assigned approle is not created."%assignedTo)
                    assignment['InstanceAssignedTo'] = assignedTo

        if activityDefinition.containsActivityTenantFormula():
                assignment['Tenant'] = self.executeFormula(activityDefinition.getActivityTenantFormulaList(), self.sheet)
        elif activityDefinition.getActivityTenant():
            assignment['Tenant'] = 'Company_'+activityDefinition.getActivityTenant()
        else:
            userIns = self.session.get(self.sheet.getValue('CreatedBy'), self.getSheetMetadata('User'))
            assignment['Tenant'] = userIns.getValue('Company')
        uParams = {prefix_name+'Status':activityDefinition.getSheetId(), prefix_name+'AssignedTo':assignment['InstanceAssignedTo']}
        if self.sheet.getSheetMetadata().getPublicCellMetadataDict().has_key(prefix_name+'Tenant'):
            uParams[prefix_name+'Tenant'] = assignment['Tenant']
        self.component.update(self.sheet, uParams)
        return assignment

    def __change_state__(self, activityDefinition):
        context_branch = getContextBranch(activityDefinition)
        prefix_name = getProcessCell_PrefixName(activityDefinition)
        assignment = self.__assign_activity__(prefix_name, activityDefinition)
        if self.sheet.getSheetMetadata().isMonitorEnabled():
            pi = self.__create_process_instance__(context_branch, assignment, prefix_name, activityDefinition)

        """After changing the state if the activity contains any action to be invoked need to invoke that"""
        if self.args.has_key('Action-Name'):
            pi_context_cell = self.args.get('Context-CellName')
            if pi_context_cell and self.sheet.getSheetMetadata().isMonitorEnabled():
                self.component.update(self.sheet, {pi_context_cell.getCellName(): pi.getSheetId()})

            actionId = self.args['Action-Name']
            metadata = self.sheet.getSheetMetadata()
            action = metadata.getBranchById(actionId)

            logging.info('Calling action %s in the sheet %s'%(action.getSheetName(), metadata.getSheetName()))
            self.component.executeAction(self.sheet, action)

            if pi_context_cell and self.sheet.getSheetMetadata().isMonitorEnabled():
                self.component.update(self.sheet, {pi_context_cell.getCellName(): None})

    def __create_process_instance__(self, context_branch, assignment, prefix_name, activityDefinition):
        processInstanceMetadata = self.session.getApplication().getSheetMetadata('ProcessInstance')
        processAuditMetadata = self.session.getApplication().getSheetMetadata('ProcessAudit')
        sheetMetadata = self.sheet.getSheetMetadata()

        activityInstance = self.session.getIfExists(activityDefinition.getSheetId(), self.getSheetMetadata('Activity'))
        if activityInstance:
            assignment['InstanceActivity'] = activityDefinition.getSheetId()
        else:
            msg = 'Activity Instance not exists for the model %s. InstanceActivity set to be None in the ProcessInstance while assigning the activity %s to the instance %s'%(sheetMetadata.getSheetName(), activityDefinition.getSheetName(), self.sheet.getSheetId())
            logging.warn(msg)
        assignment['SheetName'] = self.sheet.getSheetName()

        pi_params = {}
        if self.args.has_key('SLA-Action'):
            pi_params['NodeType'] = activityDefinition.getNodeType()
            pi_params['Iterations'] = self.args.get('SLA-Iterations')
            pi_params['SLA_Time'] = self.args.get('SLA-Time') * 60.0
        else:
            if processInstanceMetadata.getPublicCellMetadataDict().has_key('SLA_Time'):
                pi_params['Iterations'] = None
                pi_params['SLA_Time'] = None
        pi_params.update(assignment)

        if self.sheet.getValue(prefix_name+PI_CELLNAME) is None:
            shortCutCell = getCellByName(sheetMetadata, 'shortcut')
            if shortCutCell and self.sheet.get(shortCutCell):
                metadataName = self.sheet.get(shortCutCell)
            else:
                metadataName = sheetMetadata.getPublicName()
            pi_params['InstanceId'] = self.sheet.getSheetId()
            metadataInstance = self.session.getIfExists('M_'+sheetMetadata.getSheetId(), self.getSheetMetadata('Metadata'))
            if metadataInstance:
                pi_params['InstanceMetadata'] = metadataInstance.getSheetId()
            else:
                msg = 'Metadata Instance not exists for the model %s. InstanceMetadata set to be None in the ProcessInstance while assigning the activity %s to the instance %s'%(sheetMetadata.getSheetName(), activityDefinition.getSheetName(), self.sheet.getSheetId())
                logging.warn(msg)
            if context_branch.getNodeType()==AND_PROCESS or context_branch.getNodeType()==REAL_OR_PROCESS:
                if processInstanceMetadata.getPublicCellMetadataDict().has_key('ParentProcess'):
                    parentActivity = context_branch.getActivityDefinition()
                    parentProcessId = self.sheet.getValue(getProcessCell_PrefixName(parentActivity)+PI_CELLNAME)
                    parentProcess = self.session.get(parentProcessId, processInstanceMetadata)
                    processInstance = self.component.create(parentProcess, processInstanceMetadata.getPublicCellMetadata('ParentProcess'), pi_params)
                else:
                    logging.warn('Reference cell named "ParentProcess" not found in ProcessInstance. Create a self-reference cell in ProcessInstance named "ParentProcess"')
                    processInstance = self.component.new(processInstanceMetadata, pi_params)
            else:
                processInstance = self.component.new(processInstanceMetadata, pi_params)
            self.component.update(self.sheet, {prefix_name+PI_CELLNAME: processInstance.getSheetId()})
        else:
            processInstance = self.session.get(self.sheet.getValue(prefix_name+PI_CELLNAME), processInstanceMetadata)
            self.component.update(processInstance, pi_params)

        """Creating process Audit"""
        if processAuditMetadata.getPublicCellMetadataDict().has_key('ProcessInstanceRef'):
            paInstance = self.component.create(processInstance, processAuditMetadata.getPublicCellMetadata('ProcessInstanceRef'), assignment)
        else:
            logging.warn('ProcessInstanceRef cell not found in ProcessAudit please migrate the model using update system param and re-deploy')
            paInstance = self.component.create(processInstance, processAuditMetadata.getPublicCellMetadata('ProcessInstance'), assignment)
        if processInstanceMetadata.getPublicCellMetadataDict().has_key('PendingProcess'):
            if processInstance.getValue('PendingProcess'):
                prevProcessAudit = self.session.get(processInstance.getValue('PendingProcess'), processAuditMetadata)
                self.component.update(prevProcessAudit, {})

            self.component.update(processInstance, {'PendingProcess':paInstance.getSheetId()})
            self.component.update(paInstance, {'ModifiedBy':None})

        return processInstance

class EventNode(ProcessNode):

    def execute(self):
        self.resolve()
        self.start()

    def submit(self):
        if self.allowed():
            self.exit()

    def allowed(self):
        raise NotImplementedError

    def exit(self):
        raise NotImplementedError

class WorkflowProcessNode(EventNode):

    def enter(self):
        self.__change_state__(self.activityDefinition)

    def allowed(self):
        return True

    def exit(self):
        activityId = None
        if self.activityDefinition:
            activityId = self.nextProcess()
        else:
            startActivity = self.sheet.getSheetMetadata().getStartActivity()
            if startActivity:
                activityId = startActivity.getSheetId()
            else:
                logging.warn('Workflow not enabled for this model')
        while activityId:
            activity = self.sheet.getSheetMetadata().getActivityDefinitionById(activityId)
            processNode = getProcessNode(self.component, activity, self.sheet)
            activityId = processNode.execute()

class ORProcessNode(ProcessNode):
    def nextProcess(self):
        formulaList = self.activityDefinition.getFormulaList()
        if len(formulaList)>0:
            result = self.executeFormula(formulaList, self.sheet)
        else:
            if self.activityDefinition.getProcessType()==WORKFLOW:
                prevAct = ''
                if self.activityDefinition.getSequenceNo()-1>0:
                    previousActivity = self.activityDefinition.getBranch().getActivityDefinitionList()[int(self.activityDefinition.getSequenceNo())-2]
                    prevAct = '(next to %s Activity)'%(previousActivity.getSheetName())
                errMsg = "The IF %s %s in the model %s doesn't contains formula to redirect the %s. Please specify the formula."%(self.activityDefinition.getSheetName(), prevAct, self.sheet.getSheetMetadata().getSheetName(), self.activityDefinition.getProcessType())
            else:
                rootBranch = getRootBranch(self.activityDefinition)
                errMsg = "The IF %s in the action %s in the model %s doesn't contains formula to redirect the %s. Please specify the formula."%(self.activityDefinition.getSheetName(), rootBranch.getSheetName(), self.sheet.getSheetMetadata().getSheetName(), self.activityDefinition.getProcessType())
            logging.warn(errMsg)
            raise Exception(errMsg)
            result = 0.0
        nextActivity = self.getNextActivityByResult(self.activityDefinition, result)

        if nextActivity:
            nextProcessNode = self.__getProcessNode__(nextActivity)
            return nextProcessNode.execute()

class REAL_ORProcessNode(ProcessNode):
    def enter(self):
        if self.activityDefinition.getProcessType()==WORKFLOW:
            self.__change_state__(self.activityDefinition)

    def nextProcess(self):
        """Check and execute the REAL_OR branches"""
        enterIntoREAL_OR = True
        for branch in self.activityDefinition.getBranchList():
            if branch.containsFormula():
                result = self.executeFormula(branch.getFormulaList(), self.sheet)
                if not result:
                    enterIntoREAL_OR = False
        if enterIntoREAL_OR:
            for branch in self.activityDefinition.getBranchList():
                activityList = branch.getActivityDefinitionList()
                if len(activityList)>0:
                    activityId = self.__getProcessNode__(activityList[0]).execute()
                    while activityId:
                        activity = self.sheet.getSheetMetadata().getActivityDefinitionById(activityId)
                        processNode = getProcessNode(self.component, activity, self.sheet)
                        activityId = processNode.execute()
        else:
            if len(self.activityDefinition.getBranchList())>0:
                logging.info('One of the Branch failed. So exit from OR activity')
                self.__end_REAL_OR_branch__(self.activityDefinition.getBranchList()[0])
            else:
                logging.warn('OR Activity %s(%s) does not contain any branches'%(self.activityDefinition.getSheetName(), self.activityDefinition.getSheetId()))

class ANDProcessNode(ProcessNode):
    def enter(self):
        if self.activityDefinition.getProcessType()==WORKFLOW:
            self.__change_state__(self.activityDefinition)

    def nextProcess(self):
        """Check and execute the parallel(AND) branches"""

        """Set None for the all xxx_Status other wise when it execute for next time (through GoTo)
        it'll treat that all branches are finished hence will execute"""
        branchStatusParams = {}
        for branch in self.activityDefinition.getBranchList():
            branchStatusParams[branch.getSheetId()+'_Status'] = None
        self.component.update(self.sheet, branchStatusParams)
        """************************************************************************************"""

        for branch in self.activityDefinition.getBranchList():
            activityList = branch.getActivityDefinitionList()
            if branch.containsFormula():
                result = self.executeFormula(branch.getFormulaList(), self.sheet)
                if result:
                    if len(activityList)>0:
                        activityId = self.__getProcessNode__(activityList[0]).execute()
                        while activityId:
                            activity = self.sheet.getSheetMetadata().getActivityDefinitionById(activityId)
                            processNode = getProcessNode(self.component, activity, self.sheet)
                            activityId = processNode.execute()
                    else:
                        msg = "AND Branch %s of the sheet %s doesn't contain any activity. So branch is set to be end"%(branch.getSheetName(), self.sheet.getSheetMetadata().getSheetName())
                        logging.error(msg)
                        self.__end_AND_branch__(branch)
                else:
                    """Set this branch status as end"""
                    self.__end_AND_branch__(branch)
            else:
                activityList = branch.getActivityDefinitionList()
                if len(activityList)>0:
                    activityId = self.__getProcessNode__(activityList[0]).execute()
                    while activityId:
                        activity = self.sheet.getSheetMetadata().getActivityDefinitionById(activityId)
                        processNode = getProcessNode(self.component, activity, self.sheet)
                        activityId = processNode.execute()
                else:
                    msg = "AND Branch %s of the sheet %s doesn't contain any activity. So branch is set to be end"%(branch.getSheetName(), self.sheet.getSheetMetadata().getSheetName())
                    logging.error(msg)
                    self.__end_AND_branch__(branch)

class GOTOProcessNode(ProcessNode):
    def nextProcess(self):
        return self.activityDefinition.getGoTo().getSheetId()
