"""
This module defines the elements of the document object model (DOM).
This implementation follows most of the recommendations of w3_.

.. _w3: http://www.w3.org/TR/2012/WD-dom-20121206/

Inheritance Tree
----------------

|   :class:`lexor.core.node.Node` (``__builtin__.object``)
|        :class:`.CharacterData`
|             :class:`.Text`
|             :class:`.ProcessingInstruction`
|             :class:`.Comment`
|             :class:`.CData`
|             :class:`.Entity`
|             :class:`.DocumentType`
|        :class:`.Element`
|             :class:`.RawText` (:class:`.Element`, :class:`.CharacterData`)
|             :class:`.Void`
|             :class:`.Document`
|                  :class:`.DocumentFragment`

----------------------------------------------------------------------

"""
import os
import sys
from lexor.core import Node
LC = sys.modules['lexor.core']
# pylint: disable=too-many-public-methods
# pylint: disable=too-many-instance-attributes


class CharacterData(Node):
    """A simple interface to deal with strings. """

    __slots__ = ('data')

    def __init__(self, text=''):
        """Set the data property to the value of `text` and set its
        name to ``'#character-data'``. """
        Node.__init__(self)
        self.name = '#character-data'
        self.data = text

    @property
    def node_value(self):
        """Return or set the value of the node. This property is a
        wrapper for the ``data`` attribute. """
        return self.data

    @node_value.setter
    def node_value(self, value):
        """Setter function for data attribute. """
        self.data = value


class Text(CharacterData):
    """A node to represent a string object."""

    __slots__ = ()

    def __init__(self, text=''):
        """Call its base constructor and set its name to ``'#text'``.
        """
        CharacterData.__init__(self, text)
        self.name = '#text'

    def clone_node(self, _=True):
        """Return a new ``Text`` node with the same data content. """
        return Text(self.data)


class ProcessingInstruction(CharacterData):
    """Represents a "processing instruction", used to keep
    processor-specific information in the text of the document. """
    __slots__ = ('_target')

    def __init__(self, target, data=''):
        """Create a `Text` node with its `data` set to data. """
        CharacterData.__init__(self, data)
        self.name = target
        self._target = target

    @property
    def target(self):
        """The target of this processing instruction."""
        return self._target

    @target.setter
    def target(self, new_target):
        """Setter function. """
        self.name = new_target
        self._target = new_target

    def clone_node(self, _=True):
        """Returns a new PI with the same data content. """
        return ProcessingInstruction(self._target, self.data)


class Comment(CharacterData):
    """A node to store comments. """

    __slots__ = ('type')

    def __init__(self, data=''):
        """Create a comment node. """
        CharacterData.__init__(self, data)
        self.name = '#comment'
        self.type = None

    @property
    def comment_type(self):
        """Type of comment. This property is meant to help with
        documents that support different styles of comments. """
        return self.type

    @comment_type.setter
    def comment_type(self, comment_type):
        """Setter function for comment_type. """
        self.type = comment_type

    def clone_node(self, _=True):
        """Returns a new comment with the same data content. """
        node = Comment(self.data)
        node.type = self.type
        return node


class CData(CharacterData):
    """Although this node has been deprecated from the DOM_, it
    seems that xml still uses it.

    .. _DOM: https://developer.mozilla.org/en-US/docs/Web/API/Node.nodeType

    """

    __slots__ = ()

    def __init__(self, data=''):
        """Create a CDATA node and set the node name to
        ``'#cdata-section'``."""
        CharacterData.__init__(self, data)
        self.name = '#cdata-section'

    def clone_node(self, _=True):
        """Returns a new ``CData`` node with the same data content.
        """
        return CData(self.data)


class Entity(CharacterData):
    """From merriam-webster definition_:

    - *Something that exists by itself*.
    - *Something that is separate from other things*.

    This node acts in the same way as a :class:`.Text` node but it
    has one main difference. The data it contains should contain no
    white spaces. This node should be reserved for special characters
    or words that have different meanings across different languages.
    For instance in HTML you have the ``&amp;`` to represent ``&``.
    In LaTeX you have to type ``\\$`` to represent ``$``. Using this
    node will help you handle these Entities hopefully more
    efficiently than simply finding and replacing them in a Text node.

    .. _definition: http://www.merriam-webster.com/dictionary/entity

    """

    __slots__ = ()

    def __init__(self, text=''):
        """Create an ``Entity`` node and set the node name to
        ``#entity``."""
        CharacterData.__init__(self, text)
        self.name = '#entity'

    def clone_node(self, _=True):
        """Returns a new ``Entity`` with the same data content. """
        return Entity(self.data)


class DocumentType(CharacterData):
    """A node to store the doctype declaration. This node will not
    follow the specifications at this point (May 30, 2013). It will
    simply recieve the string in between ``<!doctype`` and ``>``.

    Specs: http://www.w3.org/TR/2012/WD-dom-20121206/#documenttype

    """
    __slots__ = ()

    def __init__(self, data=''):
        """Create a ``DocumentType`` node and set its name to
        ``#doctype``. """
        CharacterData.__init__(self, data)
        self.name = '#doctype'
        # The next properties should be obtained from data
        # The specs do not mention type, instead they mention name.
        # The node name here is #doctype so that it can be easily
        # identified. The doctype "name" as they refer is called type.

    def clone_node(self, _=True):
        """Returns a new doctype with the same data content. """
        node = DocumentType(self.data)
        return node


class Element(Node):
    """Node object configured to have child Nodes and attributes. """

    def __init__(self, name, data=None):
        """The parameter ``data`` should be a ``dict`` object. The
        element will use the keys and values to populate its
        attributes. You may modify the elements internal dictionary.
        However, this may unintentially overwrite the attributes
        defined by the ``__setitem__`` method. If you wish to add
        another attribute to the ``Element`` object use the
        convention of adding an underscore at the end of the
        attribute. i.e

        >>> strong = Element('strong')
        >>> strong.message_ = 'An internal message'
        >>> strong['message'] = 'Attribute message'

        """
        Node.__init__(self)
        if data is None:
            data = dict()
        self.__dict__.update(data)
        if isinstance(data, dict):
            self._order = data.keys()
        else:
            self._order = list()
            for key, _ in data:
                if key not in self._order:
                    self._order.append(key)
        self.name = name
        self.child = list()

    def __call__(self, selector):
        """Return a :class:`lexor.core.selector.Selector` object. """
        return LC.Selector(selector, self)

    def update_attributes(self, node):
        """Copies the attributes of the input node into the calling
        node. """
        for k in node:
            self.__dict__[k] = node.__dict__[k]
            if k not in self._order:
                self._order.append(k)

    def __getitem__(self, k):
        """Return the `k`-th child of this node if `k` is an integer.
        Otherwise return the attribute of name with value of `k`.

        >>> x.__getitem__(k) is x[k]
        True

        """
        if isinstance(k, str):
            return self.__dict__[k]
        if self.child:
            return self.child[k]
        return None

    def get(self, k, val=''):
        """Return the attribute of name with value of `k`."""
        return self.__dict__.get(k, val)

    def __setitem__(self, k, val):
        """Overloaded array operator. Appends or modifies an
        attribute. See its base method
        :meth:`lexor.core.node.Node.__setitem__` for documentation on
        when `val` is not string.

        >>> x.__setitem__(attname) = 'att' <==> x[attname] = 'att'

        """
        if isinstance(k, str):
            self.__dict__[k] = val
            if k not in self._order:
                self._order.append(k)
            if k == 'id' and self.owner:
                self.owner.id_dict[k] = self
        else:
            Node.__setitem__(self, k, val)

    def __delitem__(self, k):
        """Remove a child or attribute.

        >>> x.__delitem__(k) <==> del x[k]

        """
        if isinstance(k, str):
            self.__dict__.__delitem__(k)
            self._order.remove(k)
        else:
            Node.__delitem__(self, k)

    def __contains__(self, obj):
        """Return ``True`` if `obj` is a node and it is a child
        of this element or if `obj` is an attribute of this
        element. Return ``False`` otherwise.

        >>> x.__contains__(obj) == obj in x
        True

        """
        if isinstance(obj, Node):
            return self.child.__contains__(obj)
        else:
            return self._order.__contains__(obj)

    def contains(self, obj):
        """Unlike ``__contains__``, this method returns ``True`` if
        `obj` is any of the desendents of the node. """
        if obj.level < self.level + 1:
            return False
        while obj.level > self.level + 1:
            obj = obj.parent
            if obj is None:
                return False
        return obj in self

    def __iter__(self):
        """Iterate over the element attributes names.

        >>> for attribute_name in node: ...

        """
        for k in self._order:
            yield k

    @property
    def attlen(self):
        """The number of attributes. """
        return len(self._order)

    @property
    def attributes(self):
        """Return a list of the attribute names in the element. """
        return list(self._order)

    @property
    def values(self):
        """Return a list of the attribute values in the Element. """
        return [self.__dict__[k] for k in self._order]

    def attribute(self, index):
        """Return the name of the attribute at the specified index. """
        return self._order[index]

    def attr(self, index):
        """Return the value of the attribute at the specified index.
        """
        return self.__dict__[self._order[index]]

    def items(self):
        """return all the items. """
        return zip(self._order, self.values)

    def update(self, dict_):
        """update with the values of `dict_`. useful when the element
        is empty and you created an Attr object. then just update the
        values."""
        for key, val in dict_.items():
            self.__setitem__(key, val)

    def rename(self, old_name, new_name):
        """Renames an attribute. 

        >>> from lexor.core.elements import Element
        >>> node = Element('div')
        >>> node['att1'] = 'val1'
        >>> node
        div[0x10a090750 att1="val1"]:
        >>> node.rename('att1', 'new-att-name')
        >>> node
        div[0x10a090750 new-att-name="val1"]:
        
        """
        if isinstance(old_name, str):
            index = self._order.index(old_name)
        else:
            index = old_name  # Assume old_name
        self.__dict__[new_name] = self.__dict__[self._order[index]]
        del self.__dict__[self._order[index]]
        self._order[index] = new_name

    def clone_node(self, deep=False, normalize=True):
        """Returns a new node. When deep is True, it will clone also
        clone all the child nodes."""
        # May want to provide a node to which the clone will be
        # appended to. If this is done then we will not have to
        # traverse through all the elements of the node to adjust
        # the level of the child nodes when we move the node around
        node = Element(self.name)
        node.update_attributes(self)
        if deep is False or not self.child:
            return node
        crt = self
        crtcopy = node
        direction = 'd'
        while True:
            if direction is 'd':
                crt = crt.child[0]
                clone = crt.clone_node()
                crtcopy.append_child(clone)
            elif direction is 'r':
                if crt.next is None:
                    direction = 'u'
                    continue
                crt = crt.next
                clone = crt.clone_node()
                crtcopy.parent.append_child(clone)
            elif direction is 'u':
                crtcopy = crtcopy.parent
                if normalize:
                    crtcopy.normalize()
                if crt.parent is self:
                    break
                if crt.parent.next is None:
                    crt = crt.parent
                    continue
                crt = crt.parent.next
                clone = crt.clone_node()
                crtcopy.parent.append_child(clone)
            crtcopy = clone
            if crt.child:
                direction = 'd'
            else:
                direction = 'r'
        return node

    def get_elements_by_class_name(self, classname):
        """Return a list of all child elements which have all of the
        given class names. """
        nodes = []
        if not self.child:
            return nodes
        patterns = set([i.strip() for i in classname.split()])
        crt = self
        direction = 'd'
        while True:
            if direction is 'd':
                crt = crt.child[0]
            elif direction is 'r':
                if crt.next is None:
                    direction = 'u'
                    continue
                crt = crt.next
            elif direction is 'u':
                if crt.parent is self:
                    break
                if crt.parent.next is None:
                    crt = crt.parent
                    continue
                crt = crt.parent.next
            if isinstance(crt, Element) and 'class' in crt:
                crtclass = [i.strip() for i in crt['class'].split()]
                if patterns.issubset(set(crtclass)):
                    nodes.append(crt)
            if crt.child:
                direction = 'd'
            else:
                direction = 'r'
        return nodes

    def children(self, children=None, **keywords):
        """Set the elements children by providing a list of nodes or
        a string. If using a string then you may provide any of the
        following keywords to dictate how to parse and convert:

        - parser_style: ``'_'``
        - parser_lang: ``'html``
        - parser_defaults: ``None``,
        - convert_style: ``'_'``,
        - convert_from: ``None``,
        - convert_to: ``'html'``,
        - convert_defaults: ``None``,
        - convert: ``'false'``

        If no children are provided then it returns a string of the children
        written in plain html. To change this behavior provide the
        following keywords:

        - writer_style: ``'plain'``
        - writer_lang: ``'html``

        .. important::

            This requires the installation of lexor styles.

        """
        if children is None:
            lang = keywords.get('writer_lang', 'html')
            style = keywords.get('writer_style', 'plain')
            writer = LC.Writer(lang, style)
            if self.owner is not None and self.owner.defaults is not None:
                for var, val in self.owner.defaults.iteritems():
                    writer.defaults[var] = os.path.expandvars(str(val))
            for var, val in keywords.iteritems():
                writer.defaults[var] = os.path.expandvars(str(val))
            result = ''
            for child in self.child:
                writer.write(child)
                result += str(writer)
            writer.close()
            return result
        if isinstance(children, str):
            info = {
                'parser_style': '_',
                'parser_lang': 'html',
                'parser_defaults': None,
                'convert_style': '_',
                'convert_from': None,
                'convert_to': 'html',
                'convert_defaults': None,
                'convert': 'false'
            }
            for att in keywords:
                info[att] = keywords[att]
            parser = LC.Parser(info['parser_lang'],
                               info['parser_style'],
                               info['parser_defaults'])
            parser.parse(children)
            if info['convert'] == 'true' and info['convert_to'] is not None:
                if info['convert_from'] is None:
                    info['convert_from'] = info['parser_lang']
                converter = LC.Converter(info['convert_from'],
                                         info['convert_to'],
                                         info['convert_style'],
                                         info['convert_defaults'])
                converter.convert(parser.doc)
                children = converter.doc.pop()
                converter.log.pop()
            else:
                children = parser.doc
            children.temporary = True
        self.remove_children()
        self.extend_children(children)


class RawText(Element, CharacterData):
    """A few elements do not have children, instead they have data.
    Such elements exist in HTML: ``script``, ``title`` among
    others."""

    def __init__(self, name, data='', att=None):
        """You may provide `att` as a ``dict`` object. """
        CharacterData.__init__(self, data)
        Element.__init__(self, name, att)
        self.child = None

    def clone_node(self, deep=True, normalize=True):
        """Returns a new ``RawText`` element"""
        node = RawText(self.name)
        node.update_attributes(self)
        if deep is True:
            node.data = self.data
        return node


class Void(Element):
    """An element with no children. """

    def __init__(self, name, att=None):
        """You may provide `att` as a `dict` object. """
        Element.__init__(self, name, att)
        self.child = None

    def clone_node(self, _=True, normalize=True):
        """Returns a new ``Void`` element. """
        node = Void(self.name)
        node.update_attributes(self)
        return node


class Document(Element):
    """Contains information about the document that it holds. """

    def __init__(self, lang='xml', style='default'):
        """Creates a new document object and sets its name to
        ``#document``."""
        Element.__init__(self, '#document')
        self.level = -1
        self.owner = self
        self.lang = lang
        self.style = style
        self.uri_ = None
        self.defaults = None
        self.id_dict = dict()
        self.meta = dict()
        self.temporary = True

    def clone_node(self, deep=False, normalize=True):
        """Returns a new Document. Note: it does not copy
        the default values. """
        node = Document(self.lang, self.style)
        node.update_attributes(self)
        node.uri_ = self.uri_
        node.meta.update(self.meta)
        if deep is False or not self.child:
            return node
        clone = Element.clone_node(self, deep, normalize)
        clone.name = ''  # not a document
        node.extend_children(clone)
        return node

    @property
    def language(self):
        """The current document's language. This property is used by
        the writer to determine how to write the document.

        This property is a wrapper for the ``lang`` attribute. """
        return self.lang

    @language.setter
    def language(self, val):
        """Setter function for language. """
        self.lang = val

    @property
    def writing_style(self):
        """The current document's style. This property is used by
        the writer to determine how to write the document.

        This property is a wrapper for the ``style`` attribute.
        """
        return self.style

    @writing_style.setter
    def writing_style(self, val):
        """Docstring for setter. """
        self.style = val

    @property
    def uri(self):
        """The Uniform Resource Identifier. This property may become
        useful if the document represents a file. This property
        should be set by the a :class:`~lexor.core.parser.Parser`
        object telling us the location of the file that it parsed
        into the Document object. """
        return self.uri_

    @staticmethod
    def create_element(tagname, data=None):
        """Utility function to avoid having to import
        ``lexor.core.elements`` module. Returns an element object. """
        return Element(tagname, data)

    def get_element_by_id(self, element_id):
        """Return the first element, in tree order, within the
        document whose ID is element_id, or None if there is none. """
        return self.id_dict.get(element_id, None)


class DocumentFragment(Document):
    """Takes in an element and "steals" its children. This element
    should only be used as a temporary container. Note that the
    ``__str__`` method may not yield the expected results since all
    the function will do is use the ``__str__`` method in each of its
    children. First assign this object to an actual Document. """

    def __init__(self, lang='xml', style='default'):
        Document.__init__(self, lang, style)
        self.name = '#document-fragment'

    def append_child(self, new_child):
        """Adds the node new_child to the end of the list of children
        of this node. The children contained in a
        ``DocumentFragment`` only have a parent (the
        ``DocumentFragment``). As opposed as
        :meth:`lexor.core.node.Node.append_child` which also takes
        care of the ``prev`` and ``next`` attributes. """
        if isinstance(new_child, str):
            new_child = Text(new_child)
        elif not isinstance(new_child, Node):
            raise TypeError("Only Nodes can be appended.")
        if new_child.parent is not None:
            del new_child.parent[new_child.index]
        self.child.append(new_child)
        new_child.parent = self
        new_child.owner = self
        return new_child

    def __repr__(self):
        """
        >>> x.__repr__() <==> repr(x)

        """
        return ''.join([repr(node) for node in self.child])

    def __str__(self):
        """
        >>> x.__str__() <==> str(x)

        """
        return ''.join([str(node) for node in self.child])
