# 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 (objects derived from
   ``BaseTree.Tree``)

"""

import sys

from Bio._py3k import basestring
from Bio._py3k import unicode

from Bio.Phylo import PhyloXML as PX

# For speed try to use cElementTree rather than ElementTree
try:
    if (3, 0) <= sys.version_info[:2] <= (3, 1):
        # Workaround for bug in python 3.0 and 3.1,
        # see http://bugs.python.org/issue9257
        from xml.etree import ElementTree as ElementTree
    else:
        from xml.etree import cElementTree as ElementTree
except ImportError:
    from xml.etree import ElementTree as ElementTree


# Recognize the phyloXML namespace when parsing
# See http://effbot.org/zone/element-namespaces.htm
NAMESPACES = {
    'phy': 'http://www.phyloxml.org',
}

try:
    register_namespace = ElementTree.register_namespace
except AttributeError:
    if not hasattr(ElementTree, '_namespace_map'):
        # cElementTree needs the pure-Python xml.etree.ElementTree
        from xml.etree import ElementTree as ET_py
        ElementTree._namespace_map = ET_py._namespace_map

    def register_namespace(prefix, uri):
        """Set the namespace for ElementTree."""
        ElementTree._namespace_map[uri] = prefix

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

# Tell ElementTree how to write to text handles
DEFAULT_ENCODING = ("unicode" if sys.version_info[0] >= 3 else "utf-8")


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.

    :returns: 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.

    :returns: a generator of ``Bio.Phylo.PhyloXML.Phylogeny`` objects.

    """
    return Parser(file).parse()


def write(obj, file, encoding=DEFAULT_ENCODING, indent=True):
    """Write a phyloXML file.

    :Parameters:
        obj
            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.
        file
            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, 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 (PRIVATE)."""
    if tag[0] == '{':
        return tag[tag.index('}') + 1:]
    return tag


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


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


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

    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 (PRIVATE).

    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 (PRIVATE).

    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 (PRIVATE).

    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 (PRIVATE).

    Sources:
     - http://effbot.org/zone/element-lib.htm#prettyprint
     - 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):
    """Convert string to boolean (PRIVATE)."""
    if text == 'true' or text == '1':
        return True
    if text == 'false' or text == '0':
        return False
    raise ValueError('String could not be converted to boolean: ' + text)


def _dict_str2bool(dct, keys):
    """Return a new dictionary where string values are replaced with booleans (PRIVATE)."""
    out = dct.copy()
    for key in keys:
        if key in out:
            out[key] = _str2bool(out[key])
    return out


def _int(text):
    """Return text as an integer (PRIVATE)."""
    if text is not None:
        try:
            return int(text)
        except Exception:
            return None


def _float(text):
    """Return text as a float (PRIVATE)."""
    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 (PRIVATE).

    Also remove leading and trailing whitespace. See "Collapse Whitespace
    Policy" in the 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 (PRIVATE).

    See "Replace Whitespace Policy" in the 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):
        """Initialize the class."""
        # Get an iterable context for XML parsing events
        context = iter(ElementTree.iterparse(file, events=('start', 'end')))
        event, root = next(context)
        self.root = root
        self.context = context

    def read(self):
        """Parse the phyloXML file and create a single Phyloxml object."""
        phyloxml = PX.Phyloxml({_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 (PRIVATE).

        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':
                if phylogeny.root is not None:
                    raise ValueError("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).union(_clade_list_types.keys()).union(
        ['branch_length', 'name', 'node_id', 'width'])

    def _parse_clade(self, parent):
        """Parse a Clade node and its children, recursively (PRIVATE)."""
        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):
        """Parse a molecular sequence (PRIVATE)."""
        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):
        """Parse taxonomic information for a clade (PRIVATE)."""
        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):
        """Create an Other object, a non-phyloXML element."""
        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):
        """Create accession object."""
        return PX.Accession(elem.text.strip(), elem.get('source'))

    def annotation(self, elem):
        """Create annotation object."""
        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):
        """Create binary characters object."""
        def bc_getter(elem):
            """Get binary characters from subnodes."""
            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):
        """Create clade relationship object."""
        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):
        """Create branch color object."""
        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):
        """Create confidence object."""
        return PX.Confidence(
            _float(elem.text),
            elem.get('type'))

    def date(self, elem):
        """Create date object."""
        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):
        """Create geographic distribution object."""
        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):
        """Create protein domain object."""
        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):
        """Create domain architecture object."""
        return PX.DomainArchitecture(
            length=int(elem.get('length')),
            domains=_get_children_as(elem, 'domain', self.domain))

    def events(self, elem):
        """Create events object."""
        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):
        """Create identifier object."""
        provider = elem.get('provider') or elem.get('type')
        return PX.Id(elem.text.strip(), provider)

    def mol_seq(self, elem):
        """Create molecular sequence object."""
        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):
        """Create point object, coordinates of a point."""
        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):
        """Create polygon object, list of points."""
        return PX.Polygon(
            points=_get_children_as(elem, 'point', self.point))

    def property(self, elem):
        """Create properties from external resources."""
        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):
        """Create literature reference object."""
        return PX.Reference(
            doi=elem.get('doi'),
            desc=_get_child_text(elem, 'desc'))

    def sequence_relation(self, elem):
        """Create sequence relationship object, relationship between two sequences."""
        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):
        """Create uri object, expected to be a url."""
        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 (PRIVATE)."""
    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 (PRIVATE)."""
    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):
    """Handle to serialize nodes with subnodes (PRIVATE)."""
    def wrapped(self, obj):
        """Wrap nodes and subnodes as elements."""
        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):
    """Handle to serialize simple nodes (PRIVATE)."""
    def wrapped(self, obj):
        """Wrap node as element."""
        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=DEFAULT_ENCODING, indent=True):
        """Write PhyloXML to a file."""
        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):
        """Convert phyloxml to Etree element."""
        elem = ElementTree.Element('phyloxml', obj.attributes)  # Namespaces
        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):
        """Convert other to Etree element."""
        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('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('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('accession', ('source',),
                                (), has_text=True)

    annotation = _handle_complex('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('binary_characters',
                                   _clean_attrib(obj,
                                                 ('type', 'gained_count', 'lost_count',
                                                  'present_count', 'absent_count')))
        for subn in ('gained', 'lost', 'present', 'absent'):
            subelem = ElementTree.Element(subn)
            for token in getattr(obj, subn):
                subelem.append(self.bc(token))
            elem.append(subelem)
        return elem

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

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

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

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

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

    def domain(self, obj):
        """Serialize a domain node."""
        elem = ElementTree.Element('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('domain_architecture',
                                          ('length',),
                                          (('domain', 'domains'),))

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

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

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

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

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

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

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

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

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

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

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

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

    # Primitive types

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

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

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