# Copyright (c) 2004-2005 DoCoMo Euro-Labs GmbH (Munich, Germany).
# Copyright (c) 2001-2005 LOGILAB S.A. (Paris, FRANCE).
#
# http://www.docomolab-euro.com/ -- mailto:tarlano@docomolab-euro.com
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
"""defines the protocol handler interface and a default abstract implementation


:version: $Revision:$ 
:author: Logilab 

:copyright:
  2001-2005 LOGILAB S.A. (Paris, FRANCE)
  
  2004-2005 DoCoMo Euro-Labs GmbH (Munich, Germany)
  
:contact:
  http://www.logilab.fr/ -- mailto:contact@logilab.fr
  
  http://www.docomolab-euro.com/ -- mailto:tarlano@docomolab-euro.com
"""

__revision__ = "$Id: ALAbstraction.py,v 1.36 2004/04/02 10:06:46 syt Exp $"
__docformat__ = "restructuredtext en"

from threading import Thread

from narval.public import AL_NS, match_expression
from narval.engine_interfaces import IProtocolHandler

class ProtocolActivationException(Exception):
    """Base class for activation exceptions"""

class HandlerAlreadyActivated(ProtocolActivationException):
    """raised if a protocol handler is activated twice
    for semantically identical activators
    """

class HandlerNotActivated(ProtocolActivationException):
    """raised if the handler had not been activated
    with the element for which it is deactivated, or has not been activated to
    handle the input, or is not expecting any input
    """

class ProtocolHandler:
    """Base class for protocol handlers
    
    Derived classes **must** define the following attributes:
    
    :type prototype: tuple
    :cvar prototype: a 3-uple : (('activator desc',('activator matches',)),
                                 ('input desc',('input matches',)),
                                 ('output desc',('output matches',)))
                                 
    Derived classes **may** override the following attributes:
    
    :type namespace: tuple
    :cvar namespace: a sequence of ('prefix', 'nsuri') used in prototype

    :type documentation: str or None
    :cvar documentation:
      a string documenting the handler. The docstring of the class is used as
      the documentation if self.documentation is not set.
    """
    __implements__ = IProtocolHandler
    
    ACTIVATOR, INPUT, OUTPUT = range(3)
    DESC, MATCH = range(2)
    prototype = (('activator desc',('activator matches',)),
                 ('input desc',('input matches',)),
                 ('output desc',('output matches',))
                 )
    namespaces = [('al', AL_NS)]
    documentation = None
    
    def __init__(self, narval):
        self.narval = narval
        self.__xml_prototype = None
        
    def get_prototype(self):
        """returns an XML string defining the prototype for the handler
        
        The string is a valid XML document, according to the protocol
        handler's prototype DTD

        :rtype: str
        :return: the XML snippet defining the protocol handler's prototype

        This method uses self.namespaces, self.prototype and self.documentation
        to generate an XML prototype.
        """
        return self.__build_prototype()
    
    def __build_prototype(self):
        """return the xml prototype. The prototype is built on the first call
        using instance attributes *namespaces*, *prototype* and *documentation*

        :rtype: str
        :return: the XML prototype for the protocol handler
        """
        if self.__xml_prototype is None:
            proto = self.prototype
            match = self.MATCH
            activator = ''.join(["<al:match>%s</al:match>" % expr
                                 for expr in proto[self.ACTIVATOR][match]])
            input = ''.join(["<al:match>%s</al:match>" % expr
                             for expr in proto[self.INPUT][match]])
            output = ''.join(["<al:match>%s</al:match>" % expr
                              for expr in proto[self.OUTPUT][match]])
            xmlns = ' '.join(["xmlns:%s=%s" % (p, repr(u))
                              for (p, u) in self.namespaces])
            self.__xml_prototype = ''.join([ "<al:prototype ",xmlns,">",
                                             "<al:description lang='en'>",
                                             self.documentation or self.__doc__,
                                             "</al:description>",
                                             "<al:activator id='activator'>",
                                             "<al:description lang='en'>",
                                             proto[self.ACTIVATOR][self.DESC],
                                             "</al:description>",
                                             activator,
                                             "</al:activator>",
                                             "<al:input>"
                                             "<al:description lang='en'>",
                                             proto[self.INPUT][self.DESC],
                                             "</al:description>",
                                             input,
                                             "</al:input>",
                                             "<al:output>"
                                             "<al:description lang='en'>",
                                             proto[self.OUTPUT][self.DESC],
                                             "</al:description>",
                                             output,
                                             "</al:output>",
                                             "</al:prototype>"])
        return self.__xml_prototype

    def activate(self, activator):
        """activates the handler with the given element.
        
        A handler can be activated more than once, and should behave
        accordingly. For instance an HTTP handler can be activated on
        different ports. The handler should use the eid of the element
        to be able to deactivate the proper service.

        :type activator: `narval.public.ALElement`
        :param activator: the element activating the handler
        
        :raise HandlerAlreadyActivated:
          if the activate method already has been called with a semantically
          identical activator
        """
        
    def deactivate(self, eid):
        """deactivates the handler.
        
        :type eid: int
        :param eid: the eid of the element deactivating the handler
        
        :raise HandlerNotActivated:
          if the handler had not been activated with the element
        """

    def handle_outgoing(self, element):
        """passes an element matching one of the inputs of the handler.
        
        :raise `HandlerNotActivated`:
          if the handler has not been activated, or has not been activated to
          handle the input, or is not expecting any input.

        :type element: `narval.public.ALElement`
        :param element: the outgoing element
        """

    def stop(self):
        """stops the handler: stops all threads launched by the handler, if any
        """

    def register(self, registry):
        """register specific stuff for this protocol handler

        :type registry: `narval.reader.Registry`
        :param registry: the interpreter's registry
        """

    # utilities ###############################################################
    
    def check_output(self, element) :
        """check that the given element matches all the outputs defined in the
        prototype

        :type element: `narval.public.ALElement`
        :param element: the (usually outgoing) element to check

        :rtype: bool
        :return: true if the element satisfy the output prototype
        """
        return self._check_element(self.prototype[self.OUTPUT][self.MATCH],
                                   element)

    def check_input(self, element):
        """check that element matches one of the inputs defined in the prototype
        
        :type element: `narval.public.ALElement`
        :param element: the (usually incoming) element to check

        :rtype: bool
        :return: true if the element satisfy the input prototype
        """
        context = {'elmt': element}
        for match in self.prototype[self.INPUT][self.MATCH] :
            if match_expression(match, context):
                return True
        return False
       
    def check_activator(self, element):
        """check if element matches the activator
        
        :type element: `narval.public.ALElement`
        :param element: the element to check

        :rtype: `narval.public.ALElement` or None
        :return: the element if it satisfies the activator prototype, or None
        """
        if self._check_element(self.prototype[self.ACTIVATOR][self.MATCH],
                               element):
            return element
        return None

    def _check_element(self, prototype, element) :
        """check if the element match all matches of the prototype

        :type prototype: list
        :param prototype: list of match expression to satisfy

        :type element: `narval.public.ALElement`
        :param element: the element to check

        :rtype: bool
        :return: True if the element satisfy all matches in the prototype
        """
        context = {'elmt': element}
        for match in prototype:
            if not match_expression(match, context):
                return False
        return True

    def instantiate_handler_recipe(self, recipe_name, element,
                                   context_elements=None):
        """checks the element against the output declared in the handler's
        prototype, and instantiates a plan from the recipe with the element
        in the plan's memory

        :type recipe_name: str
        :param recipe_name:
          the name of the recipe to instantiate (<cookbook>.<recipe>)
        
        :type element: `narval.public.ALElement`
        :param element: the element generated by the protocol handler
        
        :type context_elements: iterable
        :param element:
          additional elements to put in the instantiated plan's context
        """
        self.narval.instantiate_handler_recipe(recipe_name, element, self,
                                               context_elements)


class TwistedProtocolHandler(ProtocolHandler):
    """a twisted based protocol handler"""
    
    REACTOR = None
    REACTOR_THREAD = None
    def twisted_run(cls, host, port, factory): # FIXME: handle only TCP
        """a class method registering twisted protocol handler to the reactor
        """
        reactor = TwistedProtocolHandler.REACTOR
        need_run = False
        if reactor is None:
            from twisted.internet import reactor
            TwistedProtocolHandler.REACTOR = reactor
            need_run = True
        reactor.connectTCP(host, port, factory)
        if need_run:
            thread = Thread(target=reactor.run, name='twisted main loop',
                            kwargs={'installSignalHandlers': False})
            # FIXME: added this because the twisted thread never return,
            # even after a call to stop()...
            thread.setDaemon(True)
            TwistedProtocolHandler.REACTOR_THREAD = thread
            thread.start()

    twisted_run = classmethod(twisted_run)

    # FIXME: deactivate
    # FIXME: how to stop/deactivate one single protocol handler ?
    
    def stop(self):
        reactor = TwistedProtocolHandler.REACTOR
        if reactor is not None:
            reactor.disconnectAll()
            #reactor.running = False
            reactor.callFromThread(reactor.stop)#stop()
            TwistedProtocolHandler.REACTOR = None
            #TwistedProtocolHandler.REACTOR_THREAD.join()
