# 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.
""" delegates for step execution """

__revision__ = "$Id: delegates.py,v 1.31 2002/02/22 17:30:10 alf Exp $"

import copy, time
from narval import bibal
from narval.lib import *
from narval.dom import Document, ELEMENT_NODE
from ALAbstraction import ALEFImplementation
from tags import tag_element

def fromtag(element, step) :
    element.from_plan = step.plan.eid
    element.from_step = step.id

# Exceptions ##################################################################
class DelegateException(Exception) :
    """raised when delegate faces problem"""

    def __init__(self,list) :
        self.list = list

class BadInput(DelegateException) :
    """problem with inputs in prepare()"""
    pass

class BadOutput(DelegateException) :
    """problem with outputs in end()"""
    pass

class ErrorOutput(DelegateException) :
    """found <error/> in outputs"""
    pass


# Delegate interface ###########################################################
class Delegate:
    """Handles the preparation, execution and termination of a step"""

    def __init__(self,step) :
        self.step = step
        self.target = None
        
    def prepare(self) :
        pass

    def run(self) :
        pass
    
    def end(self) :
        pass


# Recipe Delegate ##############################################################
class RecipeDelegate(Delegate) :
    """recipe delegate"""
    
    def prepare(self):
        STEP = self.step
        MEM = STEP.plan.memory
        STEP.set_state('ready')
        self.recipe = recipe = MEM.get_recipe(STEP.target)
        if not recipe:
            err = MEM.mk_error('Unknown recipe %s', STEP.target)
            fromtag(err, STEP)
            STEP.plan.add_elements([err])
            STEP.set_state( 'failed')
        self.recipe = MEM.check_date(recipe)
        
    def run(self):
        STEP = self.step
        STEP.outputs = []        
        MEM = STEP.plan.memory
        STEP.set_state( 'running')
        name = STEP.target
        code, msg = self.recipe.check_syntax()
        if msg:
            err = MEM.mk_error('bad recipe',msg)
            fromtag(err, STEP)
            STEP.plan.add_elements([err])
            STEP.set_state( 'failed')
        else:
            MEM.start_plan(name, STEP.plan, STEP, [])
        
    def end(self) :
        self.step.set_state('done')


# Action/Transform Delegate ####################################################
class ATDelegate(Delegate) :
    """
    Abstract class, defines methods common to Action and Transform delegates
    """

    def _get_target(self) :
        raise Exception('This method has to be overridden')
    
    def _execute_target(self,input=None, input_d=None) :
        raise Exception('This method has to be overridden')

    def prepare(self) :
        STEP = self.step
        self.target = self._get_target()
        # prototype
        try:
            STEP.prototype.prepare(self.target.prototype)
        except Exception, ex:
            import traceback
            traceback.print_exc()
            STEP.set_state( 'failed')
            return
        # inputs
        try:
            STEP.inputs = self.retreive_inputs()
            STEP.set_state( 'ready')
        except BadInput, ex :
            for type, msg in ex.list :
                err = STEP.plan.memory.mk_error(type,msg)
                fromtag(err,STEP)
                STEP.plan.add_elements([err])
            STEP.set_state( 'failed')

    def retreive_inputs(self):
        STEP = self.step
        selected_inputs = {}
        foreach_f = STEP.foreach
        foreach_input = None
        errors = []
        # select inputs
        for input in STEP.prototype.get_input_proto():
            list, use, optional = input['list'], input['use'], input['optional']
            if input['match']: cands = STEP.get_inputs(input)
            else: cands = []
            # look for map, fail if imprecise
            if foreach_f and input['id'] == foreach_f:
                foreach_input = input
                if list:
                    errors.append(('bad foreach','applies to a list'))
                else:
                    list = 1
            # select
            nb = len(cands)
            if (nb == 1) or (nb > 0 and list) :
                selected_inputs[input] = cands
                if use:
                    for e in cands:
                        tag_element(e, self.step)
            else :
                if optional :
                    selected_inputs[input] = []
                elif list :
                    errors.append( ('missing input', "Missing mandatory input %s" % (
                        [m.as_xml() for m in input['match']])) )
                elif nb:
                    msg = "%d inputs instead of one with prototype= %s. \
Candidates are %s" % (nb, str(input), [e.as_xml(get_encoding()) for e in cands])
                    errors.append( ('undecidable input',msg) )
                else:
                    errors.append(('missing input',
                                   "No input found with prototype=%s" % input))
        
        if foreach_f and not foreach_input:
            errors.append(('bad foreach', 'no input with id %s'%foreach_f))
        # fail on errors
        if errors:
            raise BadInput(errors)
        STEP.input_foreach = foreach_input
        # ready to run
        return selected_inputs
        
    # run ######################################################################

    def run(self) :
        STEP = self.step
        PLAN = STEP.plan
        STEP.set_state( 'running')

        input_sets = []
        output_sets = []

        # make input sets
        # FIXME: need copy ?
        if STEP.input_foreach :
            for e in STEP.inputs[STEP.input_foreach] :
                input_set = copy.copy(STEP.inputs)
                input_set[STEP.input_foreach] = [e]
                input_sets.append(input_set)
        else :
            input_sets.append(copy.copy(STEP.inputs))

        # execute
        for input_set in input_sets :
            # add elements to plan context
            for elements_list in input_set.values():
                PLAN.add_elements(elements_list)
            output_sets.append( (input_set, self._execute(input_set)) )
        # done
        STEP.output_sets = output_sets
        STEP.set_state( 'end')

        
    def _execute(self, input_set):
        try:
            # get output and delete args
            output = self._execute_target(input_set)
        except : # catch anything
            import traceback, sys
            traceback.print_exc()
            plan = self.step.plan
            print 'Exception in delegates for plan %s (%s) step %s:'%(
                plan.name, plan.eid, self.step.id)
            error = bibal.mk_traceback_error(plan, sys.exc_info())
            output = [error]
        return output

    # end ######################################################################

    def end(self) :
        STEP = self.step
        STEP.outputs = []        
        final_state = 'done'
        
        # check output
        for input_set, outputs in STEP.output_sets :
            try:
                if outputs is None:
                    raise BadOutput(['Not a tree'])
                self._check_output_is_correct(outputs)
                output_ok = self._match_output(outputs)
                self._accept_output(input_set, output_ok)
                STEP.outputs.append(output_ok)
            except ErrorOutput, ex :
                for error in ex.list :
                    fromtag(error, STEP)
                    STEP.plan.add_elements([error])
                if final_state != 'failed' :
                    final_state = 'error'

            except BadOutput, ex :
                msg = 'Action %s at step %s returned bad output: %s\n' % (
                    STEP.target, STEP.id, ex.list)
                for txt in ex.list :
                    msg = '%s\n---\n%s'% (msg, unicode(txt, get_encoding()))
                error = STEP.plan.memory.mk_error('bad output', msg)
                STEP.plan.add_elements([error])
                final_state = 'failed'
        STEP.set_state( final_state)


    def _check_output_is_correct(self, outputs) :
        errors = []
        # remove 'eid', check for errors
        for o in outputs:
            o.eid = None
            if o.name == 'error' and o.as_dom().namespaceURI == AL_NS:
                errors.append(o)
        if errors :
            raise ErrorOutput(errors)


    def _match_output(self, outputs) :
        STEP = self.step
        problems = []
        encod = get_encoding()
        ouput_p_list = STEP.prototype.get_output_proto()
        # try to match output elements to prototype
        match_outputs = {}
        for output_p in ouput_p_list:
            cands = output_p.match_elements(outputs)
            for cand in cands:
                match_outputs.setdefault(cand, []).append(output_p)
        # prepare output_ok
        output_ok = {}
        for output_p in ouput_p_list:
            output_ok[output_p] = []
        for element in outputs:
            if match_outputs.has_key(element) and len(match_outputs[element]) == 1:
                output_ok[match_outputs[element][0]].append(element)
            else :
                problems.append("Can't match output %s" % (element.as_xml(encod)))            
        # check that number of outputs is correct
        for output, elements in output_ok.items():
            if len(elements) == 0 and not output['optional']:
                problems.append("Missing output for %s" % output.as_xml(encod))
            elif len(elements) > 1 and not output['list'] :
                problems.append("Too many outputs. Got %s expected %s" \
                                   % ([e.as_xml(encod) for e in elements],
                                      output.as_xml(encod)))
        if problems :
            raise BadOutput( problems )
        return output_ok


    def _accept_output(self, input_set, output_ok) :
        STEP = self.step

        for output, element_list in output_ok.items() :
            imported_elements = element_list[:]
            output_ok[output] = imported_elements
            # tag
            for element in imported_elements :
                fromtag(element, STEP)
            # outdates
            outdates = output['outdates']
            if outdates != '' :
                for input, elements in input_set.items() :
                    if input['id'] == outdates :
                        for e in elements :
                            e.outdated ='yes'
            # add to plan
            STEP.plan.add_elements(imported_elements)


## Action Delegate ############################################################

class ActionDelegate(ATDelegate) :
    """Handles the execution of an action on behalf of a step"""

    def _get_target(self) :
        # get action prototype
        STEP = self.step 
        MEM = STEP.plan.memory
        return MEM.check_date(MEM.get_action(STEP.target))
    
    def _execute_target(self, input_set) :
        args_d = {}
        doc = Document()
        for input, element_list in input_set.items() :
            id, list = input['id'], input['list']
            if list:
                args_d[id] = []
                for element in element_list:
                    element.input_id = id
                    #args_d[id].append(element.as_dom())
                    args_d[id].append(doc.importNode(element.as_dom(), 1))
            elif element_list:
                element = element_list[0]
                element.input_id = id
                #args_d[id] = element.as_dom()
                args_d[id] = doc.importNode(element.as_dom(), 1)
            else:
                args_d[id] = None
        moduleName = self.target.group
        actionFunc = self.target.func
        # FIXME
        exec('from modules.%s import %s' %(moduleName,actionFunc))
        f = eval(actionFunc)
        #f = dyn_import(moduleName, actionFunc)
        output = f(args_d, doc)
        if output is None:
            return None
        out_l = []
        for l in output.values():
            # is "l" a list or a node ?
            if hasattr(l, 'ownerDocument'):
                out_l.append(ALEFImplementation.element_from_dom(l))
            else:
                for n in l:
                    out_l.append(ALEFImplementation.element_from_dom(n))
        return out_l
                

            
## Transform Delegate #########################################################
class TransformDelegate(ATDelegate) :
    """Handles the execution of a transformation on behalf of a step"""

    def _get_target(self) :
        # get transform prototype
        STEP = self.step
        MEM = STEP.plan.memory
        return MEM.check_date(MEM.get_transform(STEP.target))
    
    def _execute_target(self, input_set) :
        from cStringIO import StringIO
        t = time.localtime(time.time())
        # make args tree
        buf = StringIO()
#        print '%'*80
#        print 'TRANSFORM'
        buf.write('<al:arguments xmlns:al="%s">' % (AL_NS))
        for input, element_list in input_set.items() :
            for element in element_list :
                element.input_id = input['id']
                buf.write(element.as_xml())
        buf.write('</al:arguments>')
        from narval.dom import DomReader
        r = DomReader()
#        print buf.getvalue()
        buf.seek(0)
        try:
            args = r.fromStream(buf).documentElement
        except:
            f = open('/tmp/narval_error','w')
            f.write(buf.getvalue())
            raise
        # ALEFImplementation.element_from_stream(buf)
        buf.close()
        outputs = []
#        from xml.dom.ext.Dom2Sax import check_dom
        #from narval.lib import PrettyPrint
        for e in self.target.transform(args, Document()):
            #print 'TR RESULT'
            #PrettyPrint(e)
            #print e.nodeType, ELEMENT_NODE
            if e.nodeType == ELEMENT_NODE:
                #check_dom(e)
                outputs.append(ALEFImplementation.element_from_dom(e))
        return outputs
            
