#!/usr/bin/env python

# Copyright (c) 2000-2001 LOGILAB S.A. (Paris, FRANCE).
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place - Suite 330, Boston, MA  02111-1307
# USA.

"""Engine is the Narval interpreter"""

__revision__ = "$Id: Engine.py,v 1.102 2002/02/25 15:45:25 alf Exp $"

import time, string, os, threading, Queue

import bibal
from narval.lib import AL_NS, true, false, expand_vars, get_encoding, NO_NS
from narval.NarvalRC import NarvalRC
from narval.services.EventScheduler import EventScheduler
from narval.services.ComService import ComService
from narval.core.ALAbstraction import ALMFImplementation, ALEFImplementation
from narval.core.ChangeListeners import MemoryChangeListenerInterface,\
     StepChangeListenerInterface,TransitionChangeListenerInterface,\
     PlanChangeListenerInterface
from narval.communication import RPCFactory
from narval.protocol_handlers import load_protocolhandlers


# EngineThread #################################################################

class EngineThread(threading.Thread) :
    """ EngineThread """

    Engine = None

    def _Thread__stop(self) :
        self.Engine.end_of_thread()
        threading.Thread._Thread__stop(self)
        

class ThreadMgmtInterface :
    """
    Interface that defines the methods used to create and end EngineThreads
    """

    def create_thread(self,*args,**kargs) :
        raise Exception("interface method not implemented")

    def end_of_thread(self) :
        raise Exception("interface method not implemented")


# Narval Interfaces ############################################################

class MessageInterface :
    """
    Interface that defines Engine methods used for inter-Agent communication
    """

    def postMessage(self,messageXml) :
        raise Exception("interface method not implemented")
    
class DebugInterface :
    """ Interface that defines Engine methods used for debug """

    def debugSuspend(self) :
        raise Exception("interface method not implemented")
        
    def debugContinue(self) :
        raise Exception("interface method not implemented")

    def debugStepOneStep(self) :
        raise Exception("interface method not implemented")

class InteractionInterface :
    """Interface that defines Engine methods used by (Graph|CmdLine)UI"""

    def getMemory(self) :
        raise Exception("interface method not implemented")

    def getElement(self,elementId) :
        raise Exception("interface method not implemented")

    def addElement(self,elementXML) :
        raise Exception("interface method not implemented")

    def removeElement(self,elementId) :
        raise Exception("interface method not implemented")

    def replaceElement(self, elementId, newElementXml) :
        raise Exception("interface method not implemented")

    def startPlan(self,recipeName) :
        raise Exception("interface method not implemented")

    def saveRecipes(self) :
        raise Exception("interface method not implemented")

    def saveTransforms(self) :
        raise Exception("interface method not implemented")

    def shutdown(self):
        raise Exception("interface method not implemented")

    def getElementsList(self):
        raise Exception("interface method not implemented")

    def getElementList(self,elementId):
        raise Exception("interface method not implemented")



class HandlerManagementInterface:
    """ This class defines methods for the ProtocolHandler"""
    
    def instantiateHandlerRecipe(self, recipe_name, arguments_node, protocol):
        """
            Adds the childnodes of arguments_node to the memory after having 
            checked them against the outputs declared in the handler's 
            prototype, and instantiates a plan from the recipe.
            CAUTION : we should add use="yes" in input/match, where 
            arguments_node is inserted
        """ 
        raise Exception("interface method for HandlerManagement not implemented")
    
    def addElementFromHandlerToMemory(arguments_node, protocol):
        """ Add element to Narval Memory after having checked them against
            prototype given by protocolHandler
        """
        raise Exception("interface method for HandlerManagement not implemented")
         
    def controlHandlerActivation(self, element, callback, pname=None):
        """ Activate the right handler when an element corresponding to the
            right handler appears in Memory
            - *callback* is activate or deactivate
            - *pname* is the name of the protocol (jabber/http)
            - *element* is for instance <jabber-server>
        """
        raise Exception("interface method for HandlerManagement not implemented")

    def _findHandler(self, element, type, protocol_name):
        """ Find the handler to which element corresponds among the available
            protocols (and their prototypes)
            - *element* is an xml_string
            - *type* is one of input/output/activator
            - *protocol_name* is the name of the protocol associated with the
            type
        """
        raise Exception("interface method for HandlerManagement not implemented")

    def getPrototypeHelper(self, ph):
        """ just call getPrototype, which Handler is *ph*"""
        raise Exception("interface method for HandlerManagement not implemented")

class EngineRPCHandler:

    Narval = None
    ComService = None
    NAME = 'EngineEventDispatcher'
    EXPORTED_METHODS = {
        # listeners
        'addRemotePlanListener':     'ComService',
        'addRemoteMemoryListener':   'ComService',
        'removeRemotePlanListener':  'ComService',
        'removeRemoteMemoryListener':'ComService',
        'addRemoteEngineListener':   'ComService',
        'removeRemoteEngineListener':'ComService', 
        # memory interface
        'getMemory':      'Narval',
        'getElement':     'Narval',
        'addElement':     'Narval',
        'removeElement':  'Narval',
        'replaceElement': 'Narval',
        'startPlan':      'Narval',
        'getElementList': 'Narval',
        'getElementsList':'Narval',
        # messaging
        'postMessage':   'Narval',
        # debug
        'debugStepOneStep':'Narval',
        'debugSuspend':    'Narval',
        'debugContinue':   'Narval',
        # remote control
        'saveRecipes':   'Narval',
        'saveTransforms':'Narval',
        'shutdown':      'Narval',
        # defined in NEP
#        'addElementFromHandlerToMemory' : 'Narval',
        'instantiateHandlerRecipe': 'Narval'
        }


# Narval #######################################################################
class Narval(MemoryChangeListenerInterface, StepChangeListenerInterface,
             TransitionChangeListenerInterface, PlanChangeListenerInterface,
             ThreadMgmtInterface, MessageInterface, DebugInterface,
             InteractionInterface, HandlerManagementInterface) :
    """
    Narval is your pal ! 

    Terminology:
    * memory: place where Narval stores all it knows of.
    * elements: pieces of information stored in memory
    * recipe: a recipe is an element that describes a sequence of steps,
      separated by transitions. A recipe does nothing. It is knowledge.
    * plan: a plan is an element. A plan is an instance of a recipe. A plan
      actually does something.
    * step: A step can point to an action, a transform or a recipe.
    * transition: a transition links steps in a recipe. it can be fired iff
      the associated condition is true.
    
    The clever part of Narval is supposed to be run_step() and eval_transition()
    because of the way they build the list of arguments to pass to the actions
    and deal with the output.

    Other advantages of Narval are supposed to be:
    * its reflexivity: it can reason about what it is doing, as what it is
      doing are running plans and plans are elements in memory. it can reason
      about what it knows how to do, as recipes are elements in memory. it can
      reason about what it can do as each action is defined as a transformation
      (set of inputs and outputs).
    * its possibilities for machine learning. because everything is recorded,
      and because of the above flexibility, lots of possibilities are opened for
      automatic learning.
    """

    # Init #####################################################################    
    def __init__(self, rcfile):

        # alive and for long
        self.alive = true        
        self.quit_when_done = false
        self.save_mem_on_quit = None
        # event queue
        self.event_queue = Queue.Queue(0)
        # thread management
        self.thread_queue = Queue.Queue(0)
        self.max_threads = 10
        EngineThread.Engine = self
        # debug
        self.step_by_step = false
        self.step_by_step_semaphore = threading.Semaphore(0)
        # create memory document and elements factory
        self.memory = ALMFImplementation.create_memory(self)
        # create scheduler
        self.eventScheduler = EventScheduler(self.post_event)
        # services
        self.rpcServer = {}
        self.comService = None
        # protocol handlers: { key_protocol: ( class_protocol, prototype ) }
        self.protocols = {}
        self.handlers = {}
        # user config
        self.rc = NarvalRC(rcfile)
        # debug mode
        self.debug = 0
        # useful structures
        self.prototypes = {}
        
    # Services #################################################################

    def _start_rpc(self,type, port) :
        if not self.rpcServer.has_key(type):
            try:
                self._start_comService()
                EngineRPCHandler.Narval = self
                EngineRPCHandler.ComService = self.comService
                self.rpcServer[type] = RPCFactory.create_server(port, EngineRPCHandler,
                                                                type, self.debug)
                self.rpcServer[type].start()
            except:
                import traceback
                traceback.print_exc()

    def _stop_rpc(self) :
        for type in self.rpcServer.keys() :
            self.rpcServer[type].stop()

    def _stop_handlers(self) :
        for handler in self.handlers.values() :
            print 'stopping',handler
            handler.stop()

    def _start_comService(self) :
        if not self.comService :
            self.comService = ComService()
            self.comService.start()

    def _stop_comService(self) :
        if self.comService :
            self.comService.stop()

    # (Un)Load stuff in memory ################################################
    # check load date for action/recipe/transform
    def check_date(self, obj):
        assert obj.TYPE in ('transform', 'action', 'recipe')
        if hasattr(obj, 'load_date'):
            import os
            from narval import config
            group, name = obj.get_identity()
            if obj.TYPE == 'transform':
                path = os.path.join(config.get_home(),
                                    "transforms/%s/%s.xslt" % (group, name))
            elif obj.TYPE == 'action':
                path = os.path.join(config.get_home(), "modules/%s.py"%group)
            elif obj.TYPE == 'recipe':
                path = os.path.join(config.get_home(), "recipes/%s.xml"%group)
            last_modif = os.stat(path)[8]
            if last_modif > int(obj.load_date):
                if obj.TYPE == 'transform':
                    callback = bibal.reload_transform
                    e_l = [obj]
                elif obj.TYPE == 'action':
                    callback = bibal.reload_module
                    e_l = [e for e in self.memory.get_actions() if e.group == group]
                elif obj.TYPE == 'recipe':
                    callback = bibal.reload_recipe
                    e_l = [e for e in self.memory.get_recipes() if e.group == group]
                n_e_l = {}
                def add(x, n_e_l=n_e_l):
                    n_e_l[x.name]=x
                try:
                    obj2 = callback(ALEFImplementation, add,
                                    group, name)
                    for o in e_l:
                        if n_e_l[o.name]:
                            self.memory.replace_element(o, n_e_l[o.name])
                            del n_e_l[o.name]
                        else:
                            self.memory.remove_element(o)
                    for o in n_e_l.values():
                        self.memory.add_element(o)
                    obj = obj2
                except:
                    import traceback
                    traceback.print_exc()
        return obj

    def _load_recipes(self) :
        bibal.load_recipes(ALEFImplementation, self.memory.add_element)
        self._checkpoint_memory()

    def _save_recipes(self) :
        errors = bibal.save_recipes(self.memory)
        if errors:
            self._process_file_saving_errors(errors)
            
    def _load_modules(self) :
        bibal.load_modules(ALEFImplementation, self.memory.add_element)
        self._checkpoint_memory()
    
    def _load_protocolhandlers(self) :
        load_protocolhandlers(self.protocols, self.getPrototypeHelper)
        for k, p in self.protocols.items():
            self.handlers[k] = p[0](self)
            

    def _load_transforms(self) :
        bibal.load_transforms(ALEFImplementation,
                              self.memory.add_element)
        self._checkpoint_memory()

    def _save_transforms(self) :
        errors = bibal.save_transforms(self.memory)
        if errors:
            self._process_file_saving_errors(errors)
    
    def _load_memory(self,path=None) :
        bibal.load_memory(ALEFImplementation,
                          self.memory.add_element, path)
        self._checkpoint_memory()

    def _save_memory(self, path) :
        path = expand_vars(path)
        f = open(path,'w')
        f.write(self.memory.as_xml(get_encoding()))
        f.close()
        
    def _checkpoint_memory(self):
        pass

    def _commit_memory(self,get_mem = 1):
        pass

    def _process_file_saving_errors(self, errors):
        err = '''<al:error xmlns:al="%s">
<message>
The following files could not be saved. Please check that you have write access.
</message>''' % AL_NS
        for e in errors:
            err = '%s <filename>%s</filename>' % (err, e)
        err = '%s</al:error>' % err
        self.memory.add_element(ALEFImplementation.element_from_string(err))

    # Event Queue & Schedule ###################################################

    def post_event(self, event) :
        self._fire_event('engine','postQueueEvent', str(id(event)), str(event))
        self.event_queue.put(event)

    def _pop_event(self) :
        ev = self.event_queue.get()
        self._fire_event('engine','popQueueEvent', str(id(ev)))
        return ev
        
    def schedule_event(self,event,when=0,period=0,absoluteDate=0):
        ev = (event,when,period,absoluteDate)
        self._fire_event('engine','postSchedulerEvent', str(id(ev)), str(ev))
        self.eventScheduler.schedule_event(ev)

    # Run, quit and Process ####################################################

    def run(self) :
        """Mainloop. Execute plans endlessly unless a debugger says 'wait'.
        """
        print '\n"Hello, I\'m Narval your pal !"'
        self.eventScheduler.start()
        print "STARTING Engine.run"
        while self.alive :
            # done ?
            if self.quit_when_done and self.event_queue.empty() \
               and not self.memory.are_active_plans() :
                self._quit()
                continue

            # clear lists
            self.ready_steps = []
            self.ready_plans = []

            # debug
            if self.step_by_step :
                self.step_by_step_semaphore.acquire()

            # wait for event
            event = self._pop_event()
            self._process_event(event)

            # fire transitions
            [plan.run() for plan in self.ready_plans]
            # run actions
            [step.run() for step in self.ready_steps]

    def _quit(self) :
        print "I'm dying: event quit in the queue ! <:~("
        self.alive = false

        # stop services
        self.eventScheduler.stop()
        self._stop_rpc()
        self._stop_comService()

        # stop protocol handlers
        self._stop_handlers()

        # save memory to file ?
        if self.save_mem_on_quit :
            self._save_memory(self.save_mem_on_quit)
            
        # get rid of documents
        self._commit_memory()
        del self.memory    


    def _process_event(self,event) :
        if event[0] == 'endOfThread' :
            self._run_thread()
        # start plan ('startPlan', planName,(parent_plan,parent_step),elements)
        elif event[0] == 'startPlan' :
            self._start_plan(event[1],event[2],event[3])

        # planEnd ('planEnd', plan)
        elif event[0] == 'planEnd' :
            event[1].end()

        # stepEnd ('stepEnd', step)
        elif event[0] == 'stepEnd' :
            event[1].end()
        
        # quitting
        elif event[0] == 'quit' :
            self._quit()

        # postMessage ('postMessage', message_as_xml)
        elif event[0] == 'postMessage' :
            self.memory.add_message_as_string(event[1])

        # addElement ('addElement', element_as_xml)
        elif event[0] == 'addElement' :
            self.memory.add_element_as_string(event[1])
        
        # removeElement ('removeElement', eid)
        elif event[0] == 'removeElement' :
            self.memory.remove_element_by_id(event[1])
        
        # replaceElement ('replaceElement', eid, element_as_xml)
        elif event[0] == 'replaceElement' :
            self.memory.replace_element_as_string(event[1],event[2])
        
        # time condition ('time-condition', transition)
        elif event[0] == 'time-condition' :
            event[1].time_condition_match()

        # plan decay ('forgetPlan', plan)
        elif event[0] == 'forgetPlan' :
            self.memory.remove_element(event[1])
        # start/stop rpc ('startRPC', type, port) ('stopRPC')
        elif event[0] == 'startRPC' :
            self._start_rpc(event[1], event[2])
        elif event[0] == 'stopRPC' :
            self._stop_rpc()

        # load memory/recipes/modules/transforms
        elif event[0] == 'loadMemory' :
            self._load_memory(event[1])
        elif event[0] == 'loadRecipes' :
            self._load_recipes()
        elif event[0] == 'loadModules' :
            self._load_modules()
        elif event[0] == 'loadTransforms' :
            self._load_transforms()
        elif event[0] == 'loadProtocolHandlers':
            self._load_protocolhandlers()        
            
        # unknown
        else :
            print "WARNING: UNKNOWN EVENT",event

    # Start plan ###############################################################

    def _start_plan(self, planName, parents, elements) :
        """Change plan and first step states, then run first step
        """
        recipe = self.memory.get_recipe(planName)
        if recipe :
            recipe = self.check_date(recipe)
            code, msg = recipe.check_syntax()
        else :
            msg = 'Unknown recipe %s' % (planName)
                
        if msg :
            error = self.memory.mk_error('Bad recipe', msg)
            self.memory.add_element(error)
            print error
            print msg
            return

        # make plan and add it to memory
        plan = recipe.make_plan()
        self.memory.add_element(plan)
        plan.add_elements(elements)
        plan.set_parents(parents)
        # fire event
        self._fire_event('plan','instanciateRecipe', plan.eid, recipe.name)
        # run first step
        plan.start()

    # Thread management ########################################################

    def create_thread(self,*args,**kargs) :
        self.thread_queue.put( (args,kargs) )
        self._run_thread()

    def _run_thread(self) :
        if self.debug:
            print threading.activeCount(),'threads running...'
        if threading.activeCount() < self.max_threads :
            try:
                args, kargs = self.thread_queue.get_nowait()
                t = apply(EngineThread,args,kargs)
                t.start()
                if self.debug:
                    print 'started new thread'
            except Queue.Empty :
                pass

    def end_of_thread(self) :
        if self.debug :
            print 'thread done'
        self.post_event( ('endOfThread',) )
        
    # Internal events ##########################################################

    # take care to do stuff *before* posting events to the event queue or the
    # thready-boogieman will catch you by processing the event before you get
    # time to blink !
    
    def memory_change(self, action, element, old_element=None) :
        """ element: dom object """
        from xml.dom.ext import PrettyPrint
        eid = element.eid
        bibal.ASSERT(eid,'No eid in Memory Element ')
        # fire event
        if action == 'add' :
            self._fire_event('memory','addElement', eid)
            if element.name in ["start-plan", "al:plan"]:
                PrettyPrint(element.as_dom())
            if element.name == 'listen-on':
                d = element.as_dom()
                if d.namespaceURI == AL_NS:
                    self.post_event( ('startRPC', d.getAttributeNS(NO_NS, 'type'),
                                   int(d.getAttributeNS(NO_NS, 'port')), []) )
                    
        elif action == 'remove' :
            self._fire_event('memory','removeElement', eid)
        elif action == 'replace':
            self._fire_event('memory','replaceElement', eid)

        # event that call for a service
        if action != 'remove':
            dom = element.as_dom()
            if element.name == 'start-plan' and dom.namespaceURI == AL_NS:
                self.post_event(('startPlan',
                                 dom.getAttributeNS(NO_NS, 'recipe'),
                                 (None,None), []))

        if self.protocols:
            # event

            try:
                type = dom.getAttributeNS(NO_NS,'type')
            except:
                type = None
            type = None

            h_act = self._findHandler(element,'activator')
            h_input =  self._findHandler(element,'input',type)
            if action == 'add' and h_input:
                xmlstr = element.as_xml()
                try:
                    self.controlHandlerActivation(element,'activate',h_act[0])
                except:
                    pass
                h_input[1].handleOutgoing(xmlstr)


            if action!='remove' and h_act:
                self.controlHandlerActivation(element,'activate',h_act[0])
                        
            if action=='remove' and h_act:
                self.controlHandlerActivation(element,'deactivate',h_act[0])

    ##############################

    def step_change(self,step) :
        state = step.state
        self._fire_event('plan','stepStatusChanged', step.plan.eid,
                         step.id, step.state)
        if state == 'ready' :
            self.ready_steps.append(step)
        elif state == 'end' :
            self.post_event( ('stepEnd',step) )


    def plan_change(self, plan, action='state', element=None) :
        if action == 'state' :
            state = plan.state
            self._fire_event('plan','planStatusChanged',
                             plan.eid, state)
            if state == 'fireable' or state == 'ready':
                self.ready_plans.append(plan)
            elif state in ['end','failed-end'] :
                self.post_event( ('planEnd',plan) )
            elif state in ['done','failed']:
                # schedule decay
                self.schedule_event(('forgetPlan',plan), plan.decay)
        elif action == 'add' :
            self._fire_event('plan','planAddElement', plan.eid, element.eid)
        elif action == 'remove' :
            self._fire_event('plan','planRemoveElement', plan.eid, element.eid)


    def transition_change(self,transition) :
        self._fire_event('plan','transitionStatusChanged',
                         transition.plan.eid, transition.id,
                         transition.state)

    
    # fire Event ###############################################################

    def _fire_event(self,target,method,*args):
        if self.comService :
            self.comService.add_event(target, method, args)

    # Memory manipulation ######################################################

    def addElement(self, elementXml):
        self.post_event( ('addElement', elementXml) )

    def removeElement(self,elementId):
        self.post_event( ('removeElement',elementId) )

    def startPlan(self, recipeName, elements=[]):
        self.post_event( ('startPlan', recipeName, (None,None), elements) )

    def replaceElement(self, elementId, newElementXml):
        self.post_event( ('replaceElement', elementId, newElementXml) )

    def getMemory(self):
        return self.memory.as_xml('UTF-8')

    def getElement(self,elementId):
        element = self.memory.get_element(elementId)
        if element:
            return element.as_xml('UTF-8')
        return None

    def getElementsList(self):
        l = []
        for n in self.memory.get_elements():
            l.append(self._get_list(n))
        return l
    
    def getElementList(self,elementId):
        return self._get_list(self.memory.get_element(elementId))

    def _get_list(self, element):
        if element.name == 'error' and element.as_dom().namespaceURI == AL_NS:
            TYPE = 'error'
        else: TYPE = element.TYPE
        return (element.eid, TYPE, element.group, element.name)
    
    # Messaging ################################################################

    def postMessage(self,messageXml) :
        self.post_event( ('postMessage',messageXml) )
        
    # Debug ####################################################################

    def debugSuspend(self):
        self.step_by_step = true

    def debugContinue(self):
        self.step_by_step = false
        self.step_by_step_semaphore.release()

    def debugStepOneStep(self):
        if self.step_by_step :
            self.step_by_step_semaphore.release()
        return self.step_by_step

    # RemoteControl ############################################################

    def saveRecipes(self) :
        self._save_recipes()

    def saveTransforms(self) :
        self._save_transforms()

    def shutdown(self) :
        self.post_event( ('quit',) )



    # HandlerManagement ######################################################
    def instantiateHandlerRecipe(self, recipe_name, arguments_node, protocol):
        """recipe name will be instantiated and have as input
        arguments_node"""
        node = self.addElementFromHandlerToMemory(arguments_node, protocol)
        if node:
            self.startPlan(recipe_name, [node])

    def getPrototypeHelper(self, protocolHandler) :
        return protocolHandler(self).getPrototype()

    def addElementFromHandlerToMemory(self, arguments_node, protocol):
        """ arguments_node is of type jabber-request"""
        arguments_node = ALEFImplementation.element_from_string(arguments_node)
        if protocol.check_output(arguments_node):
            return arguments_node
        else:
            print 'Found no corresponding between one of activated handlers\
            and element that is tried to be added'

    def controlHandlerActivation(self, element, callback, protocol_name):
        # append new handlers in diciotnnary
        # callback = activate or deactivate
        # element is a narval alabstraction elt
        try:
            h = self.handlers[protocol_name]
            apply( getattr(h, callback), (element.as_xml(),) )
        except Exception, e:
            import traceback
            traceback.print_exc()
            raise 'Could not activate Handler: %s' % e


    def _findHandler(self, element, node_type, protocol_name=None):
        dom = element.as_dom()
        # temporary? if no protocol_name is given
        if protocol_name is None:
            for key, handler in self.handlers.items():
                # check if node_type (input/output/activator) matches
                check_func = getattr(handler, "check_%s"%node_type)
                if check_func(dom): 
                    return (key, handler)
        # 'create' the Handler corresponding to the protocol_name
        else:
            check_func = getattr(self.handlers[protocol_name], 
                                 "check_%s"%node_type)
            if check_func(dom):
                return (protocol_name, self.handlers[protocol_name])

        return None


# Start / Stop #################################################################

def start(narval,manageSignals = 0) :
    """Start Narval"""

    if manageSignals :
        print "manage signals"
        import signal
        signal.signal(signal.SIGINT, lambda x,y,z = narval: stop(z))
        signal.signal(signal.SIGTERM, lambda x,y,z = narval: stop(z))

    narval.run()
    print "Narval is dead..."


def stop(narval) :
    print '################### INTERRUPT'
    narval.post_event( ('quit',) )

NARVAL=None
def profile_start():
    NARVAL.run()

def profile_end(file):
    import pstats
    p = pstats.Stats(file)
    p.sort_stats('time','calls').print_stats(.25)
    p.sort_stats('cum','calls').print_stats(.25)
    p.strip_dirs().sort_stats('cum','calls').print_callers(.25)
    p.strip_dirs().sort_stats('cum','calls').print_callees()
    
# Main #########################################################################

USAGE = """Usage: narval [options]
   --debug           : print plan and memory messages
   --home DIR        : overwrite NARVAL_HOME environment variable with DIR
   --rcfile FILE     : use FILE instead of default narvalrc.xml init file
   --quit-when-done  : quit when no more running plan (only done or failed)
   --quit-after N    : quit after N seconds of execution
   --start-plan PLAN : execute PLAN right after starting
   --xmlrpc-port PORT: listens for UI xmlrpc client connections on specified port
   --pyro-port PORT  : listens for UI pyro client connections on specified port
   --max-threads N   : sets maximum number of concurrent threads to N
   --load-memory-file: uses specified FILE instead of default memory file
   --save-mem-on-quit: save memory to FILE when quitting
   --help            : print this help message
   """

def manage_home(path, chdir=1) :
    from narval import config
    if not config.set_home(path):
        msg = ('The home that you selected does not exist, or does not '+\
               'contain the required subdirectories. I can:\n'+\
              '\t[C]reate an empty directory structure for you\n'+\
              '\t[P]opulate a new directory structure using data from %s\n'+\
              '\t[S]top execution.') % config.get_share()
        print msg
        answer = ''
        while answer not in ['c','p','s']:
            answer = string.lower(raw_input('--> '))
        if answer == 'c':
            config.create_home_subdirs()
        elif answer == 'p':
            bibal.check_for_home()
        else:
            sys.exit(0)
    print 'Using %s as home' % path
    if chdir:
        os.chdir(path)
    
def Run(sysArgs):
    # process options
    import getopt,sys
    from socket import gethostname
    short_list = ''
    long_list = ['debug', 'quit-after=', 'quit-when-done', 'start-plan=',
                 'home=', 'help', 'use-database','xmlrpc-port=','pyro-port=',
                 'max-threads=', 'rcfile=',
                 'load-memory-file=', 'save-mem-on-quit=', 'profile=']
    optlist, args = [], []
    try:
        optlist, args = getopt.getopt(sysArgs[1:], short_list, long_list)
    except getopt.error,e :
        print 'Warning:',e
        print USAGE
        sys.exit(1)

    # specific home or memory setup ?
    mempath, home, prof_file, rcpath = None, None, None, None
    for opt, val in optlist :
        if opt =='--home' :
            home = os.path.abspath(val)
        elif opt == '--use-database' :
            print 'setting use_database to 1'
            ALMFImplementation.use_database = 1
        elif opt == '--load-memory-file' :
            print 'using',val,'as memory file'
            mempath = os.path.abspath(val)
        elif opt == '--rcfile' :
            print 'using', val,'as configuration file'
            rcpath = os.path.abspath(val)
        elif opt == '--profile':
            prof_file = val
        elif opt == '--help':
            print USAGE
            sys.exit(1)
            
    if home:
        manage_home(home)
    # make sure that NARVAL_HOME exist and is populated
    bibal.check_for_home()
    
    # create Narval instance
    n = Narval(rcpath)
    n.post_event( ('loadRecipes',) )
    n.post_event( ('loadTransforms',) )
    n.post_event( ('loadModules',) )
    n.post_event( ('loadProtocolHandlers',) )
    n.post_event( ('loadMemory',mempath) )

    # process options
    for opt, val in optlist :
        if opt == '--debug' :
            n.debug = 1
            n._start_comService()
            from narval.PlanListener import PlanListenerPrintImplementation
            from narval.MemoryListener import MemoryListenerPrintImplementation
            n.comService.addPlanListener(PlanListenerPrintImplementation())
            n.comService.addMemoryListener(MemoryListenerPrintImplementation())

        elif opt == '--quit-after' :
            n.schedule_event(('quit',),when=int(val))

        elif opt == '--quit-when-done' :
            n.quit_when_done = true

        elif opt == '--save-mem-on-quit' :
            n.save_mem_on_quit = val

        elif opt == '--start-plan' :
            n.post_event( ('startPlan',val,(None,None),[]) )

        elif opt == '--xmlrpc-port':
            n.addElement("<al:listen-on type='xmlrpc' host='%s' port='%d' xmlns:al='%s'/>"%(
                gethostname(),int(val), AL_NS)) 

        elif opt == '--pyro-port':
            n.addElement("<al:listen-on type='pyro' host='%s' port='%d' xmlns:al='%s'/>"%(
                gethostname(),int(val), AL_NS)) 
           
        elif opt == '--max-threads' :
            n.max_threads = int(val)
            
        elif opt in ('--home','--use-database','--load-memory-file', '--rcfile',
                     '--profile') :
            pass

        elif opt == '--protocol-handlers':
            n._getProtocolHandlers()

        else :
            print USAGE
            sys.exit(1)
    
    # start
    if prof_file:
        import profile
        print '-'*80
        print ' PROFILE EXECUTION'
        print '-'*80
        global NARVAL
        NARVAL=n
        profile.run('profile_start()', prof_file)
        profile_end(prof_file)
    else:
        start(n,0)

if __name__ == "__main__" :
    import sys
    Run(sys.argv)
