from typing import Any


class Node:
    def __init__(self, data, parent=None):
        """Creates a new Node instance.

        Params:
            data: the data that the node should consist of, can be any type.
            parent: a reference to any existing node, None by default (i.e. the node will be a root node.)
        """
        self._data = data
        self._parent = parent
        self._children = []
    
    def _set_data(self, data):
        self._data = data
    
    def _get_data(self):
        return self._data
    
    def _set_parent(self, new_parent):
        if new_parent == None:
            self._parent.remove(self)
            self._parent == None
            return
        if not isinstance(new_parent, Node):
            raise ValueError("invalid parent type")
        if new_parent == self:
            raise ValueError("invalid parent")
        if new_parent == self.parent:
            return
        if self._parent:
            self._parent.remove(self)

        new_parent.add(self)
        self._parent = new_parent
    
    def _get_parent(self):
        return self._parent
    
    def _set_children(self, children):
        raise Exception("invalid operation")
    
    def _get_children(self):
        return self._children
    
    data = property(_get_data, _set_data)
    parent = property(_get_parent, _set_parent)
    children = property(_get_children, _set_children)
    
    def size(self) -> int:
        if not self._children:
            return 1
        else:
            return self.rec_size()
    
    def rec_size(self):
        if not self._children:
            return 1
        s = 1
        for child in self._children:
            s += child.rec_size()
        return s
    
    def add(self, n):
        """Adds the node containing data to this node.

        Params:
            n: the node to add, can be an existing Node or new data.
        """
        if not isinstance(n, Node):
            n = Node(n, self)
        self._children.append(n)
        return n
    
    def remove(self, n):
        """Tries to remove the given node from this tree.

        Params:
            n: the node to remove.
        """
        if not isinstance(n, Node):
            raise ValueError(f"illegal type: {type(n)} is not of type {type(Node)}")
        elif self == n:
            raise ValueError(f"node cannot remove itself")
        self.rec_remove(n)
    
    def rec_remove(self, n):
        if self._children:
            if n in self._children:
                self._children.remove(n)
            else:
                for child in self._children:
                    child.rec_remove(n)
    
    def leave(self):
        """Remove itself from the tree it belongs to.
        """
        if self._parent:
            self._parent.remove(self)
            self._parent = None
    
    def for_each_top(self, callback, *args, **kwargs):
        """Run callback for each node in the tree top to bottom.
        """
        if self._children:
            callback(self, *args, **kwargs)
            for child in self._children:
                child.for_each_top(callback, *args, **kwargs)
        else:
            callback(self, *args, **kwargs)
    
    def for_each_bottom(self, callback, *args, **kwargs):
        """Run callback for each node in the tree bottom up.
        """
        if self._children:
            for child in self._children:
                child.for_each_bottom(callback, *args, **kwargs)
            callback(self, *args, **kwargs)
        else:
            callback(self, *args, **kwargs)
        
    def find(self, data):
        """Searches the tree for any nodes matching data.

        See also indexing.

        Params:
            data (Any): the data to search for, can be another node.
        
        Returns:
            A list containg all the nodes which has matching data.
        """
        nodes = []
        self.rec_find(data, nodes)
        return nodes
    
    def rec_find(self, data, nodes):
        if isinstance(data, Node):
            if self == data:
                nodes.append(self)
        elif self.data == data:
            nodes.append(self)
        for child in self._children:
            child.rec_find(data, nodes)
    
    @staticmethod
    def build_tree(dictionary):
        """Builds a new tree given a dictionary.
        """
        nodes = []
        if isinstance(dictionary, dict):
            for k, v in dictionary.items():
                root_node = Node(k)
                root_node.rec_build_tree(v)
                nodes.append(root_node)
        else:
            raise ValueError("invalid data: no tree structure")
        
        if len(nodes) == 1:
            return nodes[0]
        else:
            return nodes

    def rec_build_tree(self, d):
        if isinstance(d, dict):
            for k, v in d.items():
                node = self.add(k)
                node.rec_build_tree(v)
        elif isinstance(d, list):
            for v in d:
                self.rec_build_tree(v)
        elif isinstance(d, str):
            self.add(d)
    
    def __str__(self):
        if self._data:
            return str(self._data)
        else:
            return ""
    
    # Implement indexing
    def __getitem__(self, key):
        """Allows index nodes looking for nodes.

        If key is an integer or a slice, it only index the children of the node.
        Else if key is some other type
        """
        if isinstance(key, int) or isinstance(key, slice):
            return self.children[key]
        return self.find(key)
    
    # Make Node an iterable class
    def __iter__(self):
        # Intiliaze the nodes to return
        # Create a list containg all the nodes
        nodes = []
        def append(node, *argv, **kwargs):
            ns = kwargs["nodes"]
            ns.append(node)
        self.for_each_bottom(append, nodes=nodes)
        self._nodes = nodes
        return self
    
    def __next__(self):
        try:
            n = self._nodes.pop(0)
            return n
        except IndexError:
            raise StopIteration
    