"""The module defines two fundamental classes: Agent (FSM with messasging) 
and ScriptableAgent (an Agent extandable with scripts). Most objects in
the game are descendants of one of these classes. All objects with 
scripting capabilites are derived from Scriptable Agent.
"""

import fsm
import message

class Agent(fsm.FSM):
    """Agent is an FSM with message handling capabilities. In addition to 
    states and actions, Agent supports incoming and outcoming boxes of 
    messages and a set of message handlers.
    
    In the absence of incoming messages Agent works as an FSM. When Agent
    finds a command in its inBox, it runs a handler function (if any). 
    Message handlers can change FSM state. To send a command to an Agent 
    other objects call functions sendCommand or postCommand (named by win32 
    functions). Only Dispatcher(s) or Collective that have the Agent as a member 
    can send commands to it. Also Agent can post messages (reports) into 
    its outBox using sendReport function. Only Agent's owner (a Collective) 
    or Dispatchers, to which Agent reports to, can check outBox for reports.
    
    Such organization divides responsibilities between objects in the game
    and establishes communication flow: Collectives and Dispatchers send
    commands to Agents, Agents send reports back."""

    def __init__(self, name = ""):
        self.agent_debug = 0
        fsm.FSM.__init__(self,name)
        self.messageHandlers = {}
        self.inBox = []
        self.outBox = []

    def trace(self, args):
        if (self.agent_debug):
            print self.name+": "+str(args)

    def executeCommands(self):
        """Calls handler for each message in the incoming box.
        If the handler not found, the message is ignored."""
        for msg in self.inBox:
            if (msg.delay<=0) and (self.messageHandlers.has_key(msg.type)):
                self.messageHandlers[msg.type](msg)

    def update(self, timeInc = 1):
        """Handles all incoming messages and then updates FSM."""
        self.executeCommands()
        fsm.FSM.update(self, timeInc)
        return self.currState

    def clearInBox(self, timeInc = 1):
        """Removes messages with delay=0 from inBox. Timed messages
        are preserved for later porcessing."""
        timedMessages = []
        for msg in self.inBox:
            if (msg.delay > 0):
                timedMessages.append(msg)
                msg.delay -= timeInc
        self.inBox = timedMessages

    def clearOutBox(self, timeInc = 1):
        """Removes all messages with delay=0 from outBox. Timed messages
        are preserved for later porcessing."""
        timedMessages = []
        for msg in self.outBox:
            if (msg.delay > 0):
                timedMessages.append(msg)
                msg.delay -= timeInc
        self.outBox = timedMessages

    def postCommand(self, type, delay=0, paramsDict=None):
        """Places command into inBox."""
        msg = message.Message(type, delay, paramsDict)
        self.trace("appended command "+msg.type+" delay= "+str(msg.delay))
        self.inBox.append(msg)

    def sendCommand(self, type, delay=0, paramsDict=None):
        """Sends a command with immediate invocation of a handler,
        if the handler is available."""
        msg = message.Message(type, delay, paramsDict)
        self.trace("sending command "+msg.type+"delay="+str(msg.delay))
        if (msg.delay<=0) and (self.messageHandlers.has_key(msg.type)):
            self.messageHandlers[msg.type](msg)

    def sendReport(self, type, delay=0, paramsDict=None):
        """Places report message into outBox."""
        self.outBox.append(message.Message(type, delay, paramsDict))

class ScriptableAgent(Agent):
    """Extends hardcoded set of message handlers and actions with scripted
    ones, defined in script. Hardcoded functions are those implemented
    in the class' Python code. Scripted handlers and actions have higher
    priority and override same-name hardcoded ones."""

    def __init__(self, name="", script=None):
        Agent.__init__(self,name)
        self.setScript(script)

    def setScript(self,script):
        """Assigns self.script variable and also sets owner to the script."""
        self.script = script
        if (script):
            self.script.owner = self

    def executeCommandWithScript(self, msg):
        """Attempts to use scripted handler for the message.
        It returns 1 if succeeded (if handler found in script)."""
        if (self.script):
            return self.script.callMethod(msg.type,msg)
        return 0

    def setScriptedAction(self):
        """Sets scripted action (state update function). It returns 0 when
        fails (=no script is set)."""
        if (self.script):
            self.action = self.setScriptedAction
            return self.script.callMethod(self.currState)
        return 0

    def executeCommands(self):
        """For each message in the incoming box it calls handler - 'hardcoded' or
        scripted, if any. Scripted handlers have higher priority over original ones."""
        for msg in self.inBox:
            if (msg.delay<=0):
                #scripted methods have priority over default behavior
                if (self.executeCommandWithScript(msg)==0) and (self.messageHandlers.has_key(msg.type)):
                    self.messageHandlers[msg.type](msg)

    def sendCommand(self, type, delay=0, paramsDict=None):
        """This is similar to Actor's sendCommand but attempts finding scripted handler first."""
        msg = message.Message(type, delay, paramsDict)
        self.trace("sending command "+msg.type+"delay="+str(msg.delay))
        #scripted methods have priority over default behavior
        if (self.executeCommandWithScript(msg)==0) and (self.messageHandlers.has_key(msg.type)):
            self.messageHandlers[msg.type](msg)

    def setAction(self):
        """Attempts finding and setting scripted action for the current state.
        If it fails it searches for hardcoded action by calling inherited
        Agent.setAction function."""
        if (self.setScriptedAction()):
            return
        Agent.setAction(self)

#########################################################
# module test code
#########################################################

if (__name__=="__main__"):

        class TestAgent(Agent):
                def __init__(self):
                        Agent.__init__(self,"Test Agent")
                        self.actions["idle"] = self.Action_Idle
                        self.actions["active"] = self.Action_TimeCountdown
                        self.messageHandlers["wakeUp"] = self.Handler_WakeUp

                def Action_Idle(self, timeInc=1):
                        print "I am idle"

                def Action_TimeCountdown(self, timeInc=1):
                        print "self.timeInState = ", self.timeInState, " state= ", self.currState
                        if (self.timeInState>self.timeToStayAwake):
                                self.setState("idle")

                def Handler_WakeUp(self, msg):
                        self.setState("active")
                        self.timeToStayAwake = msg.dict["stay awake time"]

        ta = TestAgent()
        ta.agent_debug = 1
        ta.setState("idle")
        ta.update(1)      # here we call update and clearInBox manually...
        ta.clearInBox(1)  # ...but in the application they are called by Collective.
        ta.postCommand("wakeUp", delay=2, paramsDict={"stay awake time": 2})
        ta.update(1)
        ta.clearInBox(1)
        ta.update(1)
        ta.clearInBox(1)
        ta.update(1)
        ta.clearInBox(1)
        ta.update(1)
        ta.clearInBox(1)
        ta.update(1)
        ta.clearInBox(1)
        ta.update(1)
        ta.clearInBox(1)
        ta.update(1)
        ta.clearInBox(1)
        ta.update(1)
        ta.clearInBox(1)
        ta.update(1)