"""Create DOT code with method-calls."""

import contextlib
import typing

from . import _tools
from . import base
from . import quoting

__all__ = ['GraphSyntax', 'DigraphSyntax', 'Dot']


def comment(line: str) -> str:
    """Return comment header line."""
    return f'// {line}\n'


def graph_head(name: str) -> str:
    """Return DOT graph head line."""
    return f'graph {name}{{\n'


def digraph_head(name: str) -> str:
    """Return DOT digraph head line."""
    return f'digraph {name}{{\n'


def graph_edge(*, tail: str, head: str, attr: str) -> str:
    """Return DOT graph edge statement line."""
    return f'\t{tail} -- {head}{attr}\n'


def digraph_edge(*, tail: str, head: str, attr: str) -> str:
    """Return DOT digraph edge statement line."""
    return f'\t{tail} -> {head}{attr}\n'


class GraphSyntax:
    """DOT graph head and edge syntax."""

    _head = staticmethod(graph_head)

    _edge = staticmethod(graph_edge)


class DigraphSyntax:
    """DOT digraph head and edge syntax."""

    _head = staticmethod(digraph_head)

    _edge = staticmethod(digraph_edge)


def subgraph(name: str) -> str:
    """Return DOT subgraph head line."""
    return f'subgraph {name}{{\n'


def subgraph_plain(name: str) -> str:
    """Return plain DOT subgraph head line."""
    return f'{name}{{\n'


def node(left: str, right: str) -> str:
    """Return DOT node statement line."""
    return f'\t{left}{right}\n'


class Dot(quoting.Quote, base.Base):
    """Assemble DOT source code."""

    directed: bool

    _comment = staticmethod(comment)

    @staticmethod
    def _head(name: str) -> str:  # pragma: no cover
        """Return DOT head line."""
        raise NotImplementedError('must be implemented by concrete subclasses')

    @classmethod
    def _head_strict(cls, name: str) -> str:
        """Return DOT strict head line."""
        return f'strict {cls._head(name)}'

    _tail = '}\n'

    _subgraph = staticmethod(subgraph)

    _subgraph_plain = staticmethod(subgraph_plain)

    _node = _attr = staticmethod(node)

    @classmethod
    def _attr_plain(cls, left: str) -> str:
        return cls._attr(left, '')

    @staticmethod
    def _edge(*, tail: str, head: str, attr: str) -> str:  # pragma: no cover
        """Return DOT edge statement line."""
        raise NotImplementedError('must be implemented by concrete subclasses')

    @classmethod
    def _edge_plain(cls, *, tail: str, head: str) -> str:
        """Return plain DOT edge statement line."""
        return cls._edge(tail=tail, head=head, attr='')

    def __init__(self, *,
                 name: typing.Optional[str] = None,
                 comment: typing.Optional[str] = None,
                 graph_attr=None, node_attr=None, edge_attr=None, body=None,
                 strict: bool = False, **kwargs) -> None:
        super().__init__(**kwargs)

        self.name = name
        """str: DOT source identifier for the ``graph`` or ``digraph`` statement."""

        self.comment = comment
        """str: DOT source comment for the first source line."""

        self.graph_attr = dict(graph_attr) if graph_attr is not None else {}
        """~typing.Dict[str, str]: Attribute-value pairs applying to the graph."""

        self.node_attr = dict(node_attr) if node_attr is not None else {}
        """~typing.Dict[str, str]: Attribute-value pairs applying to all nodes."""

        self.edge_attr = dict(edge_attr) if edge_attr is not None else {}
        """~typing.Dict[str, str]: Attribute-value pairs applying to all edges."""

        self.body = list(body) if body is not None else []
        """~typing.List[str]: Verbatim DOT source lines including final newline."""

        self.strict = strict
        """bool: Rendering should merge multi-edges."""

    def _copy_kwargs(self, **kwargs):
        """Return the kwargs to create a copy of the instance."""
        return super()._copy_kwargs(name=self.name,
                                    comment=self.comment,
                                    graph_attr=dict(self.graph_attr),
                                    node_attr=dict(self.node_attr),
                                    edge_attr=dict(self.edge_attr),
                                    body=list(self.body),
                                    strict=self.strict)

    @_tools.deprecate_positional_args(supported_number=0, ignore_arg='self')
    def clear(self, keep_attrs: bool = False) -> None:
        """Reset content to an empty body, clear graph/node/egde_attr mappings.

        Args:
            keep_attrs (bool): preserve graph/node/egde_attr mappings
        """
        if not keep_attrs:
            for a in (self.graph_attr, self.node_attr, self.edge_attr):
                a.clear()
        self.body.clear()

    @_tools.deprecate_positional_args(supported_number=0, ignore_arg='self')
    def __iter__(self, subgraph: bool = False) -> typing.Iterator[str]:
        r"""Yield the DOT source code line by line (as graph or subgraph).

        Yields: Line ending with a newline (``'\n'``).
        """
        if self.comment:
            yield self._comment(self.comment)

        if subgraph:
            if self.strict:
                raise ValueError('subgraphs cannot be strict')
            head = self._subgraph if self.name else self._subgraph_plain
        else:
            head = self._head_strict if self.strict else self._head
        yield head(self._quote(self.name) + ' ' if self.name else '')

        for kw in ('graph', 'node', 'edge'):
            attrs = getattr(self, f'{kw}_attr')
            if attrs:
                yield self._attr(kw, self._attr_list(None, kwargs=attrs))

        yield from self.body

        yield self._tail

    @_tools.deprecate_positional_args(supported_number=2, ignore_arg='self',
                                      category=DeprecationWarning)
    def node(self, name: str,
             label: typing.Optional[str] = None,
             _attributes=None, **attrs) -> None:
        """Create a node.

        Args:
            name: Unique identifier for the node inside the source.
            label: Caption to be displayed (defaults to the node ``name``).
            attrs: Any additional node attributes (must be strings).

        Attention:
            When rendering ``label``, backslash-escapes
            and strings of the form ``<...>`` have a special meaning.
            See the sections :ref:`backslash-escapes` and
            :ref:`quoting-and-html-like-labels` in the user guide for details.
        """
        name = self._quote(name)
        attr_list = self._attr_list(label, kwargs=attrs, attributes=_attributes)
        line = self._node(name, attr_list)
        self.body.append(line)

    @_tools.deprecate_positional_args(supported_number=3, ignore_arg='self',
                                      category=DeprecationWarning)
    def edge(self, tail_name: str, head_name: str,
             label: typing.Optional[str] = None,
             _attributes=None, **attrs) -> None:
        """Create an edge between two nodes.

        Args:
            tail_name: Start node identifier
                (format: ``node[:port[:compass]]``).
            head_name: End node identifier
                (format: ``node[:port[:compass]]``).
            label: Caption to be displayed near the edge.
            attrs: Any additional edge attributes (must be strings).

        Note:
            The ``tail_name`` and ``head_name`` strings are separated
            by (optional) colon(s) into ``node`` name, ``port`` name,
            and ``compass`` (e.g. ``sw``).
            See :ref:`details in the User Guide <node-ports-compass>`.

        Attention:
            When rendering ``label``, backslash-escapes
            and strings of the form ``<...>`` have a special meaning.
            See the sections :ref:`backslash-escapes` and
            :ref:`quoting-and-html-like-labels` in the user guide for details.
        """
        tail_name = self._quote_edge(tail_name)
        head_name = self._quote_edge(head_name)
        attr_list = self._attr_list(label, kwargs=attrs, attributes=_attributes)
        line = self._edge(tail=tail_name, head=head_name, attr=attr_list)
        self.body.append(line)

    def edges(self, tail_head_iter) -> None:
        """Create a bunch of edges.

        Args:
            tail_head_iter: Iterable of ``(tail_name, head_name)`` pairs
                (format:``node[:port[:compass]]``).


        Note:
            The ``tail_name`` and ``head_name`` strings are separated
            by (optional) colon(s) into ``node`` name, ``port`` name,
            and ``compass`` (e.g. ``sw``).
            See :ref:`details in the User Guide <node-ports-compass>`.
        """
        edge = self._edge_plain
        quote = self._quote_edge
        self.body += [edge(tail=quote(t), head=quote(h))
                      for t, h in tail_head_iter]

    @_tools.deprecate_positional_args(supported_number=1, ignore_arg='self',
                                      category=DeprecationWarning)
    def attr(self, kw: typing.Optional[str] = None,
             _attributes=None, **attrs) -> None:
        """Add a general or graph/node/edge attribute statement.

        Args:
            kw: Attributes target
                (``None`` or ``'graph'``, ``'node'``, ``'edge'``).
            attrs: Attributes to be set (must be strings, may be empty).

        See the :ref:`usage examples in the User Guide <attributes>`.
        """
        if kw is not None and kw.lower() not in ('graph', 'node', 'edge'):
            raise ValueError('attr statement must target graph, node, or edge:'
                             f' {kw!r}')
        if attrs or _attributes:
            if kw is None:
                a_list = self._a_list(None, kwargs=attrs, attributes=_attributes)
                line = self._attr_plain(a_list)
            else:
                attr_list = self._attr_list(None, kwargs=attrs, attributes=_attributes)
                line = self._attr(kw, attr_list)
            self.body.append(line)

    @_tools.deprecate_positional_args(supported_number=1, ignore_arg='self')
    def subgraph(self, graph=None,
                 name: typing.Optional[str] = None,
                 comment: typing.Optional[str] = None,
                 graph_attr=None, node_attr=None, edge_attr=None,
                 body=None):
        """Add the current content of the given sole ``graph`` argument
            as subgraph or return a context manager
            returning a new graph instance
            created with the given (``name``, ``comment``, etc.) arguments
            whose content is added as subgraph
            when leaving the context manager's ``with``-block.

        Args:
            graph: An instance of the same kind
                (:class:`.Graph`, :class:`.Digraph`) as the current graph
                (sole argument in non-with-block use).
            name: Subgraph name (``with``-block use).
            comment: Subgraph comment (``with``-block use).
            graph_attr: Subgraph-level attribute-value mapping
                (``with``-block use).
            node_attr: Node-level attribute-value mapping
                (``with``-block use).
            edge_attr: Edge-level attribute-value mapping
                (``with``-block use).
            body: Verbatim lines to add to the subgraph ``body``
                (``with``-block use).

        See the :ref:`usage examples in the User Guide <subgraphs-clusters>`.

        When used as a context manager, the returned new graph instance
        uses ``strict=None`` and the parent graph's values
        for ``directory``, ``format``, ``engine``, and ``encoding`` by default.

        Note:
            If the ``name`` of the subgraph begins with
            ``'cluster'`` (all lowercase)
            the layout engine will treat it as a special cluster subgraph.
        """
        if graph is None:
            kwargs = self._copy_kwargs()
            kwargs.pop('filename', None)
            kwargs.update(name=name, comment=comment,
                          graph_attr=graph_attr, node_attr=node_attr, edge_attr=edge_attr,
                          body=body, strict=None)
            subgraph = self.__class__(**kwargs)

            @contextlib.contextmanager
            def subgraph_contextmanager(*, parent):
                """Return subgraph and add to parent on exit."""
                yield subgraph
                parent.subgraph(subgraph)

            return subgraph_contextmanager(parent=self)

        args = [name, comment, graph_attr, node_attr, edge_attr, body]
        if not all(a is None for a in args):
            raise ValueError('graph must be sole argument of subgraph()')

        if graph.directed != self.directed:
            raise ValueError(f'{self!r} cannot add subgraph of different kind:'
                             f' {graph!r}')

        self.body += [f'\t{line}' for line in graph.__iter__(subgraph=True)]
