# Copyright (C) 2009 by Eric Talevich (eric.talevich@gmail.com)
# This code is part of the Biopython distribution and governed by its
# license. Please see the LICENSE file that should have been included
# as part of this package.

"""PhyloXML reader/parser, writer, and associated functions.

Instantiates tree elements from a parsed PhyloXML file, and constructs an XML
file from a Bio.Phylo.PhyloXML object.

About capitalization:

    - phyloXML means the file format specification
    - PhyloXML means the Biopython module Bio.Phylo.PhyloXML and its classes
    - Phyloxml means the top-level class used by PhyloXMLIO.read (but not
      Bio.Phylo.read!), containing a list of Phylogenies (Tree-derived objects)
"""
__docformat__ = "epytext en"

import sys
import warnings

from Bio.Phylo import PhyloXML as PX

if (3, 0, 0) <= sys.version_info[:3] <= (3, 1, 2):
    # Workaround for cElementTree regression in python 3.0--3.1.2
    # See http://bugs.python.org/issue9257
    from xml.etree import ElementTree
else:
    try:
        from xml.etree import cElementTree as ElementTree
    except ImportError:
        # Alternative Python implementation, perhaps?
        try:
            from xml.etree import ElementTree as ElementTree
        except ImportError:
            # Python 2.4 -- check for 3rd-party implementations
            try:
                from lxml import etree as ElementTree
            except ImportError:
                try:
                    import cElementTree as ElementTree
                except ImportError:
                    try:
                        from elementtree import ElementTree
                    except ImportError:
                        from Bio import MissingPythonDependencyError
                        raise MissingPythonDependencyError(
                                "No ElementTree module was found. "
                                "Use Python 2.5+, lxml or elementtree if you "
                                "want to use Bio.PhyloXML.")

# Keep the standard namespace prefixes when writing
# See http://effbot.org/zone/element-namespaces.htm
NAMESPACES = {
        'phy':  'http://www.phyloxml.org',
        'xs':   'http://www.w3.org/2001/XMLSchema',
        }

try:
    register_namespace = ElementTree.register_namespace
except AttributeError:
    if not hasattr(ElementTree, '_namespace_map'):
        # cElementTree needs the pure-Python xml.etree.ElementTree
        # Py2.4 support: the exception handler can go away when Py2.4 does
        try:
            from xml.etree import ElementTree as ET_py
            ElementTree._namespace_map = ET_py._namespace_map
        except ImportError:
            warnings.warn("Couldn't import xml.etree.ElementTree; "
                    "phyloXML namespaces may have unexpected abbreviations "
                    "in the output.",
                    # NB: ImportWarning was introduced in Py2.5
                    Warning, stacklevel=2)
            ElementTree._namespace_map = {}

    def register_namespace(prefix, uri):
        ElementTree._namespace_map[uri] = prefix

for prefix, uri in NAMESPACES.iteritems():
    register_namespace(prefix, uri)


class PhyloXMLError(Exception):
    """Exception raised when PhyloXML object construction cannot continue.

    XML syntax errors will be found and raised by the underlying ElementTree
    module; this exception is for valid XML that breaks the phyloXML
    specification.
    """
    pass


# ---------------------------------------------------------
# Public API

def read(file):
    """Parse a phyloXML file or stream and build a tree of Biopython objects.

    The children of the root node are phylogenies and possibly other arbitrary
    (non-phyloXML) objects.

    @return: a single Bio.Phylo.PhyloXML.Phyloxml object.
    """
    return Parser(file).read()

def parse(file):
    """Iterate over the phylogenetic trees in a phyloXML file.

    This ignores any additional data stored at the top level, but may be more
    memory-efficient than the read() function.

    @return: a generator of Bio.Phylo.PhyloXML.Phylogeny objects.
    """
    return Parser(file).parse()

def write(obj, file, encoding='utf-8', indent=True):
    """Write a phyloXML file.

    The first argument is an instance of Phyloxml, Phylogeny or BaseTree.Tree,
    or an iterable of either of the latter two. The object will be converted to
    a Phyloxml object before serialization.

    The file argument can be either an open handle or a file name.
    """
    def fix_single(tree):
        if isinstance(tree, PX.Phylogeny):
            return tree
        if isinstance(tree, PX.Clade):
            return tree.to_phylogeny()
        if isinstance(tree, PX.BaseTree.Tree):
            return PX.Phylogeny.from_tree(tree)
        if isinstance(tree, PX.BaseTree.Clade):
            return PX.Phylogeny.from_tree(PX.BaseTree.Tree(root=tree))
        else:
            raise ValueError("iterable must contain Tree or Clade types")

    if isinstance(obj, PX.Phyloxml):
        pass
    elif (isinstance(obj, PX.BaseTree.Tree) or
          isinstance(obj, PX.BaseTree.Clade)):
        obj = fix_single(obj).to_phyloxml()
    elif hasattr(obj, '__iter__'):
        obj = PX.Phyloxml({}, phylogenies=(fix_single(t) for t in obj))
    else:
        raise ValueError("First argument must be a Phyloxml, Phylogeny, "
                "Tree, or iterable of Trees or Phylogenies.")
    return Writer(obj).write(file, encoding=encoding, indent=indent)


# ---------------------------------------------------------
# Functions I wish ElementTree had

def _local(tag):
    """Extract the local tag from a namespaced tag name."""
    if tag[0] == '{':
        return tag[tag.index('}')+1:]
    return tag

def _split_namespace(tag):
    """Split a tag into namespace and local tag strings."""
    try:
        return tag[1:].split('}', 1)
    except:
        return ('', tag)

def _ns(tag, namespace=NAMESPACES['phy']):
    """Format an XML tag with the given namespace."""
    return '{%s}%s' % (namespace, tag)

def _get_child_as(parent, tag, construct):
    """Find a child node by tag, and pass it through a constructor.

    Returns None if no matching child is found.
    """
    child = parent.find(_ns(tag))
    if child is not None:
        return construct(child)

def _get_child_text(parent, tag, construct=unicode):
    """Find a child node by tag; pass its text through a constructor.

    Returns None if no matching child is found.
    """
    child = parent.find(_ns(tag))
    if child is not None and child.text:
        return construct(child.text)

def _get_children_as(parent, tag, construct):
    """Find child nodes by tag; pass each through a constructor.

    Returns an empty list if no matching child is found.
    """
    return [construct(child) for child in 
            parent.findall(_ns(tag))]

def _get_children_text(parent, tag, construct=unicode):
    """Find child nodes by tag; pass each node's text through a constructor.

    Returns an empty list if no matching child is found.
    """
    return [construct(child.text) for child in 
            parent.findall(_ns(tag))
            if child.text]

def _indent(elem, level=0):
    """Add line breaks and indentation to ElementTree in-place.

    Sources:
        - U{ http://effbot.org/zone/element-lib.htm#prettyprint }
        - U{ http://infix.se/2007/02/06/gentlemen-indent-your-xml }
    """
    i = "\n" + level*"  "
    if len(elem):
        if not elem.text or not elem.text.strip():
            elem.text = i + "  "
        for e in elem:
            _indent(e, level+1)
            if not e.tail or not e.tail.strip():
                e.tail = i + "  "
        if not e.tail or not e.tail.strip():
            e.tail = i
    else:
        if level and (not elem.tail or not elem.tail.strip()):
            elem.tail = i

# ---------------------------------------------------------
# INPUT
# ---------------------------------------------------------

def _str2bool(text):
    if text == 'true':
        return True
    if text == 'false':
        return False
    raise ValueError('String could not be converted to boolean: ' + text)

def _dict_str2bool(dct, keys):
    out = dct.copy()
    for key in keys:
        if key in out:
            out[key] = _str2bool(out[key])
    return out

def _int(text):
    if text is not None:
        try:
            return int(text)
        except Exception:
            return None

def _float(text):
    if text is not None:
        try:
            return float(text)
        except Exception:
            return None

def _collapse_wspace(text):
    """Replace all spans of whitespace with a single space character.

    Also remove leading and trailing whitespace. See "Collapse Whitespace
    Policy" in the U{ phyloXML spec glossary
    <http://phyloxml.org/documentation/version_100/phyloxml.xsd.html#Glossary>
    }.
    """
    if text is not None:
        return ' '.join(text.split())

# NB: Not currently used
def _replace_wspace(text):
    """Replace tab, LF and CR characters with spaces, but don't collapse.

    See "Replace Whitespace Policy" in the U{ phyloXML spec glossary
    <http://phyloxml.org/documentation/version_100/phyloxml.xsd.html#Glossary>
    }.
    """
    for char in ('\t', '\n', '\r'):
        if char in text:
            text = text.replace(char, ' ')
    return text


class Parser(object):
    """Methods for parsing all phyloXML nodes from an XML stream.

    To minimize memory use, the tree of ElementTree parsing events is cleared
    after completing each phylogeny, clade, and top-level 'other' element.
    Elements below the clade level are kept in memory until parsing of the
    current clade is finished -- this shouldn't be a problem because clade is
    the only recursive element, and non-clade nodes below this level are of
    bounded size.
    """

    def __init__(self, file):
        # Get an iterable context for XML parsing events
        context = iter(ElementTree.iterparse(file, events=('start', 'end')))
        event, root = context.next()
        self.root = root
        self.context = context

    def read(self):
        """Parse the phyloXML file and create a single Phyloxml object."""
        phyloxml = PX.Phyloxml(dict((_local(key), val)
                                for key, val in self.root.items()))
        other_depth = 0
        for event, elem in self.context:
            namespace, localtag = _split_namespace(elem.tag)
            if event == 'start':
                if namespace != NAMESPACES['phy']:
                    other_depth += 1
                    continue
                if localtag == 'phylogeny':
                    phylogeny = self._parse_phylogeny(elem)
                    phyloxml.phylogenies.append(phylogeny)
            if event == 'end' and namespace != NAMESPACES['phy']:
                # Deal with items not specified by phyloXML
                other_depth -= 1
                if other_depth == 0:
                    # We're directly under the root node -- evaluate
                    otr = self.other(elem, namespace, localtag)
                    phyloxml.other.append(otr)
                    self.root.clear()
        return phyloxml

    def parse(self):
        """Parse the phyloXML file incrementally and return each phylogeny."""
        phytag = _ns('phylogeny')
        for event, elem in self.context:
            if event == 'start' and elem.tag == phytag:
                yield self._parse_phylogeny(elem)

    # Special parsing cases -- incremental, using self.context

    def _parse_phylogeny(self, parent):
        """Parse a single phylogeny within the phyloXML tree.

        Recursively builds a phylogenetic tree with help from parse_clade, then
        clears the XML event history for the phylogeny element and returns
        control to the top-level parsing function.
        """
        phylogeny = PX.Phylogeny(**_dict_str2bool(parent.attrib,
                                                   ['rooted', 'rerootable']))
        list_types = {
                # XML tag, plural attribute
                'confidence':   'confidences',
                'property':     'properties',
                'clade_relation': 'clade_relations',
                'sequence_relation': 'sequence_relations',
                }
        for event, elem in self.context:
            namespace, tag = _split_namespace(elem.tag)
            if event == 'start' and tag == 'clade':
                assert phylogeny.root is None, \
                        "Phylogeny object should only have 1 clade"
                phylogeny.root = self._parse_clade(elem)
                continue
            if event == 'end':
                if tag == 'phylogeny':
                    parent.clear()
                    break
                # Handle the other non-recursive children
                if tag in list_types:
                    getattr(phylogeny, list_types[tag]).append(
                            getattr(self, tag)(elem))
                # Complex types
                elif tag in ('date', 'id'):
                    setattr(phylogeny, tag, getattr(self, tag)(elem))
                # Simple types
                elif tag in ('name', 'description'):
                    setattr(phylogeny, tag, _collapse_wspace(elem.text))
                # Unknown tags
                elif namespace != NAMESPACES['phy']:
                    phylogeny.other.append(self.other(elem, namespace, tag))
                    parent.clear()
                else:
                    # NB: This shouldn't happen in valid files
                    raise PhyloXMLError('Misidentified tag: ' + tag)
        return phylogeny

    _clade_complex_types = ['color', 'events', 'binary_characters', 'date']
    _clade_list_types = {
            'confidence':   'confidences',
            'distribution': 'distributions',
            'reference':    'references',
            'property':     'properties',
            }
    _clade_tracked_tags = set(_clade_complex_types + _clade_list_types.keys()
                              + ['branch_length', 'name', 'node_id', 'width'])

    def _parse_clade(self, parent):
        """Parse a Clade node and its children, recursively."""
        clade = PX.Clade(**parent.attrib)
        if clade.branch_length is not None:
            clade.branch_length = float(clade.branch_length)
        # NB: Only evaluate nodes at the current level
        tag_stack = []
        for event, elem in self.context:
            namespace, tag = _split_namespace(elem.tag)
            if event == 'start':
                if tag == 'clade':
                    clade.clades.append(self._parse_clade(elem))
                    continue
                if tag == 'taxonomy':
                    clade.taxonomies.append(self._parse_taxonomy(elem))
                    continue
                if tag == 'sequence':
                    clade.sequences.append(self._parse_sequence(elem))
                    continue
                if tag in self._clade_tracked_tags:
                    tag_stack.append(tag)
            if event == 'end':
                if tag == 'clade':
                    elem.clear()
                    break
                if tag != tag_stack[-1]:
                    continue
                tag_stack.pop()
                # Handle the other non-recursive children
                if tag in self._clade_list_types:
                    getattr(clade, self._clade_list_types[tag]).append(
                            getattr(self, tag)(elem))
                elif tag in self._clade_complex_types:
                    setattr(clade, tag, getattr(self, tag)(elem))
                elif tag == 'branch_length':
                    # NB: possible collision with the attribute
                    if clade.branch_length is not None:
                        raise PhyloXMLError(
                                'Attribute branch_length was already set '
                                'for this Clade.')
                    clade.branch_length = _float(elem.text)
                elif tag == 'width':
                    clade.width = _float(elem.text)
                elif tag == 'name':
                    clade.name = _collapse_wspace(elem.text)
                elif tag == 'node_id':
                    clade.node_id = PX.Id(elem.text.strip(),
                                          elem.attrib.get('provider'))
                elif namespace != NAMESPACES['phy']:
                    clade.other.append(self.other(elem, namespace, tag))
                    elem.clear()
                else:
                    raise PhyloXMLError('Misidentified tag: ' + tag)
        return clade

    def _parse_sequence(self, parent):
        sequence = PX.Sequence(**parent.attrib)
        for event, elem in self.context:
            namespace, tag = _split_namespace(elem.tag)
            if event == 'end':
                if tag == 'sequence':
                    parent.clear()
                    break
                if tag in ('accession', 'mol_seq', 'uri',
                        'domain_architecture'):
                    setattr(sequence, tag, getattr(self, tag)(elem))
                elif tag == 'annotation':
                    sequence.annotations.append(self.annotation(elem))
                elif tag == 'name': 
                    sequence.name = _collapse_wspace(elem.text)
                elif tag in ('symbol', 'location'):
                    setattr(sequence, tag, elem.text)
                elif namespace != NAMESPACES['phy']:
                    sequence.other.append(self.other(elem, namespace, tag))
                    parent.clear()
        return sequence

    def _parse_taxonomy(self, parent):
        taxonomy = PX.Taxonomy(**parent.attrib)
        for event, elem in self.context:
            namespace, tag = _split_namespace(elem.tag)
            if event == 'end':
                if tag == 'taxonomy':
                    parent.clear()
                    break
                if tag in ('id', 'uri'):
                    setattr(taxonomy, tag, getattr(self, tag)(elem))
                elif tag == 'common_name':
                    taxonomy.common_names.append(_collapse_wspace(elem.text))
                elif tag == 'synonym':
                    taxonomy.synonyms.append(elem.text)
                elif tag in ('code', 'scientific_name', 'authority', 'rank'):
                    # ENH: check_str on rank
                    setattr(taxonomy, tag, elem.text)
                elif namespace != NAMESPACES['phy']:
                    taxonomy.other.append(self.other(elem, namespace, tag))
                    parent.clear()
        return taxonomy

    def other(self, elem, namespace, localtag):
        return PX.Other(localtag, namespace, elem.attrib,
                  value=elem.text and elem.text.strip() or None,
                  children=[self.other(child, *_split_namespace(child.tag))
                            for child in elem])

    # Complex types

    def accession(self, elem):
        return PX.Accession(elem.text.strip(), elem.get('source'))

    def annotation(self, elem):
        return PX.Annotation(
                desc=_collapse_wspace(_get_child_text(elem, 'desc')),
                confidence=_get_child_as(elem, 'confidence', self.confidence),
                properties=_get_children_as(elem, 'property', self.property),
                uri=_get_child_as(elem, 'uri', self.uri),
                **elem.attrib)

    def binary_characters(self, elem):
        def bc_getter(elem):
            return _get_children_text(elem, 'bc')
        return PX.BinaryCharacters(
                type=elem.get('type'),
                gained_count=_int(elem.get('gained_count')),
                lost_count=_int(elem.get('lost_count')),
                present_count=_int(elem.get('present_count')),
                absent_count=_int(elem.get('absent_count')),
                # Flatten BinaryCharacterList sub-nodes into lists of strings
                gained=_get_child_as(elem, 'gained', bc_getter),
                lost=_get_child_as(elem, 'lost', bc_getter),
                present=_get_child_as(elem, 'present', bc_getter),
                absent=_get_child_as(elem, 'absent', bc_getter))

    def clade_relation(self, elem):
        return PX.CladeRelation(
                elem.get('type'), elem.get('id_ref_0'), elem.get('id_ref_1'),
                distance=elem.get('distance'),
                confidence=_get_child_as(elem, 'confidence', self.confidence))

    def color(self, elem):
        red, green, blue = (_get_child_text(elem, color, int) for color in
                            ('red', 'green', 'blue'))
        return PX.BranchColor(red, green, blue)

    def confidence(self, elem):
        return PX.Confidence(
                _float(elem.text),
                elem.get('type'))

    def date(self, elem):
        return PX.Date(
                unit=elem.get('unit'),
                desc=_collapse_wspace(_get_child_text(elem, 'desc')),
                value=_get_child_text(elem, 'value', float),
                minimum=_get_child_text(elem, 'minimum', float),
                maximum=_get_child_text(elem, 'maximum', float),
                )

    def distribution(self, elem):
        return PX.Distribution(
                desc=_collapse_wspace(_get_child_text(elem, 'desc')),
                points=_get_children_as(elem, 'point', self.point),
                polygons=_get_children_as(elem, 'polygon', self.polygon))

    def domain(self, elem):
        return PX.ProteinDomain(elem.text.strip(),
                int(elem.get('from')) - 1,
                int(elem.get('to')),
                confidence=_float(elem.get('confidence')),
                id=elem.get('id'))

    def domain_architecture(self, elem):
        return PX.DomainArchitecture(
                length=int(elem.get('length')),
                domains=_get_children_as(elem, 'domain', self.domain))

    def events(self, elem):
        return PX.Events(
                type=_get_child_text(elem, 'type'),
                duplications=_get_child_text(elem, 'duplications', int),
                speciations=_get_child_text(elem, 'speciations', int),
                losses=_get_child_text(elem, 'losses', int),
                confidence=_get_child_as(elem, 'confidence', self.confidence))

    def id(self, elem):
        provider = elem.get('provider') or elem.get('type')
        return PX.Id(elem.text.strip(), provider)

    def mol_seq(self, elem):
        is_aligned = elem.get('is_aligned')
        if is_aligned is not None:
            is_aligned = _str2bool(is_aligned)
        return PX.MolSeq(elem.text.strip(), is_aligned=is_aligned)

    def point(self, elem):
        return PX.Point(
                elem.get('geodetic_datum'),
                _get_child_text(elem, 'lat', float),
                _get_child_text(elem, 'long', float),
                alt=_get_child_text(elem, 'alt', float),
                alt_unit=elem.get('alt_unit'))

    def polygon(self, elem):
        return PX.Polygon(
                points=_get_children_as(elem, 'point', self.point))

    def property(self, elem):
        return PX.Property(elem.text.strip(),
                elem.get('ref'), elem.get('applies_to'), elem.get('datatype'),
                unit=elem.get('unit'),
                id_ref=elem.get('id_ref'))

    def reference(self, elem):
        return PX.Reference(
                doi=elem.get('doi'),
                desc=_get_child_text(elem, 'desc'))

    def sequence_relation(self, elem):
        return PX.SequenceRelation(
                elem.get('type'), elem.get('id_ref_0'), elem.get('id_ref_1'),
                distance=_float(elem.get('distance')),
                confidence=_get_child_as(elem, 'confidence', self.confidence))

    def uri(self, elem):
        return PX.Uri(elem.text.strip(),
                desc=_collapse_wspace(elem.get('desc')),
                type=elem.get('type'))



# ---------------------------------------------------------
# OUTPUT
# ---------------------------------------------------------

def _serialize(value):
    """Convert a Python primitive to a phyloXML-compatible Unicode string."""
    if isinstance(value, float):
        return unicode(value).upper()
    elif isinstance(value, bool):
        return unicode(value).lower()
    return unicode(value)


def _clean_attrib(obj, attrs):
    """Create a dictionary from an object's specified, non-None attributes."""
    out = {}
    for key in attrs:
        val = getattr(obj, key)
        if val is not None:
            out[key] = _serialize(val)
    return out


def _handle_complex(tag, attribs, subnodes, has_text=False):
    def wrapped(self, obj):
        elem = ElementTree.Element(tag, _clean_attrib(obj, attribs))
        for subn in subnodes:
            if isinstance(subn, basestring):
                # singular object: method and attribute names are the same
                if getattr(obj, subn) is not None:
                    elem.append(getattr(self, subn)(getattr(obj, subn)))
            else:
                # list: singular method, pluralized attribute name
                method, plural = subn
                for item in getattr(obj, plural):
                    elem.append(getattr(self, method)(item))
        if has_text:
            elem.text = _serialize(obj.value)
        return elem
    wrapped.__doc__ = "Serialize a %s and its subnodes, in order." % tag
    return wrapped


def _handle_simple(tag):
    def wrapped(self, obj):
        elem = ElementTree.Element(tag)
        elem.text = _serialize(obj)
        return elem
    wrapped.__doc__ = "Serialize a simple %s node." % tag
    return wrapped


class Writer(object):
    """Methods for serializing a PhyloXML object to XML."""

    def __init__(self, phyloxml):
        """Build an ElementTree from a PhyloXML object."""
        assert isinstance(phyloxml, PX.Phyloxml), "Not a Phyloxml object"
        self._tree = ElementTree.ElementTree(self.phyloxml(phyloxml))

    def write(self, file, encoding='utf-8', indent=True):
        if indent:
            _indent(self._tree.getroot())
        self._tree.write(file, encoding)
        return len(self._tree.getroot())

    # Convert classes to ETree elements

    def phyloxml(self, obj):
        elem = ElementTree.Element(_ns('phyloxml'),
                # NB: This is for XSD validation, which we don't do
                # {_ns('schemaLocation', NAMESPACES['xsi']):
                #     obj.attributes['schemaLocation'],
                #     }
                )
        for tree in obj.phylogenies:
            elem.append(self.phylogeny(tree))
        for otr in obj.other:
            elem.append(self.other(otr))
        return elem

    def other(self, obj):
        elem = ElementTree.Element(_ns(obj.tag, obj.namespace), obj.attributes)
        elem.text = obj.value
        for child in obj.children:
            elem.append(self.other(child))
        return elem

    phylogeny = _handle_complex(_ns('phylogeny'),
            ('rooted', 'rerootable', 'branch_length_unit', 'type'),
            ( 'name',
              'id',
              'description',
              'date',
              ('confidence',        'confidences'),
              'clade',
              ('clade_relation',    'clade_relations'),
              ('sequence_relation', 'sequence_relations'),
              ('property',          'properties'),
              ('other',             'other'),
              ))

    clade = _handle_complex(_ns('clade'), ('id_source',),
            ( 'name',
              'branch_length',
              ('confidence',    'confidences'),
              'width',
              'color',
              'node_id',
              ('taxonomy',      'taxonomies'),
              ('sequence',      'sequences'),
              'events',
              'binary_characters',
              ('distribution',  'distributions'),
              'date',
              ('reference',     'references'),
              ('property',      'properties'),
              ('clade',         'clades'),
              ('other',         'other'),
              ))

    accession = _handle_complex(_ns('accession'), ('source',),
            (), has_text=True)

    annotation = _handle_complex(_ns('annotation'),
            ('ref', 'source', 'evidence', 'type'),
            ( 'desc',
              'confidence',
              ('property',   'properties'),
              'uri',
              ))

    def binary_characters(self, obj):
        """Serialize a binary_characters node and its subnodes."""
        elem = ElementTree.Element(_ns('binary_characters'),
                _clean_attrib(obj,
                    ('type', 'gained_count', 'lost_count',
                        'present_count', 'absent_count')))
        for subn in ('gained', 'lost', 'present', 'absent'):
            subelem = ElementTree.Element(_ns(subn))
            for token in getattr(obj, subn):
                subelem.append(self.bc(token))
            elem.append(subelem)
        return elem

    clade_relation = _handle_complex(_ns('clade_relation'),
            ('id_ref_0', 'id_ref_1', 'distance', 'type'),
            ('confidence',))

    color = _handle_complex(_ns('color'), (), ('red', 'green', 'blue'))

    confidence = _handle_complex(_ns('confidence'), ('type',),
            (), has_text=True)

    date = _handle_complex(_ns('date'), ('unit',),
            ('desc', 'value', 'minimum', 'maximum'))

    distribution = _handle_complex(_ns('distribution'), (),
            ( 'desc',
              ('point',     'points'),
              ('polygon',   'polygons'),
              ))

    def domain(self, obj):
        """Serialize a domain node."""
        elem = ElementTree.Element(_ns('domain'),
                {'from': str(obj.start + 1), 'to': str(obj.end)})
        if obj.confidence is not None:
            elem.set('confidence', _serialize(obj.confidence))
        if obj.id is not None:
            elem.set('id', obj.id)
        elem.text = _serialize(obj.value)
        return elem

    domain_architecture = _handle_complex(_ns('domain_architecture'),
            ('length',),
            (('domain', 'domains'),))

    events = _handle_complex(_ns('events'), (),
            ( 'type',
              'duplications',
              'speciations',
              'losses',
              'confidence',
              ))

    id = _handle_complex(_ns('id'), ('provider',), (), has_text=True)

    mol_seq = _handle_complex(_ns('mol_seq'), ('is_aligned',),
            (), has_text=True)

    node_id = _handle_complex(_ns('node_id'), ('provider',), (), has_text=True)

    point = _handle_complex(_ns('point'), ('geodetic_datum', 'alt_unit'),
            ('lat', 'long', 'alt'))

    polygon = _handle_complex(_ns('polygon'), (), (('point', 'points'),))

    property = _handle_complex(_ns('property'),
            ('ref', 'unit', 'datatype', 'applies_to', 'id_ref'),
            (), has_text=True)

    reference = _handle_complex(_ns('reference'), ('doi',), ('desc',))

    sequence = _handle_complex(_ns('sequence'),
            ('type', 'id_ref', 'id_source'),
            ( 'symbol',
              'accession',
              'name',
              'location',
              'mol_seq',
              'uri',
              ('annotation', 'annotations'),
              'domain_architecture',
              ('other',      'other'),
              ))

    sequence_relation = _handle_complex(_ns('sequence_relation'),
            ('id_ref_0', 'id_ref_1', 'distance', 'type'),
            ('confidence',))

    taxonomy = _handle_complex(_ns('taxonomy'),
            ('id_source',),
            ( 'id',
              'code',
              'scientific_name',
              'authority',
              ('common_name',   'common_names'),
              ('synonym',       'synonyms'),
              'rank',
              'uri',
              ('other',         'other'),
              ))

    uri = _handle_complex(_ns('uri'), ('desc', 'type'), (), has_text=True)

    # Primitive types

    # Floating point
    alt = _handle_simple(_ns('alt'))
    branch_length = _handle_simple(_ns('branch_length'))
    lat = _handle_simple(_ns('lat'))
    long = _handle_simple(_ns('long'))
    maximum = _handle_simple(_ns('maximum'))
    minimum = _handle_simple(_ns('minimum'))
    value = _handle_simple(_ns('value'))
    width = _handle_simple(_ns('width'))

    # Integers
    blue = _handle_simple(_ns('blue'))
    duplications = _handle_simple(_ns('duplications'))
    green = _handle_simple(_ns('green'))
    losses = _handle_simple(_ns('losses'))
    red = _handle_simple(_ns('red'))
    speciations = _handle_simple(_ns('speciations'))

    # Strings
    bc = _handle_simple(_ns('bc'))
    code = _handle_simple(_ns('code'))
    common_name = _handle_simple(_ns('common_name'))
    desc = _handle_simple(_ns('desc'))
    description = _handle_simple(_ns('description'))
    location = _handle_simple(_ns('location'))
    mol_seq = _handle_simple(_ns('mol_seq'))
    name = _handle_simple(_ns('name'))
    rank = _handle_simple(_ns('rank'))
    scientific_name = _handle_simple(_ns('scientific_name'))
    symbol = _handle_simple(_ns('symbol'))
    synonym = _handle_simple(_ns('synonym'))
    type = _handle_simple(_ns('type'))

