# 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.
""" AlAbstraction module.

Object classes manipulated by Narval
"""
__revision__ = "$Id: ALAbstraction.py,v 1.28 2002/01/17 18:41:21 zoe Exp $"

import copy
from types import UnicodeType as STRING_TYPE
try:
    from Ft.Xml.Xslt.DomWriter import DomWriter
    from Ft.Xml.Xslt.Processor import Processor
    from Ft.Xml.Xslt import XsltException
    from Ft.Xml.XPath import Util
except:
    from xml.xslt.DomWriter import DomWriter
    from xml.xslt.Processor import Processor
    from xml.xslt import XsltException
    from xml.xpath import Util
from narval.lib import TRUE, FALSE, AL_NS, NO_NS, xpath_lock, StripXml, PrettyPrint
from narval.bibal import shallow_calendar
from narval.dom import Document
from narval.utils.misc import intersection, difference, value, rev_value

C_ALL = 0
C_STEP = 1
C_PLAN = 2
C_PARENT = 3
C_MEM = 4
CONTEXT= {'': C_ALL, 'step': C_STEP, 'plan': C_PLAN, 'parent_plan': C_PARENT,
          'memory': C_MEM}
            
# Base abstract class #########################################################
class CachedObject:
    def __init__(self):
        # cache
        self.__dict__['_dom'] = None

    def as_dom(self, doc=None):
        if self._dom:
            return self._dom
        raise Exception('not implemented')

    def as_xml(self, encoding='UTF-8'):
        if not self._dom:
            self.as_dom()
        from cStringIO import StringIO
        buf = StringIO()
        PrettyPrint(self._dom, buf, encoding=encoding)
        data = buf.getvalue()
        buf.close()
        return data

# Mixin for dynamic objects ###################################################
class StateMixin:
    def set_state(self, state):
        # -- precondition
        oldstate = self.state
        if state == oldstate : return
        assert state in self.SDD[oldstate], \
               'Bad state diagram transition in %s %s from %s to %s' % (
            self.TYPE, (hasattr(self, 'name') and self.name)
            or (hasattr(self, 'id') and self.id),
            oldstate, state)
        # change state
        self.state = state
        # update cache if necessary
        if self._dom:
            self._dom.setAttributeNS(NO_NS, 'state', state)
        # propagate change
        for c in self.get_callbacks(state):
            c(self)


# Base Narval element #########################################################
class ALElement(CachedObject):
    """ ALElement with delegation approach """
    def __init__(self):
        CachedObject.__init__(self)
        self.__dict__['outdated'] = 'no'

    def get_identity(self):
        """ returns the (group,name) attributes in a tuple """
        return (self.group, self.name)
    
    def __repr__(self):
        return '<Narval.%s %s (%s) at %d>'%(str(self.__class__).split('.')[-1],
                                            self.name, self.eid, id(self))
        
# Concrete derivated classes ##################################################
class Element(ALElement):
    """
    Narval base element: same properties as ALElement with an dom tree to
                         handle data
    """
    TYPE = 'element'
    NO_NS_ATTRS = ('group', 'persist')
    AL_NS_ATTRS = ('eid', 'input_id', 'timestamp', 'outdated',
                   'from_plan', 'from_step')

    def __getattr__(self, attr):
        if self.__dict__['_dom']:
            if attr == 'name':
                return self.__dict__['_dom'].localName
            if attr in self.NO_NS_ATTRS:
                return self.__dict__['_dom'].getAttributeNS(NO_NS, attr)
            if attr in self.AL_NS_ATTRS:
                return self.__dict__['_dom'].getAttributeNS(AL_NS, attr)
        raise AttributeError(attr)
    
    def __setattr__(self, attr, value):
        if attr in self.NO_NS_ATTRS:
            self.__dict__['_dom'].setAttributeNS(NO_NS, attr, value)
        elif attr in self.AL_NS_ATTRS:
            #print 'Set Narval attribute',attr, value, 'to', self.__dict__['_dom'].nodeName, id(self)
            value = value or ''
            self.__dict__['_dom'].setAttributeNS(AL_NS, 'al:%s' % attr, value)
        else:
            self.__dict__[attr] = value
            
    def __delattr__(self, attr):
        if attr in self.NO_NS_ATTRS:
            self.__dict__['_dom'].setAttributeNS(NO_NS, attr, None)
        elif attr in self.AL_NS_ATTRS:
            self.__dict__['_dom'].setAttributeNS(AL_NS, 'al:%s' % attr, None)
        else:
            del self.__dict__[attr]
        
    def get_identity(self):
        """ returns the (group,name) attributes in a tuple """
        return (self._dom.getAttributeNS(NO_NS, 'group'),
                self._dom.nodeName)
    

class TransformElement(ALElement):
    """ Narval tranform element: Element +
    """
    TYPE = 'transform'
    _ATTRS = ('name', 'group', 'eid', 'input_id', 'persist',
              'from_plan', 'from_step')
    
    def __init__(self):
        ALElement.__init__(self)
        self.__dict__['prototype'] = Prototype()
        self.__dict__['processor'] = None
        self.__dict__['descr'] = ''

    def __setattr__(self, attr, value):
        if attr in self._ATTRS:
            self.__dict__['_dom'].setAttributeNS(AL_NS, 'al:%s' % attr, value)
        else:
            self.__dict__[attr] = value
        
    def __getattr__(self, attr):
        if attr in self._ATTRS:
            return self.__dict__['_dom'].getAttributeNS(AL_NS, attr)
        raise AttributeError(attr)

    def __delattr__(self, attr):
        if attr in self._ATTRS:
            self.__dict__['_dom'].setAttributeNS(AL_NS, 'al:%s' % attr, None)
        else:
            del self.__dict__[attr]

    def transform(self, element, target_doc) :
        self._mk_processor()
        xpath_lock.acquire()
        Util.FreeDocumentIndex(element.ownerDocument)
        Util.IndexDocument(element.ownerDocument)
        dom = self.processor.runNode(element, 1, {}, DomWriter(target_doc))
        xpath_lock.release()
        for node in dom.childNodes :
            node.normalize() # normalize doesn't work on document fragments
        dom = StripXml(dom)
        return dom.childNodes

    def _mk_processor(self) :
        if self.processor : return
        # builds a new processor
        from urllib import basejoin
        from narval import config
        self.processor = Processor(stylesheetIncPaths=[
            'file://%s/%s/' % (config.get_home(), 'transforms')])
        self._register_extensions()
        xpath_lock.acquire()
        try:
            self.processor.appendStylesheetString(self.as_xml())
        except XsltException, e:
            import traceback
            traceback.print_exc()
            raise e
        xpath_lock.release()

    def _register_extensions(self, modules_list = None):
        import os, sys, string
        from narval import config
        modules_list = modules_list or []
        narval_ext_path = config.get_home()+'/transforms/extensions'
        if narval_ext_path not in sys.path:
            sys.path.append(narval_ext_path)
        for entry in os.listdir(narval_ext_path):
            if not os.path.isdir(entry) and entry[-3:] == '.py' \
                   and entry != '__init__.py':
                modules_list.append(entry[:-3])
                
        env = os.environ.get('EXTMODULES')
        if env is None:
            if len(modules_list):
                self.processor.registerExtensionModules(modules_list)
            return
        if os.name == 'posix':
            dir_list = string.split(env,':')
        elif os.name in ['nt','dos']:
            dir_list = string.split(env,';')
        else:
            if len(modules_list):
                self.processor.registerExtensionModules(modules_list)
            return
        for dir in dir_list:
            for entry in os.listdir(dir):
                if not os.path.isdir(entry) and entry[-3:] == '.py' and entry != '__init__.py':
                    modules_list.append(entry[:-3])
        if len(modules_list):
            self.processor.registerExtensionModules(modules_list)


class ActionElement(ALElement):
    """ Narval action element: ALElement +
    """
    TYPE = 'action'
    def __init__(self, name, group, func, persist='no'):
        ALElement.__init__(self)
        self.name = name
        self.group = group
        self.func = func
        self.prototype = Prototype()
        self.persist = persist
        self.descr = ''
        
    def as_dom(self, doc=None):
        if self._dom:
            return self._dom
        if not doc:
            doc = Document()
        n = doc.createElementNS(AL_NS, 'al:action')
        n.setAttributeNS(NO_NS, 'name', self.name)
        n.setAttributeNS(NO_NS, 'group', self.group)
        n.setAttributeNS(NO_NS, 'func', self.func)
        n.setAttributeNS(NO_NS, 'persist', self.persist)
        n.setAttributeNS(AL_NS, 'al:eid', self.eid)
        if hasattr(self, 'input_id'):
            n.setAttributeNS(AL_NS, 'al:input_id', self.input_id)
        if hasattr(self, 'from_plan'):
            n.setAttributeNS(AL_NS, 'al:from_plan', self.from_plan)
        if hasattr(self, 'from_step'):
            n.setAttributeNS(AL_NS, 'al:from_step', self.from_step)
        if self.descr:
            d = doc.createElementNS(AL_NS, 'al:description')
            d.appendChild(doc.createTextNode(self.descr))
            n.appendChild(d)
        d = self.prototype.as_dom(doc)
        ## sometime a bug appears if we try to append an empty doc frag
        if d.childNodes:
            n.appendChild(d)
        self._dom = n
        return n


class RecipeElement(ALElement):
    """ Narval recipe element: ALElement +
                 _ recipe construction methods
                 _ plan instanciation method
    """
    TYPE = 'recipe'
    def __init__(self, name, group, restart='no', decay='90', persist='no'):
        # default DECAY set to 90 seconds, because we use too much RAM
        ALElement.__init__(self)
        self.name = name
        self.group = group
        self.restart = value(restart)
        self.decay = int(decay)
        self.persist = persist
        self.descr = ''
        self.elements = {} # {id: Step_instance}
        self.check, self.chktext = None, ''
        self.pub_args = ['name', 'group', 'decay']
    
    def as_dom(self, doc=None):
        if self._dom:
            return self._dom
        name = 'al:%s'%self.TYPE
        if not doc:
            doc = Document()
        n = doc.createElementNS(AL_NS, name)
        n.setAttributeNS(NO_NS, 'persist', self.persist)
        n.setAttributeNS(AL_NS, 'al:eid', self.eid)
        if hasattr(self, 'input_id'):
            n.setAttributeNS(AL_NS, 'al:input_id', self.input_id)
        if hasattr(self, 'from_plan'):
            n.setAttributeNS(AL_NS, 'al:from_plan', self.from_plan)
        if hasattr(self, 'from_step'):
            n.setAttributeNS(AL_NS, 'al:from_step', self.from_step)
        if self.descr:
            d = doc.createElementNS(AL_NS, 'al:description')
            d.appendChild(doc.createTextNode(self.descr))
            n.appendChild(d)
        for a in self.pub_args:
            n.setAttributeNS(NO_NS, a, str(getattr(self, a)))
        for e in self.elements.values():
            n.appendChild(e.as_dom(doc))
        self._dom = n
        return n
        
    # Edit interface ##########################################################
    def add_step(self, step):
        self.elements[step.id] = step
        return step.id

    def remove_step(self, step):
        del self.elements[step.id]
         
    def add_transition(self, transition):
        self.elements[transition.id] = transition
        return transition.id

    def remove_transition(self, transition):
        del self.elements[transition.id]
    
    #  ########################################################################
    def check_syntax(self) :
        print 'check syntax for recipe', self.group, self.name
        # if that recipe was checked, return result
        if self.check in ('ok', 'bad') :
            return self.check, self.chktext
        chktext = ''
        # FIXME: detect cycles
        # make lists
        transition_list = []
        step_list = []
        transition_ids = {}
        step_ids = {}
        for s_or_t in self.elements.values():
            if s_or_t.TYPE == 'transition':
                transition_list.append(s_or_t)
                if not s_or_t.id:
                    chktext = '%s\n* Missing transition id.' % chktext
                elif transition_ids.has_key(s_or_t.id) \
                         or step_ids.has_key(s_or_t.id):
                    chktext = '%s\n* Duplicate transition id %s.'%(
                        chktext, s_or_t.id)
                else:
                    transition_ids[s_or_t.id] = s_or_t
                
            elif s_or_t.TYPE == 'step':
                step_list.append(s_or_t)
                if not s_or_t.id:
                    chktext += '\n* Missing step id.'
                elif transition_ids.has_key(s_or_t.id) \
                         or step_ids.has_key(s_or_t.id):
                    chktext = '%s\n* Duplicate step id %s.' % (
                        chktext, s_or_t.id)
                else:
                    step_ids[s_or_t.id] = s_or_t

        steps_io = {}
        for id in step_ids.keys():
            steps_io[id] = ([],[])
        # check that transitions are pointing to existing steps
        for transition in transition_list :
            #inout = transition.in_steps + transition.out_steps
            for ref in transition.in_steps:
                if not step_ids.has_key(ref):
                    chktext = '%s\n* Bad step reference %s in transition %s' % (
                        chktext, ref, transition.id)
                else:
                    steps_io[ref][1].append(transition.id)
            for ref in transition.out_steps:
                if not step_ids.has_key(ref):
                    chktext = '%s\n* Bad step reference %s in transition %s' % (
                        chktext, ref, transition.id)
                else:
                    steps_io[ref][0].append(transition.id)
        # check start step and end step
        start_cands, end_cands = [], []
        for id in step_ids.keys():
            if len(steps_io[id][0]) == 0:
                start_cands.append(id)
            if len(steps_io[id][1]) == 0:
                end_cands.append(id)
        if len(start_cands) != 1:
            chktext += '\n* Unable to find start step (candidats:%s).'%start_cands
        else:
            self.start_step = start_cands[0]
        if len(end_cands) != 1:
            chktext += '\n* Unable to find end step (candidats:%s).'%end_cands
        else:
            self.end_step = end_cands[0]
        # check that steps are doing something
        for step in step_list :
            a_or_t = None
            if step.type == 'recipe' :
                if not self.memory.get_recipe(step.target):
                    chktext = '%s\n* Bad recipe reference %s in step %s' % \
                                   (chktext, step.target, step.id)
            elif step.type == 'action' :
                # check action exists
                a_or_t = self.memory.get_action(step.target)
                if not a_or_t:
                    chktext = '%s\n* Unknown action %s for step %s' % \
                                   (chktext, step.target, step.id)
            elif step.type == 'transform' :
                # check that transform exists
                a_or_t = self.memory.get_transform(step.target)
                if not a_or_t:
                    chktext = '%s\n* Bad transform reference %s in step %s' % \
                                   (chktext, step.target, step.id)
            else :
                chktext = chktext + '\n* ' + \
                               '%s\n* Missing action/recipe attribute for step %s' % \
                               (chktext, step.id)
            # check prototypes
            if a_or_t :
                try:
                    step.prototype.check(a_or_t.prototype)
                except PrototypeException, ex :
                    chktext = '%s\n* prototype error for step %s: %s' % \
                              (chktext, step.id, str(ex))

        # if bad, add recipe name in front
        if chktext:
            self.check = 'bad'
            self.chktext = 'Bad recipe %s: %s' % (self.name, chktext)
        else:
            self.check = 'ok'
        # return result
        return self.check, self.chktext

    def make_plan(self):
        return PlanElement(self)

    
class PlanElement(RecipeElement, StateMixin):
    """ Narval plan element: RecipeElement + running context
    """
    TYPE = 'plan'
    # State diagram Definition ################################################
    SDD = {'ready': ['running'],
           'running': ['fireable', 'failing', 'end'],
           'fireable': ['running','failing', 'end'],
           'failing': ['failed-end'],
           'failed-end': ['failed'],
           'failed': [],
           'end': ['done'],
           'done': []
           }

    def __init__(self, recipe):
        RecipeElement.__init__(self, recipe.name, recipe.group,
                               recipe.restart, recipe.decay)
        self.elements = {}
        self.trans = []
        # list for elements used by this plan 
        self.p_elements = []
        #self.elements_node = []
        self.pub_args.append('state')
        self.parent_plan = None
        self.parent_step = None
        self.transitions = {'impossible':[],'wait-step':[],'wait-condition':[],
                            'wait-time':[],'fireable':[],'history':[]}
        st_ids, tr_ids = {}, {}        
        # instantiate recipe with dynamic objects
        for id, e in recipe.elements.items():
            if e.TYPE == 'transition':
                self.elements[id] = tr_ids[id] = PlanTransition(e, self)
            elif e.TYPE == 'step':
                self.elements[id] = st_ids[id] = PlanStep(e, self)
                #/!\ was id string in recipe, object reference in plan /!\ 
                if id == recipe.start_step:
                    self.start_step = st_ids[id]
                if id == recipe.end_step:
                    self.end_step = st_ids[id]
        for id, transition in tr_ids.items():
            e = recipe.elements[id]
            in_, out, on_err = e.in_steps, e.out_steps, e.on_error
            for ref in in_:
                step = st_ids[ref]
                step.out_transitions.append(transition)
                # /!\ was id string in recipe, object reference in plan /!\ 
                transition.add_in_step(step, on_err[step.id])
            for ref in out:
                step = st_ids[ref]
                step.in_transitions.append(transition)
                transition.add_out_step(step)
        self.trans = tr_ids.values()
        self.state = 'ready' 
        self._active_steps = {}
    
    # Plan element methods #####################################################
    def set_parents(self, parents) :
        self.parent_plan, self.parent_step = parents
    
    def start(self):
        self.set_state( 'running')
        self.start_step.prepare()

    def run(self) :
        # look for transition of highest priority
        transition, priority = None, -1
        for t in self.transitions['fireable'] :
            try:
                p = t.priority
            except ValueError:
                p = 0
            if p > priority:
                transition = t
                priority = p
        assert transition, "Couldn't select transition to fire"
        # fire transition
        transition.fire()
        self.transitions['fireable'] = []


    def end(self) :
        # change state
        if self.state == 'end' :
            self.set_state( 'done')
        elif self.state == 'failed-end' :
            self.set_state( 'failed')
        # if plan has mark 'restart': restart it !
        if self.restart == 'yes' :
            planName = '%s.%s' % (self.group, self.name)
            self.memory.start_plan(planName, None, None, [])

    def get_inputs(self, input):
        """ select elements by priority """
        context = CONTEXT[input['from']]
        cands = []
        if context <= C_PLAN:
            cands = input.match_elements(self.p_elements)
            input.context = C_PLAN
        if not cands:
            if self.parent_step and context <= C_PARENT:
                cands = self.parent_step.get_inputs(input)
                input.context = C_PARENT
                if hasattr(input, 'p_level'):
                    input.p_level += 1
                else:
                    input.p_level = 1
            else:
                cands = self.memory.get_input(input)
                #cands = input.match_elements(self.memory.get_elements())
                input.context = C_MEM
        return cands

    def get_parent_level(self, element, p_level=0):
        if element in self.p_elements:
            return p_level
        elif self.parent_plan:
            return self.parent_plan.get_parent_level(element, p_level+1)
        else:
            # element in memory
            return -1
        
    def add_elements(self, elements) :
        """ If element is not already in the context of the plan, add it.
        """
        for e in difference(elements, self.p_elements):
            print 'new element in plan',e
#            print "Let me PRETTYPRINT"
#            PrettyPrint(e.as_dom())
            eid, e = self.memory.add_element(e)
            self.p_elements.append(e)
            # propagate happy news
            self.memory.plan_change(self, 'add', e)
        if self.parent_step :
            self.parent_step.outputs.append({1:elements})

    def remove_elements(self,elements):
        for e in intersection(elements, self.p_elements):
            for elt in self.p_elements: #elements_node:
                if elt.eid == e.eid:
                    print 'plan remove element', e
                    #self.elements_node.remove(elt)
                    self.p_elements.remove(e)
                    # propagate sad news
                    self.memory.plan_change(self, 'remove', e)
                    # erase
                    self.memory.delete_ref_to_element(e)
                    del elt
                    break
            else:
                print "Element eid=%s not in plan" % (e.eid)

    # ChangeListeners interfaces ##############################################
    def transition_change(self, transition):
        if transition.state == 'fireable' :
            self.transitions['fireable'].append(transition)
            self.set_state( 'fireable')
            
    def step_change(self, step):
        s = step.state
        if s == 'running':
            self._set_active(step, 'running')
        elif step is self.end_step :
            if s == 'done':
                self.set_state('end')
            elif s in ('impossible','failed','error') :
                self.set_state('failing')
        if s in ('done', 'failed','error'):
            self._set_finished(step)
        if self.state == 'failing' and not self._active_steps:
            self.set_state( 'failed-end')
                        
    def element_change(self, element):
        for node in self.trans:
            node.element_change(element)

    def get_callbacks(self, state=None):
        if self.parent_step :
            return (self.memory.plan_change, self.parent_step.plan_change)
        else:
            return (self.memory.plan_change,)

    def _set_active(self, s, state=None):
        if state is not None:
            self.set_state(state)
        assert not self._active_steps.has_key(s)
        self._active_steps[s] = 1
        
    def _set_finished(self, s):
        if self._active_steps.has_key(s):
            del self._active_steps[s]
        
# Concrete class ##############################################################
class Transition(CachedObject):
    """ Narval recipe transition element
    """
    TYPE = 'transition'
    def __init__(self, id='', priority='0'):
        CachedObject.__init__(self)
        self.id = id
        try:
            self.priority = int(priority)
        except:
            print 'Bad transition priority', priority, type(priority)
            self.priority = 0
        self.in_steps, self.out_steps = [], []
        self.on_error = {}
        # public arguments (appears in as_xml and as_dom)
        self.pub_args = ['id', 'priority']
        self.time_conditions = []
        self.conditions = []
        self.descr = ''
        
    def as_dom(self, doc=None):
        if self._dom:
            return self._dom
        if not doc:
            doc = Ddocument()
        n = doc.createElementNS(AL_NS, 'al:transition')
        for a in self.pub_args:
            n.setAttributeNS(NO_NS, a, str(getattr(self, a)))
        for id in self.in_steps:
            s = doc.createElementNS(AL_NS, 'al:in')
            s.setAttributeNS(NO_NS, 'onError', self.on_error[id])
            if not type(id) is STRING_TYPE:
                id = id.id
            s.setAttributeNS(NO_NS, 'idref', id)
            n.appendChild(s)
        for id in self.out_steps:
            s = doc.createElementNS(AL_NS, 'al:out')
            if not type(id) is STRING_TYPE:
                id = id.id
            s.setAttributeNS(NO_NS, 'idref', id)
            n.appendChild(s)
        for cond in self.conditions:
            n.appendChild(cond.as_dom(doc))
        for cond in self.time_conditions:
            s = doc.createElementNS(AL_NS, 'al:time')
            s.setAttributeNS(NO_NS, 'seconds', cond[0])
            s.setAttributeNS(NO_NS, 'minutes', cond[1])
            s.setAttributeNS(NO_NS, 'hours', cond[2])
            s.setAttributeNS(NO_NS, 'monthdays', cond[3])
            s.setAttributeNS(NO_NS, 'months', cond[4])
            s.setAttributeNS(NO_NS, 'weekdays', cond[5])
            n.appendChild(s)
        if self.descr:
            d = doc.createElementNS(AL_NS, 'al:description')
            d.appendChild(doc.createTextNode(self.descr))
            n.appendChild(d)
        self._dom = n
        return n

    def add_in_step(self, step_id, on_error='no'):
        self.on_error[step_id] = on_error
        self.in_steps.append(step_id)
        
    def add_out_step(self,step_id):
        self.out_steps.append(step_id)
        
    def remove_in_step(self, step_id):
        self.in_steps.remove(step_id)
        del self.on_errors[step_id]
        
    def remove_out_step(self,step_id):
        self.out_steps.remove(step_id)
    
class PlanTransition(Transition, StateMixin):
    """ Narval plan transition element (<-> running transition)
    """
    # State diagram Definition #################################################
    SDD = {'wait-step': ['wait-time', 'wait-condition','fireable','impossible'],
           'wait-time': ['wait-condition', 'fireable', 'impossible'],
           'wait-condition': ['fireable', 'impossible'],
           'fireable': ['fired','fail','impossible'],
           'fired': [],
           'impossible': []
           }
    def __init__(self, transition, plan):
        Transition.__init__(self, transition.id, str(transition.priority))
        self.time_conditions = transition.time_conditions
        self.conditions = []
        for cond in transition.conditions:
            clone = Condition(cloned=cond)
            self.conditions.append(clone)
            clone.owner = self
        # add specific public arguments (appears in as_xml and as_dom)
        self.pub_args.append('state')
        self.justify_table = []
        self.plan = plan
        self.init_conditions = FALSE
        self.state = 'wait-step'
        self.descr = ''
        #self.prototype.set_owner(self)

    def tagger_id(self):
        if hasattr(self, '_taggerid'):
            id = self._taggerid
        else:
            group, name = self.plan.get_identity()
            self._taggerid = id =  '.'.join((group, name, self.id))
        return id

    def fire(self) :
        # tag elements that correspond to 'use' conditions
        for condition, list in self.justify_table :
            if condition['use']:
                from tags import tag_element
                for element in list:
                    tag_element(element, self)
            # add to the plan the elements that justify the transition
            self.plan.add_elements(list)
        # set new state
        self.set_state( 'fired')
        # run out_steps
        [step.prepare(self) for step in self.out_steps]
        
    def get_justify(self) :
        return self.justify_table

    def get_inputs(self, input) :
        cands = []
        for cond, list in self.justify_table:
            cands += input.match_elements(list)
        if not cands:
            cands += input.match_elements(self.prev_steps_elements)
        return cands
    
    # ChangeListeners interfaces ###############################################
    def step_change(self, step) :
        """ Verify state of in-steps """
        if self.state == 'fired':
            return
        for step in self.in_steps:
            state = step.state
            on_error = self.on_error[step]
            # if true, transition is impossible
            if (state in ['history','failed','impossible']) \
               or (state == 'done' and on_error == 'yes') \
               or (state == 'error' and on_error != 'yes') :
                self.set_state( 'impossible')
                return
            # if true, transition may be possible later: wait
            elif (state != 'done' and on_error != 'yes') \
               or (state != 'error' and on_error == 'yes') :
                self.set_state( 'wait-step')
                return
        # in-steps ok, check time condition
        self._evaluate_time()

    def element_change(self, element) :
        if self.init_conditions and \
           self.state == 'wait-condition' :
            if self.init_conditions:
                self._evaluate_conditions_element(element)
                self._check_conditions()
 
    # time condition ###########################################################
    def time_condition_match(self):
        self.set_state( 'wait-condition')
        self._evaluate_conditions()

    def get_callbacks(self, state=None):
        l = [self.plan.memory.transition_change, self.plan.transition_change]
        for step in self.out_steps+self.in_steps :
            l.append(step.transition_change)
        return l
                
    # Private ##################################################################
    def _evaluate_time(self) :
        """Verify that *at*least*one* of the time condition holds
        """
        if self.time_conditions:
            dates = []
            for time_condition in self.time_conditions :
                sc = shallow_calendar(time_condition)
                dates.append(sc.get_next_date())
            self.set_state( 'wait-time')
            self.plan.memory.transition_wait_time(self, min(dates))
        else :
            # no time condition, proceed with other conditions
            self._evaluate_conditions()
        
    def _evaluate_conditions(self) :
        """
        Check conditions associated with the transition when all input steps.
        are done
        """
        # if not initialized yet, do it now
        if not self.init_conditions:
            self.prev_steps_elements  = []
            for s in self.in_steps :
                for step_outputs in s.get_outputs():
                    for l in step_outputs.values():
                        self.prev_steps_elements += l
            for cond in self.conditions:
                # order elements by priority
                if CONTEXT[cond['from']] <= C_STEP:
                    cond.valid_elements = cond.match_elements(self.prev_steps_elements)
                    cond.context = C_STEP
                if not cond.valid_elements:
                    cond.valid_elements = self.plan.get_inputs(cond)
            self.init_conditions = TRUE
        self._check_conditions()
        
    def _evaluate_conditions_element(self, element):
        """
        Check conditions associated with the transition when all input steps
        are done (_evaluate_conditions done) and when an element is added in
        memory
        """
        for cond in self.conditions:
            # get element context
            if element in self.plan.p_elements:
                e_context = C_PLAN
            else:
                p_level = self.plan.get_parent_level(element)
                if p_level == -1:
                    e_context = C_MEM
                else:
                    e_context = C_PARENT
                    
            # get context of elements validating the condition in previous step
            if cond.valid_elements:
                c_context = cond.context
            else:
                c_context = C_MEM
            # better context and match ?
            if e_context <= c_context and e_context > CONTEXT[cond['from']] and \
               cond.match_elements([element]):
                if e_context < c_context:
                    cond.valid_elements = [element]
                    cond.context = e_context
                else:
                    if e_context != C_PARENT:
                        cond.valid_elements.append(element)
                    elif p_level < cond.p_level:
                        cond.valid_elements = [element]
                        cond.p_level = p_level
                    elif p_level == cond.p_level:
                        cond.valid_elements.append(element)
            
    def _check_conditions(self):
        """ if at least one condition is not satisfied, return false """
        for cond in self.conditions:
            if not cond.valid_elements:
                self.set_state( 'wait-condition')
                return
        # every condition is satisfied, init table and set fireable
        for cond in self.conditions:
            cands = cond.valid_elements
            if len(cands) > 0 :
                if cond['list']:
                    self.justify_table.append((cond, cands))
                else:
                    self.justify_table.append((cond, [cands[0]]))
        self.set_state( 'fireable')


class Step(CachedObject):
    """ Narval recipe/plan step element
    """
    TYPE = 'step'
    def __init__(self, id='', type=None, target=None, foreach='', proto=None):
        CachedObject.__init__(self)
        self.id = id
        self.type = type
        self.target = target
        self.foreach = foreach
        self.label, self.descr = '', ''
        self.in_transitions, self.out_transitions = [], []
        self.arguments = ''
        # public arguments (appears in as_xml and as_dom)
        self.pub_args = ['id', 'type', 'target']
        self.prototype = proto or Prototype()
        
    def as_dom(self, doc=None):
        if self._dom:
            return self._dom
        if not doc:
            doc = Document()
        n = doc.createElementNS(AL_NS, 'al:step')
        for a in self.pub_args:
            n.setAttributeNS(NO_NS, a, getattr(self, a))
        if self.foreach:
            n.setAttributeNS(NO_NS, 'foreach', self.foreach)
        if self.label:
            l = doc.createElementNS(AL_NS, 'al:label')
            l.appendChild(doc.createTextNode(self.label))
            n.appendChild(l)
        if self.descr:
            d = doc.createElementNS(AL_NS, 'al:description')
            d.appendChild(doc.createTextNode(self.descr))
            n.appendChild(d)
        p = self.prototype.as_dom(doc)
        if p.childNodes:
            n.appendChild(p)
        if self.arguments:
            if type(self.arguments) is STRING_TYPE:
                args = ALEFImplementation.element_from_string(
                    '<al:arguments xmlns:al="%s">%s</al:arguments>' % (
                    AL_NS, self.arguments)).as_dom(doc)
            else:
                args = doc.createElementNS(AL_NS, 'al:arguments')
                for arg in self.arguments:
                    args.appendChild(arg.as_dom(doc))#.cloneNode(1))
            n.appendChild(args)
        self._dom = n
        return n

class PlanStep(Step, StateMixin):
    # State diagram Definition #################################################
    SDD = {'todo': ['ready', 'failed', 'impossible'],
           'ready': ['running'],
           'running': ['end','failed'],
           'end': ['done','failed','error'],
           'done': ['history'],
           'history': [],
           'impossible': [],
           'failed': [],
           'error': []
           }

    def  __init__(self, step, plan):
        Step.__init__(self, step.id, step.type, step.target, step.foreach,
                      step.prototype)
        # add public arguments (appears in as_xml and as_dom)
        self.pub_args.append('state')
        # convert arguments string to elements list
        if step.arguments:
            self.arguments = ALEFImplementation.elements_from_string(
                '<args>%s</args>' % step.arguments)
#            for arg in self.arguments:
#                arg.as_xml()
#                print arg._dom
        else:
            self.arguments = []
        self.plan = plan
        self.delegate = None
        self.state = 'todo'
        
        self.prototype.set_owner(self)# = step.prototype
        
    def tagger_id(self):
        if hasattr(self, '_taggerid'):
            id = self._taggerid
        else:
            group, name = self.plan.get_identity()
            self._taggerid = id =  '.'.join((group, name, self.id))
        return id
    
    # Step element methods #####################################################
    def prepare(self,transition=None) :
        self.fired_transition = transition
        if not self.delegate :
            self._make_delegate()
        self.delegate.prepare()

    def run(self) :
        if self.type == 'recipe' :
            self.delegate.run()
        else :
            self.plan.memory.create_thread(target=self.delegate.run)
        
    def end(self) :
        self.delegate.end()
        
    def get_inputs(self, input):
        """ select elements by priority """
        cands = []
        if self.arguments:
            cands = input.match_elements(self.arguments)
        if self.fired_transition and CONTEXT[input['from']] <= C_STEP:
            cands += self.fired_transition.get_inputs(input)
        if not cands:
            cands = self.plan.get_inputs(input)
        return cands
    
    def get_outputs(self):
        if hasattr(self,'outputs') and self.outputs :
            return copy.copy(self.outputs)
        else :
            return []
    
    # ChangeListener interface #################################################
    def transition_change(self,transition) :
        self._evaluate_transitions()

    def plan_change(self,plan) :
        # if child plan is end or failed, this step is end or failed
        if plan != self.plan:
            child_plan_state = plan.state
            if  child_plan_state == 'end' :
                self.set_state( 'end')
            elif child_plan_state == 'failed' :
                self.set_state( 'failed')
        
    # Private #################################################################
    def _make_delegate(self) :
        type = self.type
        assert type in ('recipe','action','transform'), 'Unknown type %s'%type
        if type == 'recipe' :
            from delegates import RecipeDelegate
            self.delegate = RecipeDelegate(self)
        elif type == 'action' :
            from delegates import ActionDelegate
            self.delegate = ActionDelegate(self)
        elif type == 'transform' :
            from delegates import TransformDelegate
            self.delegate = TransformDelegate(self)

    def _evaluate_transitions(self) :
        for transition in self.out_transitions :
            if transition.state == 'fired' and self.state == 'done':
                self.set_state( 'history')
                return
        if self.in_transitions :
            for transition in self.in_transitions :
                if transition.state != 'impossible' :
                    return
            self.set_state( 'impossible')

    def get_callbacks(self, state=None):
        if state == 'end' :
            # shortcut propagation
            return (self.plan.memory.step_change,)
        else:
            l = [self.plan.memory.step_change, self.plan.step_change]
            for transition in self.out_transitions:
                l.append(transition.step_change)
            return l
    

# Elements factory ############################################################
class ALElementsFactory:
    """ Factory for narval memory elements """
    def __init__(self):
        from readers import Reader
        self.reader = Reader()

    def element_from_dom(self, dom):
        e = self.reader.fromDOM(dom, 0)[0]
        return e
    
    def element_from_stream(self, xmlstream):
        return self.reader.fromStream(xmlstream, 0)[0]

    def element_from_string(self, xmlstring):
        return self.reader.fromString(xmlstring, 0)[0]

    def element_from_file(self, xmlfile):
        f = open(xmlfile)
        element = self.reader.fromStream(f, 0)[0]
        f.close()
        return element
    
    def elements_from_dom(self, dom):
        es = self.reader.fromDOM(dom, 1)
        return es
    
    def elements_from_stream(self, xmlstream):
        return self.reader.fromStream(xmlstream, 1)

    def elements_from_string(self, xmlstring):
        return self.reader.fromString(xmlstring, 1)
    
    def elements_from_file(self, xmlfile):
        f = open(xmlfile)
        elements = self.reader.fromStream(f, 1)
        f.close()
        return elements


# Memory factory ##############################################################
class ALMemoryFactory:
    """ Factory for memory. """

    def create_memory(self,al=None):
        from MemoryDocument import MemoryDocument
        return MemoryDocument(al)

#  ############################################################################
ALMFImplementation = ALMemoryFactory()
ALEFImplementation = ALElementsFactory()

# do not move this statement before (cyclic dependancies)
from prototype import Prototype, PrototypeException, Condition
