# lang.py - dot language creation helpers

"""Quote strings to be valid DOT identifiers, assemble attribute lists."""

import re
import collections

from . import _compat, tools

__all__ = ['quote', 'quote_edge', 'a_list', 'attr_list']

# http://www.graphviz.org/doc/info/lang.html

ID = re.compile(r'([a-zA-Z_][a-zA-Z0-9_]*|-?(\.\d+|\d+(\.\d*)?))$')

KEYWORD = re.compile(r'(?:node|edge|graph|digraph|subgraph|strict)$', re.IGNORECASE)

HTML_STRING = re.compile(r'<.*>$', re.DOTALL)

COMPASS = re.compile(r'(?:n|ne|e|se|s|sw|w|nw|c|_)$')  # TODO


def quote(identifier,
          valid_id=ID.match, dot_keyword=KEYWORD.match, html=HTML_STRING.match):
    """Return DOT identifier from string, quote if needed.

    >>> quote('')
    '""'

    >>> quote('spam')
    'spam'

    >>> quote('spam spam')
    '"spam spam"'

    >>> quote('-4.2')
    '-4.2'

    >>> quote('.42')
    '.42'

    >>> quote('<<b>spam</b>>')
    '<<b>spam</b>>'

    >>> quote(nohtml('<>'))
    '"<>"'
    """
    if html(identifier) and not isinstance(identifier, NoHtml):
        pass
    elif not valid_id(identifier) or dot_keyword(identifier):
        return '"%s"' % identifier.replace('"', '\\"')
    return identifier


def quote_edge(identifier):
    """Return DOT edge statement node_id from string, quote if needed.

    >>> quote_edge('spam')
    'spam'

    >>> quote_edge('spam spam:eggs eggs')
    '"spam spam":"eggs eggs"'

    >>> quote_edge('spam:eggs:s')
    'spam:eggs:s'
    """
    node, _, rest = identifier.partition(':')
    parts = [quote(node)]
    if rest:
        port, _, compass = rest.partition(':')
        parts.append(quote(port))
        if compass:
            parts.append(compass)
    return ':'.join(parts)


def a_list(label=None, kwargs=None, attributes=None):
    """Return assembled DOT a_list string.

    >>> a_list('spam', {'spam': None, 'ham': 'ham ham', 'eggs': ''})
    'label=spam eggs="" ham="ham ham"'
    """
    result = ['label=%s' % quote(label)] if label is not None else []
    if kwargs:
        items = ['%s=%s' % (quote(k), quote(v))
            for k, v in tools.mapping_items(kwargs) if v is not None]
        result.extend(items)
    if attributes:
        if hasattr(attributes, 'items'):
            attributes = tools.mapping_items(attributes)
        items = ['%s=%s' % (quote(k), quote(v))
            for k, v in attributes if v is not None]
        result.extend(items)
    return ' '.join(result)


def attr_list(label=None, kwargs=None, attributes=None):
    """Return assembled DOT attribute list string.

    Sorts kwargs and attributes if they are plain dicts (to avoid
    unpredictable order from hash randomization in Python 3 versions).

    >>> attr_list()
    ''

    >>> attr_list('spam spam', kwargs={'eggs': 'eggs', 'ham': 'ham ham'})
    ' [label="spam spam" eggs=eggs ham="ham ham"]'

    >>> attr_list(kwargs={'spam': None, 'eggs': ''})
    ' [eggs=""]'
    """
    content = a_list(label, kwargs, attributes)
    if not content:
        return ''
    return ' [%s]' % content


class NoHtml(object):
    """Mixin for string subclasses disabling fall-through of ``<...>``."""

    __slots__ = ()

    _doc = "%s subclass that does not treat ``<...>`` as DOT HTML string."

    @classmethod
    def _subcls(cls, other):
        name = '%s_%s' % (cls.__name__, other.__name__)
        bases = (other, cls)
        ns = {'__doc__': cls._doc % other.__name__}
        return type(name, bases, ns)


NOHTML = collections.OrderedDict((c, NoHtml._subcls(c)) for c in _compat.string_classes)


def nohtml(s):
    """Return copy of ``s`` that will not treat ``<...>`` as DOT HTML string in quoting.

    Args:
        s: String in which leading ``<`` and trailing ``>`` should be treated as literal.
    Raises:
        TypeError: If s is not a ``str`` on Python 3, or a ``str``/``unicode`` on Python 2.

    >>> quote('<>-*-<>')
    '<>-*-<>'

    >>> quote(nohtml('<>-*-<>'))
    '"<>-*-<>"'
    """
    try:
        subcls = NOHTML[type(s)]
    except KeyError:
        raise TypeError('%r does not have one of the required types: %r' %
                        (s, list(NOHTML)))
    return subcls(s) 

@@ -276,16 +276,63 @@
 g.html%0A%0A
+HTML_STRING = re.compile(r'%3C.*%3E$', re.DOTALL)%0A%0A
 ID = re.
@@ -403,59 +403,58 @@
 WORD
+S
  = 
-re.compile(r'(?:node%7Cedge%7C
+%7B'node', 'edge', '
 graph
-%7C
+', '
 digraph
-%7C
+', '
 subgraph
 %7Cstr
@@ -453,136 +453,86 @@
 raph
-%7C
+', '
 strict
-)$', re.IGNORECASE)%0A%0AHTML_STRING = re.compile(r'%3C.*%3E$', re.DOTALL)%0A%0ACOMPASS = re.compile(r'(?:n%7Cne%7Ce%7Cse%7Cs%7Csw%7Cw%7Cnw%7Cc%7C_)$')
+'%7D%0A%0ACOMPASS = %7B'n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw', 'c', '_'%7D
   # 
@@ -569,16 +569,40 @@
         
+ html=HTML_STRING.match,
  valid_i
@@ -628,16 +628,17 @@
 word
+s
 =KEYWORD
 .mat
@@ -637,38 +637,9 @@
 WORD
-.match, html=HTML_STRING.match
+S
 ):%0A 
@@ -1088,31 +1088,42 @@
  or 
-dot_keyword(identifier)
+identifier.lower() in dot_keywords
 :%0A  

switch keyword check from re to lowercase set containment, reorder