# Copyright (c) 2004-2005 DoCoMo Euro-Labs GmbH (Munich, Germany).
# http://www.docomolab-euro.com/ -- mailto:tarlano@docomolab-euro.com
#
# Copyright (c) 2001-2005 LOGILAB S.A. (Paris, FRANCE).
# 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
"""base classes for narval memory elements


: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: tags.py,v 1.4 2001/12/21 13:52:51 syt Exp $'

from copy import copy, deepcopy
from sets import Set

from narval import AL_NS, TYPE_NS, NO_NS
from narval.serialutils import yn_value, yn_rev_value

class NSAttribute(object):
    """namespace attribute descriptor"""
    
    def __init__(self, namespace = NO_NS, default = None,
                 loader = str, serializer = str):
        self.attrname = None
        self.namespace = namespace
        self.default = default
        self.serializer = serializer
        self.loader = loader
        self.key = None
        self.prefix = None

    def __repr__(self):
        return '<%s %s:%s>' % (self.__class__.__name__, self.namespace,
                               self.attrname)
    
    def set_name(self, attrname):
        """should only be called by the meta class"""
        self.attrname = attrname
        self.key = (self.namespace, attrname)

    def set_prefix(self, prefix):
        """set the serialization prefix if this attribute is in a xml
        namespace
        """
        self.prefix = prefix

    def as_xml(self, value, encoding='UTF-8'):
        """return the xml attribute definition for this attribute"""
        #value = self.__get__(obj, obj.__class__)
        value = self.serializer(value)
        if self.namespace is None:
            return '%s="%s"' % (self.attrname, value)
        else:
            return '%s:%s="%s"' % (self.prefix, self.attrname, value)
        
    def from_string(self, value):
        """deserialize the attribute value from a string"""
        return self.loader(value)
        
    def __get__(self, obj, objtype):
        if obj is None:
            return self # what should we do here ?
        if self.key is None:
            raise AttributeError('accessing unnamed attribute on %s' % obj)
        # return self._value ?
        try:
            return obj.getattr(self.key)
        except KeyError:
            return self.default

    def __set__(self, obj, value):
        obj.setattr(self.key, value)
        # self._value = value ?


class MetaElement(type):
    """metaclass for narval elements, preprocessing classes according to
    the value of their __namespaces__ attribute and to their
    'namespace attributes' (i.e. attributes handled by a NSAttribute
    descriptor)
    """
    
    def __new__(mcs, name, bases, classdict):
        namespaces = classdict.get('__namespaces__', {})
        ns_strings = Set()
        effective_ns = {} # this will hold the final __namespaces__ dict
        # Update final dict with each __namespaces__ definition
        for baseclass in bases:
            base_namespaces = getattr(baseclass, '__namespaces__', {})
            effective_ns.update(base_namespaces)
            ns_strings |= getattr(baseclass, 'ns_strings', Set())
        classdict['__namespaces__'] = effective_ns
        effective_ns.update(namespaces)
        # prepare descriptors
        for attrname, descr in classdict.items():
            if isinstance(descr, NSAttribute):
                descr.set_name(attrname)
                if descr.namespace:
                    try:
                        descr.set_prefix(effective_ns[descr.namespace])
                    except KeyError:
                        raise Exception('No prefix defined for namespace %s'
                                        % descr.namespace)
        # set the namespaces string used for serialization
        # FIXME: add only effectively used namespaces
        for namespace, prefix in effective_ns.items():
            ns_strings.add('xmlns:%s="%s"' % (prefix, namespace))
        classdict['ns_strings'] = ns_strings
        return type.__new__(mcs, name, bases, classdict)


class NSAttributesElement(object):
    """base class for element supporting attributes in different namespaces

    notice this class or derived should always be the left most in the inheritance
    list
    """
    __metaclass__ = MetaElement
    
    # xml element definition, REQUIRE OVERRIDING
    # (namespace, tagname)    
    __xml_element__ = (None, None)
    # mapping of defined prefixes / namespaces
    # {prefix: namespace}
    __namespaces__ = {AL_NS : 'al',
                      TYPE_NS : 'type',
                      }
    # defined by the metaclass, but here it's ok either and please pylint :)
    ns_strings = Set()
    
    def __init__(self, **kwargs):
        """kwargs is a attrname/attrvalue dict
        /!\ raises a AttributeError if a non existing attrname is given
        """
        super(NSAttributesElement, self).__init__()
##         # this attribute should be defined before the "super" call
        self._ns_attrs = {}
        existing_attrs = [descr.attrname for descr in self.ns_attributes()]
        for attrname, attrvalue in kwargs.items():
            if attrname not in existing_attrs:
                raise TypeError('Unknown attribute %r' % attrname)
            setattr(self, attrname, attrvalue)

    def ns_attributes(self):
        """return NSAttribute descriptors"""
        for attrname in dir(self.__class__):
            descr = getattr(self.__class__, attrname)
            if isinstance(descr, NSAttribute):
                yield descr
            
    def init_attrs(self, attrs):
        """init element's attributes according to the given dictionary.
        All values in the dictionary are given as unicode strings (actually as
        returned by the xml parser).

        :type attrs: dict( tuple(str, str) : unicode)
        :param attrs: dictionary of attributes to define
        """
        for descr in self.ns_attributes():
            key = descr.key
            try:
                value = attrs[key]
            except KeyError:
                continue
            self.setattr(key, descr.from_string(value))
        
    def clone_attrs(self, attrs, deep=False):
        """init element's attributes according to the given dictionary of
        preprocessed attribute (i.e. no need to deserialize)

        :type attrs: dict( tuple(str, str) : unicode)
        :param attrs: dictionary of attributes to define
        """
        self._ns_attrs = {}
        for attr_key, value in attrs.items():
            if attr_key == (AL_NS, 'eid'):
                continue
            if deep:
                self.setattr(attr_key, deepcopy(value))
            else:
                self.setattr(attr_key, value)
        
    def setattr(self, attr_key, value):
        """set an attribute to a particular name space

        :type attr_key: tuple(str, str)
        :param attr_key:
          a 2-uple with the the attribute's namespace as first element
          and the attribute's name as second element
        
        :return: the attribute's value
        """
        self._ns_attrs[attr_key] = value
        
    def getattr(self, attr_key):
        """get an attribute from a particular name space

        :type attr_key: tuple(str, str)
        :param attr_key:
          a 2-uple with the the attribute's namespace as first element
          and the attribute's name as second element
        
        :return: the attribute's value
        """
        return self._ns_attrs[attr_key]

            
    def as_xml(self, encoding='UTF-8', namespaces_def=True):
        """return the XML representation of the element.
        The default behavior handle any non container element.

        :type encoding: str
        :param encoding: the encoding to use in the returned string

        :rtype: str
        :return: XML string representing the element
        """
        if namespaces_def:
            ns_defs = ' %s' % self.namespaces_as_xml()
        else:
            ns_defs = ''
        attrs = self.attributes_as_xml()
        ns, tag = self.__xml_element__
        if not ns is None:
            tag = '%s:%s' % (self.__namespaces__[ns], tag)
        child_data = self.children_as_xml(encoding)
        if child_data:
            return '<%s%s %s>\n%s\n</%s>' % (tag, ns_defs, attrs, child_data, tag)
        return '<%s%s %s/>' % (tag, ns_defs, attrs)

    def namespaces_as_xml(self):
        """return the xml representation of namespaces/prefix definition. The
        value returned by this method is computed only once per class.
        
        :rtype: str
        :return: XML string representing the element's attributes
        """
        return ' '.join(self.ns_strings)
        
    def attributes_as_xml(self, encoding='UTF-8'):
        """return the xml representation of element's attributes
        
        :rtype: str
        :return: XML string representing the element's attributes
        """
        result = []
        # sort attributes for test...
        attrs = [(descr.key, descr) for descr in self.ns_attributes()]
        attrs.sort()
        for key, descr in attrs:
            # catch key error to serialize only defined attributes, not
            # default values
            try:
                value = self.getattr(descr.key)
            except KeyError:
                continue
            result.append(descr.as_xml(value, encoding))
        return ' '.join(result)

    def children_as_xml(self, encoding='UTF-8'):
        """return the XML representation of children in this element.

        :type encoding: str
        :param encoding: the encoding to use in the returned string

        :rtype: str
        :return: XML string representing the element
        """
        return ''

class DescriptionableMixin(object):
    """a mixin for elements supporting a description in multiple languages.

    WARNING: class using that mixin MUST add the "descriptions" field to the
    list of internal attributes

    :type descriptions: dict
    :ivar descriptions:
      dictionary of available descriptions, indexed by the language code (two
      letters)
    """
    
    def __init__(self, cloned=None, **kwargs):
        super(DescriptionableMixin, self).__init__(**kwargs)
        # dictionary of description indexed by language
        if cloned is not None:
            self.descriptions = cloned.descriptions.copy()
        else:
            self.descriptions = {}

    def description_as_xml(self, encoding='UTF-8'):
        """return a xml string for descriptions

        :type encoding: str
        :param encoding: the encoding to use in the returned string

        :rtype: str
        :return: a XML snippet string containing all available descriptions
        """
        result = []
        for lang, descr in self.descriptions.items():
            result.append('<al:description lang="%s">%s</al:description>' %
                          (lang.encode(), descr.encode(encoding)))
        return '\n'.join(result)
        

class ALElement(NSAttributesElement):
    """base class for all elements used in narval memory

    handle attributes set dynamically by narval

    :type eid: int
    :ivar eid: unique identifier of the element
    
    :type persist: bool
    :ivar persist:
      indicates whether the element should be removed from memory when it's not
      anymore referenced
    
    :type outdated: bool
    :ivar outdated:
      indicates whether the element has been outdated and should not be further
      considered
    
    :type input_id: str or None
    :ivar input_id:
      identifier of the input which has introduced this element into a step
    
    :type output_id: str or None
    :ivar output_id: 
      identifier of the output which has produced this element in a step
    
    :type from_plan: int or None
    :ivar from_plan:  eid of the plan which has produced this element
    
    :type from_step: int or None
    :ivar from_step: eid of the plan's step which has produced this element
    
    :type timestamp: float or None
    :ivar timestamp: time stamp set when the element is added to the memory
    """
    # implemented interfaces
    __implements__ = ()

    eid = NSAttribute(AL_NS, None, int, str)
    persist = NSAttribute(AL_NS, None, yn_value, yn_rev_value)
    outdated = NSAttribute(AL_NS, None, yn_value, yn_rev_value)
    input_id = NSAttribute(AL_NS, None, str, str)
    output_id = NSAttribute(AL_NS, None, str, str)
    from_plan = NSAttribute(AL_NS, None, int, str)
    from_step = NSAttribute(AL_NS, None, str, str)
    timestamp = NSAttribute(AL_NS, None, float, str)
    name = NSAttribute(TYPE_NS, None, str, str)

    # handler used to handle child elements when deserializing
    # should implements necessary methods of IXMLHandler
    __child_handler__ = None
    
    def __repr__(self):
        return '<%s eid=%s at %s>' % (self.__class__.__name__,
                                        self.eid, hex(abs(id(self))))
    
    def reset_runtime_attributes(self):
        """reset all runtime attributes (ie attributes in the AL_NS namespace)
        """
        for descr in self.ns_attributes():
            if descr.namespace == AL_NS:
                self.setattr(descr.key, None)

    def clone(self):
        """return a clone of this element"""
        elmt = copy(self)
        elmt._ns_attrs = self._ns_attrs.copy()
        elmt.eid = None
        return elmt

    def display_type(self):
        """return a string used to display this element's type"""
        return self.__class__.__xml_element__[1]

    def display_group(self):
        """return a string used to display this element's group"""
        return getattr(self, 'group', 'element')

    def display_name(self):
        """return a string used to display this element's name"""
        return getattr(self, 'name', '')
    
    def display_data(self):
        return (self.eid, self.display_type(),
                self.display_group(), self.display_name())
