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

from collections.abc import Sequence, Set, Mapping
import functools
import re
from typing import Final
import warnings

from . import _tools
from . import exceptions

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

# https://www.graphviz.org/doc/info/lang.html
# https://www.graphviz.org/doc/info/attrs.html#k:escString

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

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

KEYWORDS: Final[Set[str]] = {'node', 'edge', 'graph', 'digraph', 'subgraph', 'strict'}

COMPASS: Final[Set[str]] = {'n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw', 'c', '_'}  # TODO

FINAL_ODD_BACKSLASHES: Final = re.compile(r'(?<!\\)(?:\\{2})*\\$')

QUOTE_WITH_OPTIONAL_BACKSLASHES: Final = re.compile(r'''
                                                    (?P<escaped_backslashes>(?:\\{2})*)
                                                    \\?  # treat \" same as "
                                                    (?P<literal_quote>")
                                                    ''', flags=re.VERBOSE)

ESCAPE_UNESCAPED_QUOTES: Final = functools.partial(QUOTE_WITH_OPTIONAL_BACKSLASHES.sub,
                                                   r'\g<escaped_backslashes>'
                                                   r'\\'
                                                   r'\g<literal_quote>')


@_tools.deprecate_positional_args(supported_number=1)
def quote(identifier: str,
          is_html_string=HTML_STRING.match,
          is_valid_id=ID.match,
          dot_keywords=KEYWORDS,
          endswith_odd_number_of_backslashes=FINAL_ODD_BACKSLASHES.search,
          escape_unescaped_quotes=ESCAPE_UNESCAPED_QUOTES) -> str:
    r"""Return DOT identifier from string, quote if needed.

    >>> quote('')  # doctest: +NO_EXE
    '""'

    >>> 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('<>'))
    '"<>"'

    >>> print(quote('"'))
    "\""

    >>> print(quote('\\"'))
    "\""

    >>> print(quote('\\\\"'))
    "\\\""

    >>> print(quote('\\\\\\"'))
    "\\\""
    """
    if is_html_string(identifier) and not isinstance(identifier, NoHtml):
        pass
    elif not is_valid_id(identifier) or identifier.lower() in dot_keywords:
        if endswith_odd_number_of_backslashes(identifier):
            warnings.warn('expect syntax error scanning invalid quoted string:'
                          f' {identifier!r}',
                          category=exceptions.DotSyntaxWarning)
        return f'"{escape_unescaped_quotes(identifier)}"'
    return identifier


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

    >>> quote_edge('spam')  # doctest: +NO_EXE
    '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)


@_tools.deprecate_positional_args(supported_number=1)
def a_list(label: str | None = None,
           kwargs: Mapping[str, str] | None = None,
           attributes: (Mapping[str, str]
                        | Sequence[tuple[str, str]]
                        | None) = None) -> str:
    """Return assembled DOT a_list string.

    >>> a_list('spam', kwargs={'spam': None, 'ham': 'ham ham', 'eggs': ''})  # doctest: +NO_EXE
    'label=spam eggs="" ham="ham ham"'
    """
    result = [f'label={quote(label)}'] if label is not None else []
    if kwargs:
        result += [f'{quote(k)}={quote(v)}'
                   for k, v in _tools.mapping_items(kwargs) if v is not None]
    if attributes:
        items = (_tools.mapping_items(attributes)
                 if isinstance(attributes, Mapping) else attributes)
        result += [f'{quote(k)}={quote(v)}'
                   for k, v in items if v is not None]
    return ' '.join(result)


@_tools.deprecate_positional_args(supported_number=1)
def attr_list(label: str | None = None,
              kwargs: Mapping[str, str] | None = None,
              attributes: (Mapping[str, str]
                           | Sequence[tuple[str, str]]
                           | None) = None) -> str:
    """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.7).

    >>> attr_list()  # doctest: +NO_EXE
    ''

    >>> 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=kwargs, attributes=attributes)
    if not content:
        return ''
    return f' [{content}]'


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

    _quote = staticmethod(quote)
    _quote_edge = staticmethod(quote_edge)

    _a_list = staticmethod(a_list)
    _attr_list = staticmethod(attr_list)


def escape(s: str) -> str:
    r"""Return string disabling special meaning of backslashes and ``'<...>'``.

    Args:
        s: String in which backslashes and ``'<...>'``
            should be treated as literal.

    Returns:
        Escaped string subclass instance.

    Raises:
        TypeError: If ``s`` is not a ``str``.

    Example:
        >>> import graphviz  # doctest: +NO_EXE
        >>> print(graphviz.escape(r'\l'))
        \\l

    See also:
        Upstream documentation:
        https://www.graphviz.org/doc/info/attrs.html#k:escString
    """
    return nohtml(s.replace('\\', '\\\\'))


class NoHtml(str):
    """String subclass that does not treat ``'<...>'`` as DOT HTML string."""

    __slots__ = ()


def nohtml(s: str) -> str:
    """Return string not treating ``'<...>'`` as DOT HTML string in quoting.

    Args:
        s: String in which leading ``'<'`` and trailing ``'>'``
            should be treated as literal.

    Returns:
        String subclass instance.

    Raises:
        TypeError: If ``s`` is not a ``str``.

    Example:
        >>> import graphviz  # doctest: +NO_EXE
        >>> g = graphviz.Graph()
        >>> g.node(graphviz.nohtml('<>-*-<>'))
        >>> print(g.source)  # doctest: +NORMALIZE_WHITESPACE
        graph {
            "<>-*-<>"
        }
    """
    return NoHtml(s)
