#!/usr/bin/env python3
#
# specparser.py
#
# Reads in a spec document and generates pretty data structures from it.
#
# Copyright (C) 2009-2010 Collabora Ltd.
#
# This library is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or (at
# your option) any later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
# for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this library; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# Authors: Davyd Madeley <davyd.madeley@collabora.co.uk>
#

import sys
import xml.dom.minidom
import functools

import xincludator

XMLNS_TP = 'http://telepathy.freedesktop.org/wiki/DbusSpec#extensions-v0'

class UnknownAccess(Exception): pass
class UnknownDirection(Exception): pass
class UnknownType(Exception): pass
class UnnamedItem(Exception): pass
class UntypedItem(Exception): pass
class UnsupportedArray(Exception): pass
class BadNameForBindings(Exception): pass
class BrokenHTML(Exception): pass
class WrongNumberOfChildren(Exception): pass
class MismatchedFlagsAndEnum(Exception): pass
class TypeMismatch(Exception): pass
class MissingVersion(Exception): pass
class DuplicateEnumValueValue(Exception): pass
class BadFlagValue(Exception): pass
class BadFlagsType(Exception): pass

class Xzibit(Exception):
    def __init__(self, parent, child):
        self.parent = parent
        self.child = child

    def __str__(self):
        print ("""
    Nested <%s>s are forbidden.
    Parent:
        %s...
    Child:
        %s...
        """ % (self.parent.nodeName, self.parent.toxml()[:100],
               self.child.toxml()[:100]))

def getText(dom):
    try:
        if dom.childNodes[0].nodeType == dom.TEXT_NODE:
            return dom.childNodes[0].data
        else:
            return ''
    except IndexError:
        return ''

def getChildrenByName(dom, namespace, name):
    return list(filter(lambda n: n.nodeType == n.ELEMENT_NODE and \
                            n.namespaceURI == namespace and \
                            n.localName == name,
                      dom.childNodes))

def getChildrenByNameAndAttribute(dom, namespace, name, attribute, value):
    retu
rn list(filter(lambda n: n.nodeType == n.ELEMENT_NODE and \
                            n.namespaceURI == namespace and \
                            n.localName == name and \
                            n.getAttribute(attribute) == value,
                      dom.childNodes))

def getOnlyChildByName(dom, namespace, name):
    kids = getChildrenByName(dom, namespace, name)

    if len(kids) == 0:
        return N
one

    if len(kids) > 1:
        raise WrongNumberOfChildren(
            '<%s> node should have at most one <%s xmlns="%s"/> child' %
            (dom.tagName, name, namespace))

    return kids[0]

def getAnnotationByName(dom, name):
    kids = getChildrenByNameAndAttribute(dom, None, 'annotation', 'name', name)

    if len(kids) == 0:
        return None

    if len(kids) > 1:
        raise WrongNumberOfChildren(
            '<%s> node should have at most one %s annotation' %
            (dom.tagName, name))

    return kids[0].getAttribute('value')

def getNamespace(n):
    if n.namespaceURI is not None:
        return n.namespaceURI
    ancestor = n.parentNode

    while ancestor is not None and ancestor.nodeType == n.ELEMENT_NODE:
        if n.prefix is None:
            xmlns = ancestor.getAttribute('xmlns')
        else:
            xmlns = ancestor.getAttribute('xmlns:%s' % n.prefix)

        if xmlns is not None:
            return xmlns

        ancestor = ancestor.parentNode

def build_name(namespace, name):
    """Returns a name by appending `name' to the namespace of this object.
    """
    return '.'.join(
        filter(lambda n: n is not None and n != '',
               [namespace, name.replace(' ', '')])
        )

class Base(object):
    """The base class for any type of XML node in the spec that implements the
       'name' attribute.

       Don't instantiate this class directly.
    """
    devhelp_name = ""

    def __init__(self, parent, namespace, dom):
        self.short_name = name = dom.getAttribute('name')
        self.namespace = namespace
        self.name = build_name(namespace, name)
        self.parent = parent

        for child in dom.childNodes:
            if (child.nodeType == dom.TEXT_NODE and
                    child.data.strip() != ''):
                raise BrokenHTML('Text found in node %s of %s, did you mean '
                        'to use <tp:docstring/>? Offending text:\n\n%s' %
                    (self.__class__.__name__, self.parent, child.data.strip()))
            elif child.nodeType == dom.ELEMENT_NODE:
                if child.tagName in ('p', 'em', 'strong', 'ul', 'li', 'dl',
                        'a', 'tt', 'code'):
                    raise BrokenHTML('HTML element <%s> found in node %s of '
                            '%s, did you mean to use <tp:docstring/>?' %
                        (child.tagName, self.__class__.__name__, self.parent))

        self.docstring = getOnlyChildByName(dom, XMLNS_TP, 'docstring')
        self.added = getOnlyChildByName(dom, XMLNS_TP, 'added')
        self.deprecated = getOnlyChildByName(dom, XMLNS_TP, 'deprecated')
        if self.deprecated is None:
            self.is_deprecated = (getAnnotationByName(dom, 'org.freedesktop.DBus.Deprecated') == 'true')
        else:
            self.is_deprecated = True

        self.changed = getChildrenByName(dom, XMLNS_TP, 'changed')

        self.validate()

    def validate(self):
        if self.short_name == '':
            raise UnnamedItem("Node %s of %s has no name" % (
                self.__class__.__name__, self.parent))

    def check_consistency(self):
        pass

    def get_type_name(self):
        return self.__class__.__name__

    def get_spec(self):
        return self.parent.get_spec()

    def get_root_namespace(self):
        return self.get_interface().name

    def get_interface(self):
        return self.parent.get_interface()

    def get_anchor(self):
        return "%s:%s" % (
            self.get_type_name().replace(' ', '-'),
            self.short_name)

    def get_url(self):
        return "%s#%s" % (self.get_interface().get_url(), self.get_anchor())

    def _get_generic_with_ver(self, nnode, htmlclass, txt):
        if nnode is None:
            return ''
        else:
            # make a copy of this node, turn it into a HTML <div> tag
            node = nnode.cloneNode(True)
            node.tagName = 'div'
            node.baseURI = None
            node.setAttribute('class', 'annotation %s' % htmlclass)

            try:
                node.removeAttribute('version')

                doc = self.get_spec().document

                span = doc.createElement('span')
                span.setAttribute('class', 'version')

                text = doc.createTextNode(
                    txt % nnode.getAttribute('version') + ' ')
                span.appendChild(text)

                node.insertBefore(span, node.firstChild)
            except xml.dom.NotFoundErr:
                raise MissingVersion(
                        '%s was %s, but gives no version' % (self, htmlclass))

            self._convert_to_html(node)

            return node.toxml()

    def get_added(self):
        return self._get_generic_with_ver(self.added, 'added',
                                          "Added in %s.")

    def get_deprecated(self):
        if self.deprecated is None:
            if self.is_deprecated:
                return '<div class="annotation deprecated no-version">Deprecated.</div>'
            else:
                return ''
        else:
            return self._get_generic_with_ver(self.deprecated, 'deprecated',
  