# copyright 2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact 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 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 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""cubicweb-seda adapter classes for profile (schema) generation"""

from collections import defaultdict, namedtuple

from six import text_type, string_types

from lxml import etree
from pyxst.xml_struct import graph_nodes

from logilab.common import attrdict

from cubicweb.predicates import is_instance
from cubicweb.view import EntityAdapter

from cubes.seda.xsd import BASE_TYPES, XSDM_MAPPING, JUMP_ELEMENTS
from cubes.seda.xsd2yams import SKIP_ATTRS
from cubes.seda.entities import simplified_profile


JUMPED_OPTIONAL_ELEMENTS = set(('FileInfo', 'PhysicalDimensions', 'Coverage'))


def substitute_xml_prefix(prefix_name, namespaces):
    """Given an XML prefixed name in the form `'ns:name'`, return the string `'{<ns_uri>}name'`
    where `<ns_uri>` is the URI for the namespace prefix found in `namespaces`.

    This new string is then suitable to build an LXML etree.Element object.

    Example::

      >>> substitude_xml_prefix('xlink:href', {'xlink': 'http://wwww.w3.org/1999/xlink'})
      '{http://www.w3.org/1999/xlink}href'

    """
    try:
        prefix, name = prefix_name.split(':', 1)
    except ValueError:
        return prefix_name
    assert prefix in namespaces, 'Unknown namespace prefix: {0}'.format(prefix)
    return '{{{0}}}'.format(namespaces[prefix]) + name


def content_types(content_type):
    """Return an ordered tuple of content types from pyxst `textual_content_type` that may be None, a
    set or a string value.
    """
    if content_type:
        if isinstance(content_type, set):
            content_types = sorted(content_type)
        else:
            content_types = (content_type,)
    else:
        content_types = ()
    return content_types


def _concept_value(concept, language):
    """Return string value to be inserted in a SEDA export for the given concept.

    * `concept` may be None, in which case None will be returned

    * `language` is the language matching the exported format (one of 'seda-2', 'seda-1' or
      'seda-02')
    """
    assert language in ('seda-2', 'seda-1', 'seda-02')
    if concept is None:
        return None
    for code in (language, 'seda', 'en'):
        try:
            return concept.labels[code]
        except KeyError:
            continue
    raise RuntimeError('Concept %s has no preferred label in one of %s language'
                       % (language, 'seda', 'en'))


def serialize(value):
    """Return typed `value` as an XSD string."""
    if value is None:
        return None
    if hasattr(value, 'eid'):
        if value.cw_etype == 'ConceptScheme':
            return value.absolute_url()
        if value.cw_etype == 'Concept':
            return _concept_value(value, 'seda-2')
        if hasattr(value, 'id'):
            # value is something in the profile which has a id (e.g. archive unit, data object)
            return value.id
        return None  # intermediary entity
    if isinstance(value, bool):
        return 'true' if value else 'false'
    assert isinstance(value, string_types), repr(value)
    return text_type(value)


def minmax_cardinality(string_cardinality, _allowed=('0..1', '0..n', '1', '1..n')):
    """Return (minimum, maximum) cardinality for the cardinality as string (one of '0..1', '0..n',
    '1' or '1..n').
    """
    assert string_cardinality in _allowed
    if string_cardinality[0] == '0':
        minimum = 0
    else:
        minimum = 1
    if string_cardinality[-1] == 'n':
        maximum = graph_nodes.INFINITY
    else:
        maximum = 1
    return minimum, maximum


def element_minmax_cardinality(occ, card_entity):
    """Return (minimum, maximum) cardinality for the given pyxst Occurence and entity.

    Occurence 's cardinality may be overriden by the entity's user_cardinality value.
    """
    cardinality = getattr(card_entity, 'user_cardinality', None)
    if cardinality is None:
        return occ.minimum, occ.maximum
    else:
        return minmax_cardinality(cardinality)


def attribute_minimum_cardinality(occ, card_entity):
    """Return 0 or 1 for the given pyxst attribute's Occurence. Cardinality may be overriden by
    the data model's user_cardinality value.
    """
    cardinality = getattr(card_entity, 'user_cardinality', None)
    if cardinality is None:
        return occ.minimum
    else:
        return minmax_cardinality(cardinality, ('0..1', '1'))[0]


def iter_path_children(xselement, entity):
    """Return an iterator on `entity` children entities according to `xselement` definition.

    (`path`, `target`) is returned with `path` the path definition leading to the target, and
    `target` either a final value in case of attributes or a list of entities.
    """
    for rtype, role, _path in XSDM_MAPPING.iter_rtype_role(xselement.local_name):
        if _path[0][2] in BASE_TYPES:
            # entity attribute
            if getattr(entity, rtype) is not None:
                yield _path, getattr(entity, rtype)
        else:
            related = entity.related(rtype, role, entities=True)
            if related:
                yield _path, related


class RNGMixin(object):
    """Mixin class providing some Relax NG schema generation helper methods."""

    def rng_element_parent(self, parent, minimum, maximum=1):
        """Given a etree node and minimum/maximum cardinalities of a desired child element,
        return suitable parent node for it.

        This will be one of rng:optional, rng:zeroOrMore or rng:oneOrMore that will be created by
        this method or the given parent itself if minimum == maximum == 1.
        """
        if minimum == 1 and maximum == 1:
            return parent
        elif minimum == 0 and maximum == 1:
            return self.element('rng:optional', parent)
        elif minimum == 0 and maximum == graph_nodes.INFINITY:
            return self.element('rng:zeroOrMore', parent)
        elif minimum == 1 and maximum == graph_nodes.INFINITY:
            return self.element('rng:oneOrMore', parent)
        else:
            assert False, ('unexpected min/max cardinality:', minimum, maximum)

    def rng_attribute_parent(self, parent, minimum):
        """Given a etree node and minimum cardinality of a desired attribute,
        return suitable parent node for it.

        This will be rng:optional that will be created by this method or the given parent itself if
        minimum == 1.
        """
        if minimum == 1:
            return parent
        else:
            return self.element('rng:optional', parent)

    def rng_value(self, element, qualified_datatype, fixed_value=None):
        """Given a (etree) schema element, a data type (e.g. 'xsd:token') and an optional fixed
        value, add RNG declaration to the element to declare the datatype and fix the value if
        necessary.
        """
        prefix, datatype = qualified_datatype.split(':')
        type_attrs = {'type': datatype}
        if prefix != 'xsd':
            type_attrs['datatypeLibrary'] = self.namespaces[prefix]
        if fixed_value is not None:
            self.element('rng:value', element, type_attrs, text=fixed_value)
        else:
            self.element('rng:data', element, type_attrs)


class SEDA2ExportAdapter(EntityAdapter):
    """Abstract base class for export of SEDA profile."""
    __abstract__ = True
    __select__ = is_instance('SEDAArchiveTransfer')
    encoding = 'utf-8'
    content_type = 'application/xml'
    # to be defined in concret implementations
    extension = None
    namespaces = {}
    root_attributes = {}

    @property
    def file_name(self):
        """Return a file name for the dump"""
        return '%s.%s' % (self.entity.dc_title(), self.extension)

    def dump(self):
        """Return an schema string for the adapted SEDA profile."""
        root = self.dump_etree()
        return etree.tostring(root, encoding=self.encoding, pretty_print=True, standalone=False)

    def dump_etree(self):
        """Return an XSD etree for the adapted SEDA profile."""
        raise NotImplementedError()

    def qname(self, tag):
        return substitute_xml_prefix(tag, self.namespaces)

    def element(self, tag, parent=None, attributes=None, text=None):
        """Generic method to build a XSD element tag.

        Params:

        * `tag`, tag name of the element

        * `parent`, the parent etree node

        * `attributes`, dictionary of attributes - may contain a special 'documentation' attribute
          that will be added in a xsd:annotation node

        * `text`, textual content of the tag if any
        """
        attributes = attributes or {}
        tag = self.qname(tag)
        documentation = attributes.pop('documentation', None)
        for attr, value in attributes.items():
            newattr = substitute_xml_prefix(attr, self.namespaces)
            attributes[newattr] = value
            if newattr != attr:
                attributes.pop(attr)
        if parent is None:
            elt = etree.Element(tag, attributes, nsmap=self.namespaces)
        else:
            elt = etree.SubElement(parent, tag, attributes)
        if text is not None:
            elt.text = text
        if documentation:
            annot = self.element('xsd:annotation', elt)
            self.element('xsd:documentation', annot).text = documentation
        return elt

    def dispatch_occ(self, profile_element, occ, target_value, to_process, card_entity):
        callback = getattr(self, 'element_' + occ.target.__class__.__name__.lower())
        callback(occ, profile_element, target_value, to_process, card_entity)

    def _dump(self, root):
        entity = self.entity
        xselement = XSDM_MAPPING.root_xselement
        transfer_element = self.init_transfer_element(xselement, root, entity)
        to_process = defaultdict(list)
        to_process[xselement].append((entity, transfer_element))
        # first round to ensure we have necessary basic structure
        for xselement, etype, child_defs in XSDM_MAPPING:
            # print 'PROCESS', getattr(xselement, 'local_name', xselement.__class__.__name__), etype
            for entity, profile_element in to_process.pop(xselement, ()):
                assert etype == entity.cw_etype
                self._process(entity, profile_element, child_defs, to_process)
        # then process remaining elements
        # print 'STARTING ROUND 2'
        while to_process:
            xselement = next(iter(to_process))
            entities_profiles = to_process.pop(xselement, ())
            if entities_profiles:
                try:
                    etype, child_defs = XSDM_MAPPING[xselement]
                except KeyError:
                    # element has no children
                    continue
                for entity, profile_element in entities_profiles:
                    assert etype == entity.cw_etype
                    self._process(entity, profile_element, child_defs, to_process)

        assert not to_process, to_process

    def _process(self, entity, profile_element, child_defs, to_process):
        for occ, path in child_defs:
            # print '  child', getattr(occ.target, 'local_name', occ.target.__class__.__name__), \
            #    [x[:-1] for x in path]
            if not path:
                assert not isinstance(occ.target, graph_nodes.XMLAttribute)
                assert occ.target.local_name in JUMP_ELEMENTS, occ.target
                if not any(iter_path_children(occ.target, entity)):
                    # element has no children, skip it
                    continue
                if occ.target.local_name in JUMPED_OPTIONAL_ELEMENTS:
                    # elements in JUMPED_OPTIONAL_ELEMENTS are jumped but have optional cardinality,
                    # so search in all it's child element, and mark it as mandatory if one of them
                    # is mandatory, else keep it optional
                    cardinality = '0..1'
                    for _path, target in iter_path_children(occ.target, entity):
                        if _path[0][2] in BASE_TYPES:
                            # special case of a mandatory attribute: parent element will be
                            # mandatory if some value is specified, else that's fine
                            if target is not None:
                                cardinality = '1'
                                break
                        elif any(te.user_cardinality == '1' for te in target):
                            cardinality = '1'
                            break
                else:
                    cardinality = None
                # jumped element: give None as target_value but register the generated element for
                # later processing
                self.dispatch_occ(profile_element, occ, None, to_process,
                                  card_entity=attrdict({'user_cardinality': cardinality}))
                to_process[occ.target].append((entity, self.jumped_element(profile_element)))
            else:
                # print '  values', _path_target_values(entity, path)
                for card_entity, target_value in _path_target_values(entity, path):
                    self.dispatch_occ(profile_element, occ, target_value, to_process,
                                      card_entity=card_entity)

    def init_transfer_element(self, xselement, root, entity):
        """Initialize and return the XML element holding the ArchiveTransfer definition, as well as
        any other necessary global definitions.
        """
        raise NotImplementedError()

    def jumped_element(self, profile_element):
        """Return the last generated element, for insertion of its content."""
        raise NotImplementedError()


class SEDA2XSDExport(SEDA2ExportAdapter):
    """Adapter to build an XSD representation of a SEDA profile, using SEDA 2.0 specification."""
    __regid__ = 'SEDA-2.0.xsd'
    extension = 'xsd'
    namespaces = {
        None: 'fr:gouv:culture:archivesdefrance:seda:v2.0',
        'seda': 'fr:gouv:culture:archivesdefrance:seda:v2.0',
        'xsd': 'http://www.w3.org/2001/XMLSchema',
        'xlink': 'http://www.w3.org/1999/xlink',
    }
    root_attributes = {
        'attributeFormDefault': 'unqualified',
        'elementFormDefault': 'qualified',
        'targetNamespace': 'fr:gouv:culture:archivesdefrance:seda:v2.0',
        'version': '1.0',
    }

    def dump_etree(self):
        """Return an XSD etree for the adapted SEDA profile."""
        self.defined_content_types = set()
        self.root = root = self.element('xsd:schema', attributes=self.root_attributes)
        self.element('xsd:import', parent=root,
                     attributes={'namespace': 'http://www.w3.org/XML/1998/namespace',
                                 'schemaLocation': 'http://www.w3.org/2001/xml.xsd'})
        self.element('xsd:import', parent=root,
                     attributes={'namespace': 'http://www.w3.org/1999/xlink',
                                 'schemaLocation': 'http://www.w3.org/1999/xlink.xsd'})
        self._dump(root)
        xsd_cleanup_etree(root)
        return root

    def init_transfer_element(self, xselement, root, entity):
        profile_element = self.element('xsd:element', root,
                                       {'name': xselement.local_name,
                                        'documentation': entity.user_annotation})
        self.element('xsd:sequence', self.element('xsd:complexType', profile_element))
        open_type = self.element('xsd:complexType', root, {'name': 'OpenType', 'abstract': 'true'})
        open_type_seq = self.element('xsd:sequence', open_type)
        self.element('xsd:attribute', open_type, {'ref': 'xml:id', 'use': 'optional'})
        self.element('xsd:attribute', open_type, {'ref': 'xlink:href', 'use': 'optional'})
        self.element('xsd:any', open_type_seq, {'namespace': '##other', 'processContents': 'lax',
                                                'minOccurs': '0'})
        return profile_element

    def jumped_element(self, profile_element):
        return self._parent_element(profile_element)[-1]

    def element_alternative(self, occ, profile_element, target_value, to_process, card_entity):
        attrs = xsd_element_cardinality(occ, card_entity)
        parent_element = self._parent_element(profile_element)
        target_element = self.element('xsd:choice', parent_element, attrs)
        to_process[occ.target].append((target_value, target_element))

    def element_sequence(self, occ, profile_element, target_value, to_process, card_entity):
        attrs = xsd_element_cardinality(occ, card_entity)
        parent_element = self._parent_element(profile_element)
        target_element = self.element('xsd:sequence', parent_element, attrs)
        to_process[occ.target].append((target_value, target_element))

    def element_xmlattribute(self, occ, profile_element, target_value, to_process, card_entity):
        attrs = xsd_attribute_cardinality(occ, card_entity)
        q = self.qname
        xpath = q('xsd:complexType') + '/' + q('xsd:simpleContent') + '/' + q('xsd:extension')
        parent = profile_element.find(xpath)
        if parent is None:
            parent = profile_element.find(self.qname('xsd:complexType'))
            assert parent is not None
        xselement = occ.target
        attrs['name'] = xselement.local_name
        content_type = self.xsd_content_type(xselement.textual_content_type)
        attrs['type'] = content_type
        target_element = self.element('xsd:attribute', parent, attrs)
        value = serialize(target_value)
        if value is not None:
            if xselement.local_name == 'id':
                target_element.attrib[self.qname('seda:profid')] = value
            else:
                target_element.attrib['fixed'] = value

    def element_xmlelement(self, occ, profile_element, target_value, to_process, card_entity):  # noqa
        attrs = xsd_element_cardinality(occ, card_entity)
        attrs['documentation'] = getattr(card_entity, 'user_annotation', None)
        xselement = occ.target
        if xselement.local_name == 'Signature':
            attrs['type'] = 'OpenType'
            self._target_element(xselement, profile_element, attrs)
        elif isinstance(occ, dict):  # fake occurence introduced for some elements'content
            # target element has already been introduced: it is now given as profile_element
            target_element = profile_element
            extension_element = target_element[0][0][0]
            self.fill_element(xselement, target_element, extension_element,
                              target_value, card_entity)
        else:
            target_element = self._target_element(xselement, profile_element, attrs)
            content_type = self.xsd_content_type(xselement.textual_content_type)
            if content_type:
                type_element = self.element('xsd:complexType', target_element)
                content_element = self.element('xsd:simpleContent', parent=type_element)
                extension_element = self.element('xsd:extension', parent=content_element,
                                                 attributes={'base': content_type})
                self.fill_element(xselement, target_element, extension_element,
                                  target_value, card_entity, copy_attributes=True)
            else:
                type_element = self.element('xsd:complexType', target_element)
                seq_element = self.element('xsd:sequence', type_element)
                # target is a complex element
                if getattr(target_value, 'eid', None):  # value is an entity
                    if target_value.cw_etype == 'Agent':
                        self.fill_organization_element(seq_element, target_value)
                elif xselement.local_name in ('ArchivalAgency', 'TransferringAgency'):
                    self.fill_organization_element(seq_element, None)
                elif target_value is not None:
                    assert False, (xselement, target_value)
            if getattr(target_value, 'eid', None):  # value is an entity
                to_process[xselement].append((target_value, target_element))

    def fill_element(self, xselement, target_element, extension_element, value, card_entity,
                     copy_attributes=False):
        if xselement.local_name == 'KeywordType':
            if value:
                attrs = {'fixed': value.scheme.description or value.scheme.dc_title()}
            else:
                attrs = {'default': 'edition 2009'}
            attrs['name'] = 'listVersionID'
            attr = self.element('xsd:attribute', attributes=attrs, parent=extension_element)
        elif (xselement.local_name == 'KeywordReference' and card_entity.scheme):
            self.concept_scheme_attribute(xselement, extension_element, card_entity.scheme)
        elif getattr(value, 'cw_etype', None) == 'Concept':
            self.concept_scheme_attribute(xselement, extension_element, value.scheme)
        elif copy_attributes:
            for attrname, occ in xselement.attributes.items():
                if attrname in ('id', 'href') or attrname.startswith(('list', 'scheme')):
                    attrs = xsd_attribute_cardinality(occ, None)
                    attrs['name'] = attrname
                    attrs['type'] = self.xsd_content_type(occ.target.textual_content_type)
                    attr = self.element('xsd:attribute', attributes=attrs, parent=extension_element)
        fixed_value = serialize(value)
        if fixed_value is not None:
            attr = 'default' if hasattr(value, 'id') else 'fixed'
            target_element.attrib[attr] = fixed_value

    def concept_scheme_attribute(self, xselement, type_element, scheme):
        try:
            scheme_attr = xselement_scheme_attribute(xselement)
        except KeyError:
            return
        self.element('xsd:attribute', type_element,
                     attributes={'name': scheme_attr,
                                 'fixed': scheme.absolute_url()})

    def fill_organization_element(self, parent_element, value):
        target_element = self.element('xsd:element', parent_element, {'name': 'Identifier'})
        type_element = self.element('xsd:simpleType', target_element)
        restriction_element = self.element('xsd:restriction', type_element,
                                           {'base': 'xsd:string'})
        if value:
            self.element('xsd:enumeration', restriction_element,
                         {'value': value.absolute_url()})

    def _parent_element(self, profile_element):
        q = self.qname
        if profile_element.tag in (q('xsd:choice'), q('xsd:sequence')):
            parent = profile_element
        else:
            xpath = q('xsd:complexType') + '/' + q('xsd:sequence')
            parent = profile_element.find(xpath)
            assert parent is not None
        return parent

    def _target_element(self, xselement, profile_element, attrs):
        parent_element = self._parent_element(profile_element)
        attrs['name'] = xselement.local_name
        return self.element('xsd:element', parent_element, attrs)

    def xsd_content_type(self, content_type):
        """Return XSD content type from pyxst `textual_content_type` that may be None, a set or a string
        value.
        """
        if content_type:
            if isinstance(content_type, set):
                # to satisfy XSD schema, we've to create an intermediary type holding the union of
                # types
                type_name = ''.join(sorted(content_type))
                if type_name not in self.defined_content_types:
                    type_element = self.element('xsd:simpleType', self.root, {'name': type_name})
                    union_element = self.element('xsd:union', parent=type_element)
                    content_type = ' '.join(sorted('xsd:' + ct for ct in content_type))
                    union_element.attrib['memberTypes'] = content_type
                    self.defined_content_types.add(type_name)
                return type_name
            content_type = 'xsd:' + content_type
        return content_type


class SEDA2RelaxNGExport(RNGMixin, SEDA2ExportAdapter):
    """Adapter to build a Relax NG representation of a SEDA profile, using SEDA 2.0 specification.
    """
    __regid__ = 'SEDA-2.0.rng'
    extension = 'rng'

    namespaces = SEDA2XSDExport.namespaces.copy()
    namespaces['rng'] = 'http://relaxng.org/ns/structure/1.0'
    namespaces['a'] = 'http://relaxng.org/ns/compatibility/annotations/1.0'

    root_attributes = {
        'ns': 'fr:gouv:culture:archivesdefrance:seda:v2.0',
        'datatypeLibrary': 'http://www.w3.org/2001/XMLSchema-datatypes',
    }

    def dump_etree(self):
        """Return an XSD etree for the adapted SEDA profile."""
        root = self.element('rng:grammar', attributes=self.root_attributes)
        start = self.element('rng:start', root)
        # XXX http://lists.xml.org/archives/xml-dev/200206/msg01074.html ?
        # self.element('xsd:import', parent=root,
        #              attributes={'namespace': 'http://www.w3.org/1999/xlink',
        #                          'schemaLocation': 'http://www.w3.org/1999/xlink.xsd'})
        self._dump(start)

        open_type = self.element('rng:define', root, {'name': 'OpenType'})
        open_elt = self._create_hierarchy(open_type, ['rng:zeroOrMore', 'rng:element'])
        self.element('rng:anyName', open_elt)
        self._create_hierarchy(open_elt, ['rng:zeroOrMore', 'rng:attribute', 'rng:anyName'])

        # add a 'text' node to empty rng:element to satisfy the RNG grammar
        namespaces = self.namespaces.copy()
        del namespaces[None]  # xpath engine don't want None prefix
        for element in root.xpath('//rng:element[not(*)]', namespaces=namespaces):
            self.element('rng:text', element)
        return root

    def init_transfer_element(self, xselement, root, entity):
        transfer_element = self.element('rng:element', root,
                                        {'name': xselement.local_name,
                                         'documentation': entity.user_annotation})
        exc = self._create_hierarchy(
            transfer_element, ['rng:zeroOrMore', 'rng:attribute', 'rng:anyName', 'rng:except'])
        self.element('rng:nsName', exc)
        self.element('rng:nsName', exc, {'ns': ''})
        return transfer_element

    def jumped_element(self, profile_element):
        element = profile_element[-1]
        if element.tag != '{http://relaxng.org/ns/structure/1.0}element':
            # optional, zeroOrMore, etc.: should pick their child element
            element = element[-1]
            assert element.tag == '{http://relaxng.org/ns/structure/1.0}element', element
        return element

    def element_alternative(self, occ, profile_element, target_value, to_process, card_entity):
        parent_element = self._rng_element_parent(occ, card_entity, profile_element)
        target_element = self.element('rng:choice', parent_element)
        to_process[occ.target].append((target_value, target_element))

    def element_sequence(self, occ, profile_element, target_value, to_process, card_entity):
        parent_element = self._rng_element_parent(occ, card_entity, profile_element)
        target_element = self.element('rng:group', parent_element)  # XXX sequence
        to_process[occ.target].append((target_value, target_element))

    def element_xmlattribute(self, occ, profile_element, target_value, to_process, card_entity):
        parent_element = self._rng_attribute_parent(occ, card_entity, profile_element)
        self._rng_attribute(occ.target, parent_element, serialize(target_value))

    def element_xmlelement(self, occ, profile_element, target_value, to_process, card_entity):  # noqa
        parent_element = self._rng_element_parent(occ, card_entity, profile_element)
        xselement = occ.target
        attrs = {'documentation': getattr(card_entity, 'user_annotation', None),
                 'name': xselement.local_name}
        if xselement.local_name == 'Signature':
            element = self.element('rng:element', parent_element, attrs)
            self.element('rng:ref', element, {'name': 'OpenType'})
        elif isinstance(occ, dict):  # fake occurence introduced for some elements'content
            # target element has already been introduced: it is now given as profile_element
            self.fill_element(xselement, profile_element, target_value, card_entity)
        else:
            target_element = self.element('rng:element', parent_element, attrs)
            xstypes = content_types(xselement.textual_content_type)
            if xstypes:
                if len(xstypes) == 1:
                    parent_element = target_element
                else:
                    parent_element = self.element('rng:choice', target_element)
                for xstype in xstypes:
                    self.fill_element(xselement, parent_element, target_value, card_entity,
                                      xstype=xstype, copy_attributes=True)
            else:
                # target is a complex element
                if getattr(target_value, 'eid', None):  # value is an entity
                    if target_value.cw_etype == 'Agent':
                        self.fill_organization_element(target_element, target_value)
                elif xselement.local_name in ('ArchivalAgency', 'TransferringAgency'):
                    self.fill_organization_element(target_element, None)
                elif target_value is not None:
                    assert False, (xselement, target_value)
            if getattr(target_value, 'eid', None):  # value is an entity
                to_process[xselement].append((target_value, target_element))

    def fill_element(self, xselement, profile_element, value, card_entity,  # noqa
                     copy_attributes=False, xstype=None):
        if xselement.local_name == 'KeywordType':
            attr = self.element('rng:attribute', attributes={'name': 'listVersionID'},
                                parent=self.element('rng:optional', profile_element))
            if value:
                list_value = value.scheme.description or value.scheme.dc_title()
                attrs = {'type': xstype} if xstype else {}
                self.element('rng:value', attr, attrs, text=list_value)
            else:
                attr.attrib[self.qname('a:defaultValue')] = 'edition 2009'

        elif (xselement.local_name == 'KeywordReference' and card_entity.scheme):
            self.concept_scheme_attribute(xselement, profile_element, card_entity.scheme)

        elif getattr(value, 'cw_etype', None) == 'Concept':
            self.concept_scheme_attribute(xselement, profile_element, value.scheme)

        elif copy_attributes:
            for attrname, occ in xselement.attributes.items():
                if attrname in ('id', 'href') or attrname.startswith(('list', 'scheme')):
                    parent_element = self._rng_attribute_parent(occ, card_entity, profile_element)
                    self._rng_attribute(occ.target, parent_element)
        fixed_value = serialize(value)
        if fixed_value is not None:
            if hasattr(value, 'id'):
                profile_element.attrib[self.qname('a:defaultValue')] = fixed_value
            else:
                if (profile_element
                        and profile_element[-1].tag == '{http://relaxng.org/ns/structure/1.0}data'):
                    xstype = profile_element[-1].attrib.get('type')
                    profile_element.remove(profile_element[-1])
                attrs = {'type': xstype} if xstype else {}
                self.element('rng:value', profile_element, attrs, text=fixed_value)
        elif xstype is not None:
            self.element('rng:data', profile_element, {'type': xstype})

    def concept_scheme_attribute(self, xselement, type_element, scheme):
        try:
            scheme_attr = xselement_scheme_attribute(xselement)
        except KeyError:
            return
        scheme_attr = self.element('rng:attribute', type_element,
                                   attributes={'name': scheme_attr})
        self.element('rng:value', scheme_attr, text=scheme.absolute_url())

    def fill_organization_element(self, parent_element, value):
        target_element = self.element('rng:element', parent_element, {'name': 'Identifier'})
        if value:
            self.element('rng:value', target_element, text=value.absolute_url())

    def _rng_element_parent(self, occ, card_entity, profile_element):
        minimum, maximum = element_minmax_cardinality(occ, card_entity)
        return self.rng_element_parent(profile_element, minimum, maximum)

    def _rng_attribute_parent(self, occ, card_entity, profile_element):
        minimum = attribute_minimum_cardinality(occ, card_entity)
        return self.rng_element_parent(profile_element, minimum)

    def _rng_attribute(self, xselement, parent_element, value=None):
        xstypes = content_types(xselement.textual_content_type)
        if len(xstypes) > 1:
            parent_element = self.element('rng:choice', parent_element)
        for xstype in xstypes:
            attr_element = self.element('rng:attribute', parent_element,
                                        {'name': xselement.local_name})
            if value is not None:
                if xselement.local_name == 'id':
                    attr_element.attrib[self.qname('seda:profid')] = value
                else:
                    self.element('rng:value', attr_element, {'type': xstype}, text=value)
            else:
                self.element('rng:data', attr_element, {'type': xstype})

    def _create_hierarchy(self, parent, tags):
        for tag in tags:
            parent = self.element(tag, parent)
        return parent


class XAttr(namedtuple('_XAttr', ['name', 'qualified_type', 'cardinality', 'fixed_value'])):
    """Simple representation of an attribute element in a schema (RNG or XSD).

    Parameters:

    * `name`, the attribute's name,

    * `qualified_type`, its qualified type (e.g. 'xsd:string'),

    * `cardinality`, optional cardinality as string (None, '1' or '0..1') - default to '1' if some
      fixed value is provided, else to None (i.e. attribute is prohibited),

    * `fixed_value`, optional fixed value for the attribute.

    """
    def __new__(cls, name, qualified_type, cardinality=None, fixed_value=None):
        assert cardinality in (None, '1', '0..1'), cardinality
        if cardinality is None and fixed_value is not None:
            cardinality = '1'
        return super(XAttr, cls).__new__(cls, name, qualified_type, cardinality, fixed_value)


LIST_VERSION_ID_2009 = XAttr('listVersionID', 'xsd:token', '1', 'edition 2009')
LIST_VERSION_ID_2011 = XAttr('listVersionID', 'xsd:token', '1', 'edition 2011')


class SEDA1XSDExport(SEDA2XSDExport):
    """Adapter to build an XSD representation of a simplified SEDA profile, using SEDA 1.0
    specification.

    The SEDA2XSDExport implementation may be driven by the SEDA 2.0 XSD model because it's used as
    the basis for the Yams model generation. We can't do the same thing with lower version of SEDA,
    hence the limitation to simplified profile, and a direct implementation of the export.
    """
    __regid__ = 'SEDA-1.0.xsd'
    __select__ = SEDA2XSDExport.__select__ & simplified_profile()
    extension = 'xsd'

    namespaces = {
        None: 'fr:gouv:culture:archivesdefrance:seda:v1.0',
        'xsd': 'http://www.w3.org/2001/XMLSchema',
        'qdt': 'fr:gouv:culture:archivesdefrance:seda:v1.0:QualifiedDataType:1',
        'udt': 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:10',
        'clmDAFFileTypeCode': 'urn:un:unece:uncefact:codelist:draft:DAF:fileTypeCode:2009-08-18',
        'clmIANACharacterSetCode':
        'urn:un:unece:uncefact:codelist:standard:IANA:CharacterSetCode:2007-05-14',
        'clmIANAMIMEMediaType':
        'urn:un:unece:uncefact:codelist:standard:IANA:MIMEMediaType:2008-11-12',
        'clm60133': 'urn:un:unece:uncefact:codelist:standard:6:0133:40106',
    }
    root_attributes = {
        'targetNamespace': 'fr:gouv:culture:archivesdefrance:seda:v1.0',
        'attributeFormDefault': 'unqualified',
        'elementFormDefault': 'qualified',
        'version': '1.0',
    }

    concepts_language = 'seda-1'

    def element_schema(self, parent, name, xsd_type=None, fixed_value=None, cardinality='1',
                       documentation=None, xsd_attributes=()):
        attributes = {'name': name}
        if fixed_value is not None:
            attributes['fixed'] = text_type(fixed_value)
        if xsd_type is not None and not xsd_attributes:
            attributes['type'] = xsd_type
        assert cardinality in ('0..1', '0..n', '1', '1..n')
        if cardinality != '1':
            if cardinality[0] == '0':
                attributes['minOccurs'] = '0'
            if cardinality[-1] == 'n':
                attributes['maxOccurs'] = 'unbounded'
        if documentation:
            attributes['documentation'] = documentation
        element = self.element('xsd:element', parent, attributes)
        children_parent = None
        if xsd_type is None:
            attributes_parent = self.element('xsd:complexType', element)
            children_parent = self.element('xsd:sequence', attributes_parent)
        elif xsd_attributes:
            ct = self.element('xsd:complexType', element)
            scontent = self.element('xsd:simpleContent', ct)
            attributes_parent = self.element('xsd:extension', scontent, {'base': xsd_type})
        for xattr in xsd_attributes:
            self.attribute_schema(attributes_parent, xattr)
        return children_parent

    def attribute_schema(self, parent, xattr):
        attrs = {'name': xattr.name, 'type': xattr.qualified_type}
        if xattr.cardinality is None:
            attrs['use'] = 'prohibited'
        elif xattr.cardinality == '1':
            attrs['use'] = 'required'
        else:
            attrs['use'] = 'optional'
        if xattr.fixed_value is not None:
            attrs['fixed'] = text_type(xattr.fixed_value)
        self.element('xsd:attribute', parent, attrs)

    # business visit methods #######################################################################

    def dump_etree(self):
        """Return an XSD etree for the adapted SEDA profile."""
        root = self.element('xsd:schema', attributes=self.root_attributes)
        # self.element('xsd:import', parent=root,
        #              attributes={'namespace': 'http://www.w3.org/XML/1998/namespace',
        #                          'schemaLocation': 'http://www.w3.org/2001/xml.xsd'})
        self.xsd_transfer(root, self.entity)
        return root

    def xsd_transfer(self, parent, archive_transfer):
        """Append XSD elements for the archive transfer to the given parent node."""
        transfer_node = self.element_schema(parent, 'ArchiveTransfer',
                                            documentation=archive_transfer.title,
                                            xsd_attributes=[XAttr('Id', 'xsd:ID')])
        for comment in archive_transfer.comments:
            self.element_schema(transfer_node, 'Comment', 'udt:TextType',
                                fixed_value=comment.comment,
                                cardinality=comment.user_cardinality,
                                documentation=comment.user_annotation,
                                xsd_attributes=[XAttr('languageID', 'xsd:language')])
        self.element_schema(transfer_node, 'Date', 'udt:DateTimeType')
        self.element_schema(transfer_node, 'TransferIdentifier', 'qdt:ArchivesIDType',
                            xsd_attributes=self.xsd_attributes_scheme())
        for archive_unit in archive_transfer.archive_units:
            self.xsd_archive(transfer_node, archive_unit)

    def xsd_archive(self, parent, archive_unit):
        """Append XSD elements for an archive to the given parent node."""
        archive_node = self.element_schema(parent, 'Archive',
                                           cardinality=archive_unit.user_cardinality,
                                           documentation=archive_unit.user_annotation,
                                           xsd_attributes=[XAttr('Id', 'xsd:ID')])
        # hard-coded description's language XXX fine, content language may be specified
        self.element_schema(archive_node, 'DescriptionLanguage', 'qdt:CodeLanguageType',
                            fixed_value='fra',
                            xsd_attributes=[LIST_VERSION_ID_2011])
        name_entity = self.archive_unit_name(archive_unit)
        self.element_schema(archive_node, 'Name', 'udt:TextType',
                            fixed_value=name_entity.title,
                            documentation=name_entity.user_annotation,
                            xsd_attributes=[XAttr('languageID', 'xsd:language')])
        appraisal_rule_entity = self.archive_unit_appraisal_rule(archive_unit)
        if appraisal_rule_entity:
            self.xsd_appraisal_rule(archive_node, appraisal_rule_entity)
        # not optional in seda 1
        access_rule_entity = self.archive_unit_access_rule(archive_unit)
        self.xsd_access_rule(archive_node, access_rule_entity)
        content_entity = self.archive_unit_content(archive_unit)
        self.xsd_content_description(archive_node, content_entity)
        self.xsd_children(archive_node, archive_unit)

    def archive_unit_name(self, archive_unit):
        seq = archive_unit.first_level_choice.content_sequence
        return seq.contents[0].title

    def archive_unit_appraisal_rule(self, archive_unit):
        seq = archive_unit.first_level_choice.content_sequence
        return seq.reverse_seda_appraisal_rule[0] if seq.reverse_seda_appraisal_rule else None

    def xsd_appraisal_rule(self, parent, appraisal_rule):
        # XXX cardinality 1 on rule, not multiple + element name : 'Appraisal' ou 'AppraisalRule'
        # (cf http://www.archivesdefrance.culture.gouv.fr/seda/api/index.html)
        ar_node = self.element_schema(parent, 'Appraisal',
                                      cardinality=appraisal_rule.user_cardinality,
                                      documentation=appraisal_rule.user_annotation,
                                      xsd_attributes=[XAttr('Id', 'xsd:ID')])
        ar_code = appraisal_rule.final_action_concept
        ar_code_value = _concept_value(ar_code, self.concepts_language)
        self.element_schema(ar_node, 'Code', 'qdt:CodeAppraisalType',
                            fixed_value=ar_code_value,
                            xsd_attributes=[LIST_VERSION_ID_2009])

        rule = appraisal_rule.rules[0] if appraisal_rule.rules else None
        value = _concept_value(rule.rule_concept, self.concepts_language) if rule else None
        self.element_schema(ar_node, 'Duration', 'qdt:ArchivesDurationType',
                            fixed_value=value,
                            documentation=rule.user_annotation if rule else None)
        self.element_schema(ar_node, 'StartDate', 'udt:DateType')

    def archive_unit_access_rule(self, archive_unit):
        seq = archive_unit.first_level_choice.content_sequence
        return seq.reverse_seda_access_rule[0] if seq.reverse_seda_access_rule else None

    access_restriction_tag_name = 'AccessRestrictionRule'

    def xsd_access_rule(self, parent, access_rule):
        """Append XSD elements for an access restriction to the given parent node."""
        ar_node = self.element_schema(parent, self.access_restriction_tag_name,
                                      cardinality=access_rule.user_cardinality,
                                      documentation=access_rule.user_annotation,
                                      xsd_attributes=[XAttr('Id', 'xsd:ID')])
        # XXX cardinality 1
        rule = access_rule.rules[0] if access_rule.rules else None
        value = _concept_value(rule.rule_concept, self.concepts_language) if rule else None
        self.element_schema(ar_node, 'Code', 'qdt:CodeAccessRestrictionType',
                            fixed_value=value,
                            documentation=rule.user_annotation if rule else None,
                            xsd_attributes=[LIST_VERSION_ID_2009])
        self.element_schema(ar_node, 'StartDate', 'udt:DateType')

    def archive_unit_content(self, archive_unit):
        seq = archive_unit.first_level_choice.content_sequence
        return seq.contents[0]

    def xsd_content_description(self, parent, content):
        """Append XSD elements for a description content to the given parent node"""
        cd_node = self.element_schema(parent, 'ContentDescription',
                                      # XXX should always be 1
                                      # cardinality=content.user_cardinality,
                                      documentation=content.user_annotation,
                                      xsd_attributes=[XAttr('Id', 'xsd:ID')])
        self.xsd_description_level(cd_node, content.description_level_concept)
        for seda2_name, seda1_name in (('start', 'oldest'), ('end', 'latest')):
            date_entity = getattr(content, '%s_date' % seda2_name)
            if date_entity:
                self.element_schema(cd_node, '%sDate' % seda1_name.capitalize(), 'udt:DateType',
                                    cardinality=date_entity.user_cardinality,
                                    documentation=date_entity.user_annotation)
        if content.description:
            self.xsd_description(cd_node, content.description)
        for keyword in content.keywords:
            self.xsd_keyword(cd_node, keyword)

    def xsd_description_level(self, parent, concept):
        """Append XSD elements for a description level to the given parent node"""
        value = _concept_value(concept, self.concepts_language)
        self.element_schema(parent, 'DescriptionLevel', 'qdt:CodeDescriptionLevelType',
                            fixed_value=value,
                            xsd_attributes=[LIST_VERSION_ID_2009])

    def xsd_description(self, parent, description):
        """Append XSD elements for a description to the given parent node"""
        self.element_schema(parent, 'Description', 'udt:TextType',
                            cardinality=description.user_cardinality,
                            documentation=description.user_annotation,
                            fixed_value=description.description,
                            xsd_attributes=[XAttr('languageID', 'xsd:language')])

    # extracted from xsd_keyword to allow parametrization for SEDA 1.0 vs 0.2 generation
    kw_tag_name = 'Keyword'
    kw_content_tag_type = 'qdt:KeywordContentType'
    kw_content_tag_attributes = [XAttr('role', 'xsd:token'),
                                 XAttr('languageID', 'xsd:language')]

    def xsd_keyword(self, parent, keyword):
        """Append XSD elements for a keyword to the given parent node"""
        kw_node = self.element_schema(parent, self.kw_tag_name,
                                      cardinality=keyword.user_cardinality,
                                      documentation=keyword.user_annotation,
                                      xsd_attributes=[XAttr('Id', 'xsd:ID')])
        content = keyword.content_string
        url = None
        if keyword.reference:
            concept = keyword.reference.concept
            if concept:
                url = self.cwuri_url(concept)
                if content is None:
                    content = concept.label()
        self.element_schema(kw_node, 'KeywordContent', self.kw_content_tag_type,
                            fixed_value=content,
                            xsd_attributes=self.kw_content_tag_attributes)
        scheme = keyword.reference.scheme if keyword.reference else None
        self.element_schema(kw_node, 'KeywordReference', 'qdt:ArchivesIDType',
                            fixed_value=url,
                            xsd_attributes=self.xsd_attributes_scheme(scheme))
        # XXX keyword type

    def xsd_children(self, parent, entity):
        """Iter on archive/archive object children, which may be either
        archive objects or documents, and append XSD elements for them to the given parent node.
        """
        for au_or_bdo in entity.cw_adapt_to('ITreeBase').iterchildren():
            if au_or_bdo.cw_etype == 'SEDABinaryDataObject':
                self.xsd_document(parent, au_or_bdo)
            else:
                assert au_or_bdo.cw_etype == 'SEDAArchiveUnit'
                self.xsd_archive_object(parent, au_or_bdo)

    archive_object_tag_name = 'ArchiveObject'

    def xsd_archive_object(self, parent, archive_unit):
        """Append XSD elements for the archive object to the given parent node."""
        ao_node = self.element_schema(parent, self.archive_object_tag_name,
                                      cardinality=archive_unit.user_cardinality,
                                      documentation=archive_unit.user_annotation,
                                      xsd_attributes=[XAttr('Id', 'xsd:ID')])
        content_entity = self.archive_unit_content(archive_unit)
        self.element_schema(ao_node, 'Name', 'udt:TextType',
                            fixed_value=content_entity.title.title,
                            documentation=content_entity.title.user_annotation,
                            xsd_attributes=[XAttr('languageID', 'xsd:language')])
        appraisal_rule_entity = self.archive_unit_appraisal_rule(archive_unit)
        if appraisal_rule_entity:
            self.xsd_appraisal_rule(ao_node, appraisal_rule_entity)
        access_rule_entity = self.archive_unit_access_rule(archive_unit)
        if access_rule_entity:
            self.xsd_access_rule(ao_node, access_rule_entity)
        self.xsd_content_description(ao_node, content_entity)
        self.xsd_children(ao_node, archive_unit)

        return ao_node

    def xsd_document(self, parent, data_object):
        """Append XSD elements for the document to the given parent node."""
        document_node = self.element_schema(parent, 'Document',
                                            cardinality=data_object.user_cardinality,
                                            documentation=data_object.user_annotation,
                                            xsd_attributes=[XAttr('Id', 'xsd:ID')])

        def safe_cardinality(entity):
            if entity is None:
                return None
            return entity.user_cardinality

        def safe_concept_value(entity):
            if entity is None:
                return None
            return _concept_value(entity.concept, self.concepts_language)

        format_id = data_object.format_id
        encoding = data_object.encoding
        self.element_schema(document_node, 'Attachment', 'qdt:ArchivesBinaryObjectType',
                            xsd_attributes=[
                                XAttr('format', 'clmDAFFileTypeCode:FileTypeCodeType',
                                      cardinality=safe_cardinality(format_id),
                                      fixed_value=safe_concept_value(format_id)),
                                XAttr('encodingCode',
                                      'clm60133:CharacterSetEncodingCodeContentType',
                                      cardinality=safe_cardinality(encoding),
                                      fixed_value=safe_concept_value(encoding)),
                                # hard-coded attributes
                                XAttr('characterSetCode',
                                      'clmIANACharacterSetCode:CharacterSetCodeContentType'),
                                XAttr('mimeCode', 'clmIANAMIMEMediaType:MIMEMediaTypeContentType'),
                                XAttr('uri', 'xsd:anyURI'),
                                XAttr('filename', 'xsd:string'),
                            ])
        references = list(data_object.referenced_by)
        assert len(references) == 1
        seq = references[0]
        self.element_schema(document_node, 'Type', 'qdt:CodeDocumentType',
                            fixed_value=safe_concept_value(seq.contents[0].type),
                            xsd_attributes=[LIST_VERSION_ID_2009])
        # if document.description:
        #     self.xsd_description(document_node, document.description)

    @classmethod
    def xsd_attributes_scheme(cls, scheme=None):
        """Return a list of :class:`XAttr` for a scheme definition, with some proper values
        specified if a scheme is given.
        """
        attributes = [
            XAttr('schemeID', 'xsd:token'),
            XAttr('schemeName', 'xsd:string',
                  fixed_value=scheme.title if scheme and scheme.title else None),
            XAttr('schemeAgencyName', 'xsd:string'),
            XAttr('schemeVersionID', 'xsd:token'),
            XAttr('schemeDataURI', 'xsd:anyURI'),
            XAttr('schemeURI', 'xsd:anyURI',
                  fixed_value=cls.cwuri_url(scheme) if scheme else None),
        ]
        # if scheme is not None:
        #     # schemeID XXX move to saem
        #     attributes[0]['fixed'] = scheme.ark
        #     attributes[0]['use'] = 'required'
        return attributes

    @staticmethod
    def cwuri_url(entity):
        """Return "public" URI for the given entity.

        In a staticmethod to ease overriding in subclasses (eg saem).
        """
        return entity.cwuri


class SEDA02XSDExport(SEDA1XSDExport):
    """Adapter to build an XSD representation of a SEDA profile, using SEDA 0.2 specification"""
    __regid__ = 'SEDA-0.2.xsd'

    namespaces = SEDA1XSDExport.namespaces.copy()
    namespaces[None] = 'fr:gouv:ae:archive:draft:standard_echange_v0.2'
    namespaces['qdt'] = 'fr:gouv:ae:archive:draft:standard_echange_v0.2:QualifiedDataType:1'
    namespaces['udt'] = 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:6'

    root_attributes = SEDA1XSDExport.root_attributes.copy()
    root_attributes['targetNamespace'] = 'fr:gouv:ae:archive:draft:standard_echange_v0.2'
    root_attributes['version'] = '1.1'

    def xsd_archive(self, parent, archive_unit):
        """Append XSD elements for an archive to the given parent node."""
        archive_node = self.element_schema(parent, 'Contains',
                                           cardinality=archive_unit.user_cardinality,
                                           documentation=archive_unit.user_annotation,
                                           xsd_attributes=[XAttr('Id', 'xsd:ID')])
        # hard-coded description's language
        self.element_schema(archive_node, 'DescriptionLanguage', 'qdt:CodeLanguageType',
                            fixed_value='fr',
                            xsd_attributes=[LIST_VERSION_ID_2009])
        name_entity = self.archive_unit_name(archive_unit)
        self.element_schema(archive_node, 'Name', 'udt:TextType',
                            fixed_value=name_entity.title,
                            documentation=name_entity.user_annotation,
                            xsd_attributes=[XAttr('languageID', 'xsd:language')])
        # in SEDA 0.2, description level is on the archive element, not on its content description
        content_entity = self.archive_unit_content(archive_unit)
        self.xsd_description_level(archive_node, content_entity.description_level_concept)
        appraisal_rule_entity = self.archive_unit_appraisal_rule(archive_unit)
        if appraisal_rule_entity:
            self.xsd_appraisal_rule(archive_node, appraisal_rule_entity)
        # in SEDA 0.2, access restriction is not mandatory
        access_rule_entity = self.archive_unit_access_rule(archive_unit)
        if access_rule_entity:
            self.xsd_access_rule(archive_node, access_rule_entity)
        content_entity = self.archive_unit_content(archive_unit)
        self.xsd_content_description(archive_node, content_entity)
        self.xsd_children(archive_node, archive_unit)

    def xsd_content_description(self, parent, content):
        """Append XSD elements for a description content to the given parent node"""
        if not (content.start_date or content.end_date or content.description or content.keywords):
            return
        cd_node = self.element_schema(parent, 'ContentDescription',
                                      # XXX should always be 1
                                      # cardinality=content_description.user_cardinality,
                                      documentation=content.user_annotation,
                                      xsd_attributes=[XAttr('Id', 'xsd:ID')])
        for seda2_name, seda1_name in (('start', 'oldest'), ('end', 'latest')):
            date_entity = getattr(content, '%s_date' % seda2_name)
            if date_entity:
                self.element_schema(cd_node, '%sDate' % seda1_name.capitalize(), 'udt:DateType',
                                    cardinality=date_entity.user_cardinality,
                                    documentation=date_entity.user_annotation)
        if content.description:
            self.xsd_description(cd_node, content.description)
        # in SEDA 0.2, content description has a description language as well
        self.element_schema(cd_node, 'DescriptionLanguage', 'qdt:CodeLanguageType',
                            fixed_value='fr',
                            xsd_attributes=[LIST_VERSION_ID_2009])
        for keyword in content.keywords:
            self.xsd_keyword(cd_node, keyword)
        # in SEDA 0.2, there may be some access restriction on the content description but it's on
        # the archive in our model

    def xsd_archive_object(self, parent, archive_unit):
        """Append XSD elements for the archive object to the given parent node."""
        ao_node = super(SEDA02XSDExport, self).xsd_archive_object(parent, archive_unit)
        # in SEDA 0.2, description level is on the archive object element, not on its content
        # description
        content_entity = self.archive_unit_content(archive_unit)
        self.xsd_description_level(ao_node, content_entity.description_level_concept)

    # in SEDA 0.2, ArchiveObject tag name is 'Contains' (as for Archive)
    archive_object_tag_name = 'Contains'
    # in SEDA 0.2, AccessRestrictionRule tag name is 'AccessRestriction'
    access_restriction_tag_name = 'AccessRestriction'
    # in SEDA 0.2, keyword tag name is 'ContentDescriptive', not 'Keyword' and keyword content type
    # is TextType and there is no 'role' attribute
    kw_tag_name = 'ContentDescriptive'
    kw_content_tag_type = 'udt:TextType'
    kw_content_tag_attributes = [XAttr('languageID', 'xsd:language')]


class OldSEDARNGExportMixin(RNGMixin):

    def element_schema(self, parent, name, xsd_type=None, fixed_value=None, cardinality='1',
                       documentation=None, xsd_attributes=()):
        attributes = {'name': name}
        if documentation:
            attributes['documentation'] = documentation
        parent = self.rng_element_parent(parent, *minmax_cardinality(cardinality))
        element = self.element('rng:element', parent, attributes)
        for xattr in xsd_attributes:
            self.attribute_schema(element, xattr)

        if xsd_type is not None:
            self.rng_value(element, xsd_type, fixed_value)
        else:
            assert fixed_value is None
        return element

    def attribute_schema(self, parent, xattr):
        if xattr.cardinality is None:
            return  # XXX prohibit?
        minimum = minmax_cardinality(xattr.cardinality, ('0..1', '1'))[0]
        parent = self.rng_element_parent(parent, minimum)
        attr = self.element('rng:attribute', parent, {'name': xattr.name})
        self.rng_value(attr, xattr.qualified_type, xattr.fixed_value)

    def dump_etree(self):
        """Return an RNG etree for the adapted SEDA profile."""
        root = self.element('rng:grammar', attributes=self.root_attributes)
        start = self.element('rng:start', root)
        self.xsd_transfer(start, self.entity)
        return root


class SEDA1RNGExport(OldSEDARNGExportMixin, SEDA1XSDExport):
    """Adapter to build an RNG representation of a simplified SEDA profile, using SEDA 1.0
    specification.
    """
    __regid__ = 'SEDA-1.0.rng'
    extension = 'rng'

    namespaces = SEDA1XSDExport.namespaces.copy()
    namespaces['rng'] = 'http://relaxng.org/ns/structure/1.0'
    root_attributes = {
        'ns': 'fr:gouv:culture:archivesdefrance:seda:v1.0',
        'datatypeLibrary': 'http://www.w3.org/2001/XMLSchema-datatypes',
    }


class SEDA02RNGExport(OldSEDARNGExportMixin, SEDA02XSDExport):
    """Adapter to build an RNG representation of a simplified SEDA profile, using SEDA 0.2
    specification.
    """
    __regid__ = 'SEDA-0.2.rng'
    extension = 'rng'

    namespaces = SEDA1XSDExport.namespaces.copy()
    namespaces['rng'] = 'http://relaxng.org/ns/structure/1.0'
    root_attributes = {
        'ns': 'fr:gouv:ae:archive:draft:standard_echange_v0.2',
        'datatypeLibrary': 'http://www.w3.org/2001/XMLSchema-datatypes',
    }


def xsd_element_cardinality(occ, card_entity):
    """Return XSD element cardinality for the given pyxst Occurence. Cardinality may be overriden by
    the data model's user_cardinality value.
    """
    minimum, maximum = element_minmax_cardinality(occ, card_entity)
    attribs = {}
    if minimum != 1:
        attribs['minOccurs'] = str(minimum)
    if maximum != 1:
        attribs['maxOccurs'] = 'unbounded'
    return attribs


def xsd_attribute_cardinality(occ, card_entity):
    """Return XSD attribute cardinality for the given pyxst Occurence. Cardinality may be overriden by
    the data model's user_cardinality value.
    """
    if attribute_minimum_cardinality(occ, card_entity) == 1:
        return {'use': 'required'}
    else:
        return {'use': 'optional'}


def xsd_cleanup_etree(element):
    """Cleanup given XSD element tree.

    * forces attributes to be defined after sequence/choices (enforced by XSD schema),
    * remove empty sequence/choice,
    * skip sequence/choice with only one child and which are either not a complexType child or their
      unique children is itself a sequence or choice.
    """
    for subelement in list(element):
        xsd_cleanup_etree(subelement)
        if subelement.tag == '{http://www.w3.org/2001/XMLSchema}attribute':
            element.remove(subelement)
            element.append(subelement)
        elif (subelement.tag in ('{http://www.w3.org/2001/XMLSchema}sequence',
                                 '{http://www.w3.org/2001/XMLSchema}choice',
                                 '{http://www.w3.org/2001/XMLSchema}complexType')
              and len(subelement) == 0):
            element.remove(subelement)
        elif (subelement.tag in ('{http://www.w3.org/2001/XMLSchema}sequence',
                                 '{http://www.w3.org/2001/XMLSchema}choice')
              and len(subelement) == 1
              and (element.tag != '{http://www.w3.org/2001/XMLSchema}complexType'
                   or subelement[0].tag in ('{http://www.w3.org/2001/XMLSchema}sequence',
                                            '{http://www.w3.org/2001/XMLSchema}choice'))):
            element.replace(subelement, subelement[0])


def _path_target_values(entity, path):
    """Given an entity and a path to traverse, return the list of (entity, value) at the end of the
    path.

    Values may be entities or final value if the last relation in the path is an attribute. Values
    are associated to the entity holding their cardinality, which may be either the given entity or
    an intermediary entity.
    """
    is_simple = len(path) == 1
    if is_simple:
        (rtype, role, target_etype, _), = path
        return _simple_path_target_values(entity, rtype, role, target_etype)
    else:
        return _complex_path_target_values(entity, path)


def _simple_path_target_values(entity, rtype, role, target_etype):
    if target_etype in BASE_TYPES:
        return [(None, getattr(entity, rtype, None))]
    targets = entity.related(rtype, role, entities=True)
    rschema = entity._cw.vreg.schema.rschema
    rdefschema = next(iter(rschema(rtype).rdefs.values()))
    if rdefschema.composite:
        return [(target, target) for target in targets]
    elif targets:
        return [(None, target) for target in targets]
    else:
        return [(None, None)]


def _complex_path_target_values(entity, path):
    rschema = entity._cw.vreg.schema.rschema
    entities = (entity,)
    rtype_targets = []
    for rtype, role, target_etype, _ in path:
        try:
            rdefschema = next(iter(rschema(rtype).rdefs.values()))
        except KeyError:
            if rtype in SKIP_ATTRS:
                return rtype_targets
            # element is still in the intermediary representation but not in the schema
            return [(None, None)]
        rtype_targets = []
        for entity in entities:
            if target_etype in BASE_TYPES:
                rtype_targets.append((entity, getattr(entity, rtype, None)))
            else:
                try:
                    targets = entity.related(rtype, role, entities=True)
                except KeyError:
                    # the relation is not defined in the schema: element is not modelized but should
                    # be added in the XSD
                    rtype_targets.append((entity, None))
                    continue
                if targets:
                    rtype_targets += [(entity, t) for t in targets]
                # if relation is not composite, that means it's a "value" relation, hence we should
                # always emit a value (its associated XSD element must be defined)
                elif not rdefschema.composite:
                    rtype_targets.append((entity, None))
        entities = [v for e, v in rtype_targets]
    return rtype_targets


def xselement_scheme_attribute(xselement):
    try:
        return xselement.attributes['listSchemeURI'].target.local_name
    except KeyError:
        return xselement.attributes['schemeURI'].target.local_name
