# 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.
__revision__ = "$Id: prototype.py,v 1.18 2001/12/21 13:52:51 syt Exp $"

"""  Models steps/actions/transforms prototypes. """

from narval.lib import select_many, XMLNS_NS, XSL_NS, AL_NS, NO_NS, TRUE, FALSE
from tags import is_tagged
from ALAbstraction import CachedObject
from narval.dom import Document
from narval.utils.misc import value, rev_value


class PrototypeException(Exception) :
    pass


## match object ###############################################################
class Match(CachedObject):

    def __init__(self, match, ns_context):
        assert match, 'MATCH required'
        CachedObject.__init__(self)
        self.match = match
        self.nsm = nsm = {'al':AL_NS, 'xsl':XSL_NS}
        for uri, prefix in ns_context.items():
            if not (uri == XMLNS_NS and prefix == 'xmlns'):
                nsm[prefix] = uri

    def as_dom(self, doc=None):
        if self._dom:
            return self._dom
        if not doc:
            doc = Document()
        mn = doc.createElementNS(AL_NS, 'al:match')
        mn.appendChild(doc.createTextNode(self.match))
        for prefix, uri in self.nsm.items():
            if not (uri == AL_NS and prefix == 'al'):
                mn.setAttributeNS(XMLNS_NS, prefix, uri)
        self._dom = mn
        return mn

    
## prototype input/output/condition ###########################################
class PrototypeEntry(CachedObject):
    """ A prototype entry (<input> or <output>). """

    def __init__(self, cloned=None):
        CachedObject.__init__(self)
        if cloned:
            self._data = {}
            for key, val in cloned._data.items():
                if key == 'match':
                    self._data[key] = val[:]
                else:
                    self._data[key] = val
        self.__str__ = self.as_xml
        self.descr = ''
        
    def __getitem__(self, key):
        return self._data[key]

    def __setitem__(self, key, val):
        self._data[key] = val
        
    def match_elements(self, elements):
        match_e = []
        use = self['use']
        for element in elements:
            if (element.outdated != 'yes') and (
                (not use) or (use and not is_tagged(element, self.owner))):
                for match in self._data['match']:
                    if not select_many(element.as_dom(), 'self::%s'%match.match,
                                       nss=match.nsm):
                        break
                else:
                    match_e.append(element)
        return match_e

    def as_dom(self, doc=None):
        if self._dom:
            return self._dom
        name = 'al:%s' % self.ROLE
        if not doc:
            doc = Document()
        d = self._data
        n = doc.createElementNS(AL_NS, name)
        if rev_value(d['list']):
            n.setAttributeNS(NO_NS, 'list', rev_value(d['list']))
        if self.descr:
            de = doc.createElementNS(AL_NS, 'al:description')
            de.appendChild(doc.createTextNode(self.descr))
            n.appendChild(de)
        for match_o in d['match']:
            n.appendChild(match_o.as_dom(doc))
        self._dom = n
        return n


class Condition(PrototypeEntry):
    ROLE = 'condition'
    def __init__(self, use='no', list='no', from_='', cloned=None):
        PrototypeEntry.__init__(self, cloned=cloned)
        if not cloned:
            self._data = {'list' : value(list),
                          'use' : value(use),
                          'from' : from_,
                          'match' : []}
        self.valid_elements = []
        
##     def match_elements(self, elements):
##         match_e = self.valid_elements
##         use = self['use']
##         for element in elements:
##             if element.outdated != 'yes':
##                 for match in self._data['match']:
##                     if not select_many(element.as_dom(), 'self::%s'%match.match,
##                                        nss=match.nsm):
##                         break
##                 else:
##                     match_e.append(element)
##         return match_e

    def as_dom(self, doc=None):
        n = PrototypeEntry.as_dom(self, doc)
        d = self._data
        n.setAttributeNS(NO_NS, 'use',  rev_value(d['use']))
        n.setAttributeNS(NO_NS, 'list', rev_value(d['list']))
        n.setAttributeNS(NO_NS, 'from', d['from'])
        return n


class InputEntry(PrototypeEntry):
    ROLE = 'input'
    def __init__(self, id='', optional='no', list='no', use='no',
                 from_='', cloned=None):
        PrototypeEntry.__init__(self, cloned=cloned)
        if not cloned:
            assert id, 'id attribute is required for a PrototypeEntry'
            self._data = {'id' : id,
                          'optional' : value(optional),
                          'list' : value(list),
                          'use' : value(use),
                          'from' : from_,
                          'match' : []}
            
    def as_dom(self, doc=None):
        n = PrototypeEntry.as_dom(self, doc)
        d = self._data
        n.setAttributeNS(NO_NS, 'id', d['id'])
        if rev_value(d['optional']):
            n.setAttributeNS(NO_NS, 'optional', rev_value(d['optional']))
        if rev_value(d['use']):
            n.setAttributeNS(NO_NS, 'use', rev_value(d['use']))
        if d['from']:
            n.setAttributeNS(NO_NS, 'from', d['from'])
        return n

    
class OutputEntry(PrototypeEntry):
    ROLE = 'output'
    def __init__(self, id='', optional='no', list='no',
                 outdates='',  cloned=None):
        PrototypeEntry.__init__(self, cloned=cloned)
        if not cloned:
            assert id, 'id attribute is required for a PrototypeEntry'
            self._data = {'id' : id,
                         'optional' : value(optional),
                         'list' : value(list),
                         'outdates' : outdates,
                         'match' : []}

    def match_elements(self, elements):
        match_e = []
        for element in elements:
            for match in self._data['match']:
                if not select_many(element.as_dom(), 'self::%s'%match.match,
                                   nss=match.nsm):
                    break
            else:
                match_e.append(element)
        return match_e

    def as_dom(self, doc=None):
        n = PrototypeEntry.as_dom(self, doc)
        d = self._data
        n.setAttributeNS(NO_NS, 'id', d['id'])
        if rev_value(d['optional']):
            n.setAttributeNS(NO_NS, 'optional', rev_value(d['optional']))
        if d['outdates']:
            n.setAttributeNS(NO_NS, 'outdates', d['outdates'])
        return n
    
    
## full step/action/transform prototype object ################################
class Prototype(CachedObject):
    """A step/action/transform prototype is a list of entries."""

    def __init__(self) :
        CachedObject.__init__(self)
        self._inputs, self._outputs = [], []
        self.overload = None
        self.in_mixed, self.out_mixed = None, None
        
    def set_owner(self, owner) :
        self.owner = owner
        for i in self._inputs:
            i.owner = owner

    def prepare(self, overloaded):
        self.overload = overloaded

    def add_in_entry(self, id, optional='no', list='no', use='no', from_=''):
        d = InputEntry(id, optional, list, use=use, from_=from_)
        self._inputs.append(d)
        return d

    def add_out_entry(self, id, optional='no', list='no', outdates='') :
        d = OutputEntry(id, optional, list, outdates=outdates)
        self._outputs.append(d)
        return d

    def get_input_proto(self):
        if self.in_mixed:
            return self.in_mixed
        if self.overload:
            newlist = _merge_input(self.overload._inputs, self._inputs)
            self.in_mixed = newlist
            for i in newlist:
                i.owner =self.owner

        else:
            newlist = [e for e in self._inputs if e['match'] or not e['optional']]
        return newlist

    def get_output_proto(self):
        if self.out_mixed:
            return self.out_mixed
        if self.overload:
            newlist = _merge_output(self.overload._outputs, self._outputs)
            self.out_mixed = newlist
        else:
            newlist = [e for e in self._outputs if e['match'] or not e['optional']]
        return newlist
        
    def check(self, overloaded):
        input, output = overloaded._inputs, overloaded._outputs
        for n in self._inputs:
            self._check_entry(input, n)
        for n in self._outputs:
            self._check_entry(output, n)
        chktext = _check_entries(_merge_input(input, self._inputs))
        chktext = '%s%s' % (
            chktext,  _check_entries(_merge_output(output, self._outputs)))
        if chktext:
            raise PrototypeException(chktext)
    
    def as_dom(self, doc=None):
        if self._dom:
            return self._dom
        if not doc:
            doc = Document()
        docf = doc.createDocumentFragment()
        for e in self._inputs + self._outputs:
            docf.appendChild(e.as_dom(doc))
        self._dom = docf
        return docf
                     
    def _check_entry(self, entries, entry) :
        for old in entries :
            if old['id'] == entry['id'] :
                break
        else :
            raise PrototypeException('ID %s not found' % (entry['id']))

def _check_entries(entries):
    chktext, id_dict = '', {}
    for entry in entries:
        if not entry['match'] and not entry['optional']:
            chktext = '%s\n* input prototype error: no match for %s'%\
                      (chktext, entry['id'])
        if id_dict.has_key(entry['id']):
            chktext = '%s\n* input prototype error: duplicate id %s'%\
                      (chktext, entry['id'])
        else:
            id_dict[entry['id']] = 1
    return chktext

        
def _merge_input(target_proto, step_proto) :
    """ merge step prototype with target prototype """
    newlist = []
    for old in target_proto:
        for new in step_proto:
            if old['id'] == new['id'] :
                data = InputEntry(cloned=old)
                if old['optional'] and new['optional'] == 0 :
                    data['optional'] = 0
                if old['list'] and new['list'] == 0 :
                    data['list'] = 0
                if not old['use'] and new['use'] == 1 :
                    data['use'] = 1
                for match_o in new['match']:
                    if match_o.match not in [m.match for m in old['match']]:
                        data['match'].append(match_o)
                #if data['match'] or not data['optional']:
                newlist.append(data)
                old['from'] = new['from']
                break
        else:
            #if old['match'] or not old['optional']:
            newlist.append(old)
    return newlist

def _merge_output(targetproto, stepproto) :
    """ merge step prototype with target prototype """
    newlist = []
    for old in targetproto:
        for new in stepproto:
            if old['id'] == new['id'] :
                data = OutputEntry(cloned=old)
                if old['optional'] and new['optional'] == 0 :
                    data['optional'] = 0
                if old['list'] and new['list'] == 0 :
                    data['list'] = 0
                if not old['outdates'] and new['outdates'] :
                    data['outdates'] = new['outdates']
                for match_o in new['match']:
                    if match_o.match not in [m.match for m in old['match']]:
                        data['match'].append(match_o)
                if data['match'] or not data['optional']:
                    newlist.append(data)
                break
        else:
            if old['match'] or not old['optional']:
                newlist.append(old)
    return newlist


