# 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
"""Narval's main memory


: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: Memory.py,v 1.20 2002/10/14 14:24:11 syt Exp $"
__docformat__ = "restructuredtext en"

import time
import traceback
from cStringIO import StringIO

from narval import engine_interfaces
from narval.tags import clear_tags
from narval.recipe import RecipeElement
from narval.action import ActionElement
from narval.plan import PlanElement
from narval.public import AL_NS, implements, \
     multi_match_expression

from narval.interfaces.core import IError, IMessage
from narval.elements import create_error
from narval.elements.core import StartPlanElement, GroupAliasElement


def mk_time_stamp():
    """return a time stamp

    :rtype: float
    :return: time representation for now
    """
    return time.time()



class Memory:
    """narval's main memory, holds a set of elements, derived from
    `narval.public.ALElement`
    

    :type narval: `narval.engine.Narval`
    :ivar narval: the narval interpreter

    :type elements: dict
    :ivar elements: elements in memory, indexed by their eid

    :type actions: dict
    :ivar actions:
      eid of actions (`narval.action.ActionElement`) in memory, indexed
      by their identity (<group>.<name>)

    :type recipes: dict
    :ivar recipes:
      eid of recipes (`narval.recipe.RecipeElement`) in memory, indexed
      by their identity (<group>.<name>)

    :type plans: list
    :ivar plans:
      list of plans (`narval.plan.PlanElement`) in memory

    :type eid_count: int
    :ivar eid_count: eid counter

    :type eid_ref_count: dict
    :ivar eid_ref_count:
      references counter : keys are eids and values number of references to the
      element with the given eid
    """

    __implements__ = (engine_interfaces.ITransitionChangeListener,
                      engine_interfaces.IStepChangeListener,
                      engine_interfaces.IPlanChangeListener)
    
    def __init__(self, narval=None) :
        self.narval = narval
        self.elements = {}
        self.actions, self.recipes = {}, {}
        self._aliases = {}
        self.plans = []
        self.eid_count = 1
        self.eid_ref_count = {}
        self.registry = self.narval.registry
        
    def get_element(self, eid):
        """return the element with the given eid

        :type eid: int
        :param eid: the unique identifier of the element
        
        :rtype: `narval.public.ALElement` or None
        :return: the element or None if the given eid is not found
        """
        return self.elements.get(eid)
        
    def get_elements(self) :
        """return all elements in memory
        
        :rtype: list
        :return: all elements in memory
        """
        return self.elements.values()

    def get_recipe(self, recipe_name) :
        """return the recipe with the given name

        :type recipe_name: str
        :param recipe_name: <group>.<name>
        
        :rtype: `narval.recipe.RecipeElement`
        :return: the recipe element or None if the given name is not found
        """
        return self.recipes[self._key_from_name(recipe_name)]

    def get_recipes(self):
        """return all recipe elements in memory

        :rtype: list
        :return: all recipe elements in memory
        """
        return self.recipes.values()

    def get_action(self, name) :
        """return the action with the given name

        :type name: str
        :param name: <group>.<name>
        
        :rtype: `narval.action.ActionElement`
        :return: the action element or None if the given name is not found
        """
        return self.actions[self._key_from_name(name)]
    
    def _key_from_name(self, name):
        group, name = name.split('.')
        try:
            group = self._aliases[group]
        except KeyError:
            pass
        return group, name
    
    def get_actions(self):
        """return all action elements in memory

        :rtype: list
        :return: all action elements in memory
        """
        return self.actions.values()

    def as_xml(self, encoding='UTF-8'):
        """return the memory's content as an xml string

        :type encoding: str
        :param encoding:
          the encoding to use in the returned string, default to UTF-8

        :rtype: str
        :return: the memory as an XML document
        """
        stream = StringIO()
        write = stream.write
        write('<?xml version="1.0" encoding="%s"?>\n<memory xmlns:al="%s">\n' % (
            encoding, AL_NS))
        for elmt in self.elements.values():
            write(elmt.as_xml(encoding))
            write('\n')
        write('</memory>')
        return stream.getvalue()

    # memory manipulation methods ##############################################
    
    def add_message_as_string(self, msg_str):
        """add a message as xml string to the memory

        a message is sent by others assistants

        :type msg_str: str
        :param msg_str:
          the XML document containing the message (see narval's DTD for the
          message's syntax)
        """
        element = self.registry.from_string(msg_str)[0]
        if not implements(element, IMessage):
            log(LOG_ERR, "received a false message, dropped it")
        else:
            element.type = 'incoming'
            self.add_element(element)

    def add_element_as_string(self, element_str):
        """add an element as xml string to the memory

        :type element_str: str
        :param element_str: the XML document containing the element
        """
        self.add_element(self.registry.from_string(element_str)[0])

    def replace_element_as_string(self, eid, element_str):
        """replace an element with a given eid by the element contained in a
        xml string

        :type eid: int
        :param eid: the identifier of the element to replace

        :type element_str: str
        :param element_str: the XML document containing the new element
        """
        try:
            element = self.elements[eid]
            new_element = self.registry.from_string(element_str)[0]
            self.replace_element(element, new_element)
        except KeyError :
            log(LOG_WARN, 'No element with eid %s in memory', eid)

    def add_elements(self, elements):
        """add the given elements list to the memory

        :type elements: list
        :param elements: list of element to add in memory
        """
        map(self.add_element, elements)

    def add_element(self, element):
        """add an element to memory

        if the element has already an eid, just increment its reference counter
        else, (ie yet in memory) assign unique id to the element (eid) and
        append it to memory.

        :type element: `narval.public.ALElement`
        :param element: the element to add in memory

        :rtype: tuple
        :return: the eid of the element and the element itself
        """
        if element.eid:
            eid = element.eid
            self.eid_ref_count[eid] += 1
        else :
            # assign new eid to element
            element.eid = eid = self.eid_count
            self.eid_count += 1
            self.eid_ref_count[eid] = 1
            element.timestamp = mk_time_stamp()
            # do some caching processing
            self.elements[eid] = element
            if isinstance(element, GroupAliasElement):
                self._aliases[element.name] = element.actual
            elif isinstance(element, PlanElement):
                self.plans.append(element)
            elif isinstance(element, RecipeElement):
                self.recipes[element.get_identity()] = element
            elif isinstance(element, ActionElement):
                self.actions[element.get_identity()] = element
            elif self.narval.debug and implements(element, IError):
                log(LOG_DEBUG, element.as_xml(self.narval.encoding))
            element.memory = self
            # element added to memory: fire event.
            self.narval.memory_change('add', element)
            # this new element may trigger a transition. propagate.
            for plan in self.plans:
                plan.element_change(element)
        return eid, element

    def replace_element(self, old_element, new_element):
        """replace an element in memory

        :type old_element: `narval.public.ALElement`
        :param old_element: the element that should be replaced

        :type new_element: `narval.public.ALElement`
        :param new_element: the new element
        """
        new_element.eid = old_element.eid
        new_element.memory = self
        self.elements[old_element.eid] = new_element
        if isinstance(old_element, PlanElement):
            self.plans.remove(old_element)
        elif isinstance(old_element, RecipeElement):
            del self.recipes[old_element.get_identity()]
        elif isinstance(old_element, ActionElement):
            del self.actions[old_element.get_identity()]
        if isinstance(new_element, PlanElement):
            self.plans.append(new_element)
        elif isinstance(new_element, RecipeElement):
            self.recipes[new_element.get_identity()] = new_element
        elif isinstance(new_element, ActionElement):
            self.actions[new_element.get_identity()] = new_element
        self.narval.memory_change('replace', new_element, old_element)
        # FIXME: this new element may trigger a transition. propagate ?

    def remove_element(self, element):
        """remove an element from memory

        persistent or outdated elements are actually not removed
        
        :type element: `narval.public.ALElement`
        :param element: the element that should be removed
        """
        if element.outdated or not element.persist:
            clear_tags(element)
            self.narval.memory_change('remove', element)
            del self.elements[element.eid]
            if isinstance(element, PlanElement):
                self.plans.remove(element)
                element.cleanup()
            elif isinstance(element, RecipeElement):
                del self.recipes[element.get_identity()]
            elif isinstance(element, ActionElement):
                del self.actions[element.get_identity()]
            del element    

    def remove_element_by_id(self, eid):
        """remove the element with the given eid from the memory
        
        :type eid: int
        :param eid: the identifier of the element to remove
        """
        try:
            self.remove_element(self.elements[eid])
        except KeyError :
            log(LOG_WARN, 'no element with eid %s in memory', eid)

    def delete_ref_to_element(self, element):
        """decrement the reference counter for the given element

        :type element: `narval.public.ALElement`
        :param element: element that should be "decrefed"
        """
        eid = element.eid
        self.eid_ref_count[eid] -= 1
        if self.eid_ref_count[eid] < 1:
            self.remove_element(element)

    def references_count(self, eid):
        """return the number of references on the element with the given eid"""
        return self.eid_ref_count[eid]
    
    # memory facilities ########################################################

    def are_active_plans(self) :
        """return true if there are some active plans in memory
        
        :rtype: bool
        :return:
          flag indicating whether there are or not some active plans in memory
        """
        for plan in self.plans:
            if plan.state not in ('done', 'failed'):
                return True
        return False

    def mk_error(self, msg, err_type=None) :
        """build an error element

        :type msg: str
        :param msg: the error's message

        :type err_type: str or None
        :param err_type: optional error's type
        
        :rtype: IError
        :return: an error element with the given type / msg
        """
        return create_error(msg, err_type)

    def mk_traceback_error(self, plan, info) :
        """build an error element from a python traceback

        :type plan: plan
        :param plan: the originate plan of the error

        :type info: tuple
        :param info:
          the result of sys.exc_info(), ie a tuple (ex class, ex instance,
          traceback)
        
        :rtype: IError
        :return: an error element with the given type / msg
        """
        log(LOG_ERR, 'error in plan %s', plan.eid)
        exc_class, value, tbck = info
        buf = StringIO()
        traceback.print_exception(exc_class, value, tbck, file=buf)
        tb_string = buf.getvalue()
        error = self.mk_error(tb_string, exc_class)
        return error

    def get_inputs(self, input, context=None):
        """return elements in memory matching the given input prototype, after
        having incremented their reference counter
        
        :type input: `narval.prototype.InputEntry`
        :param input: the prototype input looking for elements

        :rtype: list
        :return: matching elements
        """
        cands = input.match_elements(self.elements.values(), context)
        # FIXME (syt): i think the commented code below introduce a memory leak,
        # since elements retreived via this method are also inc-refed when
        # the plan.add_elements call memory.add_elements, while plan.cleanup
        # only remove a single reference
        #
        #for cand in cands:
        #    self.eid_ref_count[cand.eid] += 1
        return cands

    def match_elements(self, match):
        """return elements corresponding to the given <match> expression

        :type match: str
        :param match: the expression to match

        :rtype: list
        :return: matching elements
        """
        return multi_match_expression(match, self.elements.values())
    
    def transition_wait_time(self, transition, date):
        """schedule a time condition for a transition

        :type transition: `narval.plan.PlanTransition`
        :param transition: the transition with a time condition

        :type date: float
        :param date: the absolute date of the time condition
        """
        self.narval.schedule_event(('time_condition', transition), when=date,
                                   date=True)
        
    # proxy for engine #########################################################

    def step_change(self, step) :
        """see `narval.engine.Narval.step_change`"""
        self.narval.step_change(step)

    def plan_change(self, plan, action='state', element=None) :
        """see `narval.engine.Narval.plan_change`"""
        self.narval.plan_change(plan, action, element)

    def transition_change(self, transition) :
        """see `narval.engine.Narval.transition_change`"""
        self.narval.transition_change(transition)

    def start_plan(self, recipe_name, parent_plan, parent_step, elements):
        """see `narval.engine.Narval.start_plan`"""
        start_plan = StartPlanElement(recipe=recipe_name)
        start_plan.parent_plan = parent_plan
        start_plan.parent_step = parent_step
        start_plan.context = elements
        self.narval.start_plan(start_plan)

    def check_date(self, element):
        """see `narval.engine.Narval.check_date`"""
        return self.narval.check_date(element)

    def create_thread(self, *args, **kwargs) :
        """see `narval.engine.Narval.create_thread`"""
        self.narval.create_thread(*args, **kwargs)
        


