"""Data structure: Trees."""
from typing import Any, Deque, Iterator, Optional, TypeVar, Union, cast

from contextlib import suppress

from .graphs import DependencyGraph
from .objects import shortlabel
from .types.graphs import DependencyGraphT
from .types.trees import NodeT

__all__ = ("Node",)

T = TypeVar("T")


class Node(NodeT[T]):
    """Tree node.

    Notes:
        Nodes have a link to

            - the ``.root`` node (or None if this is the top-most node)
            - the ``.parent`` node (if this is a child node).
            - a list of children

        A Node may have arbitrary ``.data`` associated with it, and arbitrary
        data may also be stored in ``.children``.

    Arguments:
        data (Any): Data to associate with node.

    Keyword Arguments:
        root (NodeT): Root node.
        parent (NodeT): Parent node.
        children (list[NodeT]): List of child nodes.
    """

    _root: Optional[NodeT[T]] = None
    _parent: Optional[NodeT[T]] = None

    @classmethod
    def _new_node(cls, data: T, **kwargs: Any) -> NodeT[T]:
        return cls(data, **kwargs)

    def __init__(
        self,
        data: T,
        *,
        root: NodeT | None = None,
        parent: NodeT | None = None,
        children: list[NodeT[T]] | None = None,
    ) -> None:
        self.data = data
        if root is not None:
            self.root = root
        if parent is not None:
            self.parent = parent
        self.children = children or []

    def new(self, data: T) -> NodeT:
        """Create new node from this node."""
        node = self._new_node(
            data,
            root=self.root if self.root is not None else self,
            parent=self,
        )
        self.children.append(node)
        return node

    def reattach(self, parent: NodeT[T]) -> NodeT[T]:
        """Attach this node to `parent` node."""
        self.root = parent.root if parent.root is not None else parent
        self.parent = parent
        parent.add_deduplicate(self)
        return self

    def detach(self, parent: NodeT[T]) -> NodeT[T]:
        """Detach this node from `parent` node."""
        if self.parent is not None:
            self.parent.discard(self)
        self._parent = None
        self._root = None
        return self

    def add_deduplicate(self, data: Union[T, NodeT[T]]) -> None:
        if data not in self.children:
            self.children.append(data)

    def add(self, data: Union[T, NodeT[T]]) -> None:
        """Add node as a child node."""
        self.children.append(data)

    def discard(self, data: T) -> None:
        """Remove node so it's no longer a child of this node."""
        # XXX slow
        with suppress(ValueError):
            self.children.remove(data)

    def traverse(self) -> Iterator[NodeT[T]]:
        """Iterate over the tree in BFS order."""
        stack: Deque[NodeT[T]] = Deque([self])
        while stack:
            node = stack.popleft()
            yield node
            for child in node.children:
                if isinstance(child, NodeT):
                    stack.append(child)
                else:
                    yield child

    def walk(self) -> Iterator[NodeT[T]]:
        """Iterate over hierarchy backwards.

        This will yield parent nodes all the way up to the root.
        """
        node: Optional[NodeT[T]] = self
        while node:
            yield node
            node = node.parent

    def as_graph(self) -> DependencyGraphT:
        """Convert to :class:`~mode.utils.graphs.DependencyGraph`."""
        graph = DependencyGraph()
        stack: Deque[NodeT] = Deque([self])
        while stack:
            node = stack.popleft()
            for child in node.children:
                graph.add_arc(node.data)
                if isinstance(child, NodeT):
                    stack.append(cast(Node, child))
                    graph.add_edge(node.data, child.data)
                else:
                    graph.add_edge(node.data, child)
        return graph

    def __repr__(self) -> str:
        return f"{type(self).__name__}: {self.path}"

    @property
    def depth(self) -> int:
        return self._find_depth()

    def _find_depth(self) -> int:
        return sum(1 for _ in enumerate(self.walk()))

    @property
    def path(self) -> str:
        return "/".join(
            reversed([shortlabel(node.data) for node in self.walk()])
        )

    @property
    def parent(self) -> Optional[NodeT]:
        return self._parent

    @parent.setter
    def parent(self, node: NodeT) -> None:
        if node is self:
            raise ValueError("Parent node cannot be itself.")
        self._parent = node

    @property
    def root(self) -> Optional[NodeT]:
        return self._root

    @root.setter
    def root(self, node: NodeT) -> None:
        if node is self:
            raise ValueError("Root node cannot be itself.")
        self._root = node
