#!/usr/bin/env python
# Copyright (C) 2011
# Brett Alistair Kromkamp - brettkromkamp@gmail.com
# Copyright (C) 2012-2017
# Xiaming Chen - chenxm35@gmail.com
# and other contributors.
# All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Node structure in treelib.

A :class:`Node` object contains basic properties such as node identifier,
node tag, parent node, children nodes etc., and some operations for a node.
"""
from __future__ import unicode_literals

import copy
import uuid
import sys
from collections import defaultdict
from warnings import warn
from typing import cast, List, Any, Optional, Union

from .exceptions import NodePropertyError
from .misc import deprecated

if sys.version_info >= (3, 9):
    StrList = list[str]
else:
    StrList = List[str]  # Python 3.8 and earlier


class Node(object):
    """
    Nodes are elementary objects that are stored in the `_nodes` dictionary of a Tree.
    Use `data` attribute to store node-specific data.
    """

    #: Mode constants for routine `update_fpointer()`.
    (ADD, DELETE, INSERT, REPLACE) = list(range(4))

    def __init__(
        self,
        tag: Optional[str] = None,
        identifier: Optional[str] = None,
        expanded: bool = True,
        data: Any = None,
    ) -> None:
        """Create a new Node object to be placed inside a Tree object"""

        #: if given as a parameter, must be unique
        self._identifier: Optional[str] = None
        self._set_identifier(identifier)

        #: None or something else
        #: if None, self._identifier will be set to the identifier's value.
        if tag is None:
            self._tag = self._identifier
        else:
            self._tag = tag

        #: boolean
        self.expanded = expanded

        #: identifier of the parent's node :
        self._predecessor: dict = {}
        #: identifier(s) of the soons' node(s) :
        self._successors: dict = defaultdict(list)

        #: User payload associated with this node.
        self.data = data

        # for retro-compatibility on bpointer/fpointer
        self._initial_tree_id: Optional[str] = None

    def __lt__(self, other) -> bool:
        return self.tag < other.tag

    def set_initial_tree_id(self, tree_id: str) -> None:
        if self._initial_tree_id is None:
            self._initial_tree_id = tree_id

    def _set_identifier(self, nid: Optional[str]) -> None:
        """Initialize self._set_identifier"""
        if nid is None:
            self._identifier = str(uuid.uuid1())
        else:
            self._identifier = nid

    @property
    @deprecated(alias="node.predecessor")
    def bpointer(self):
        """Use predecessor method, this property is deprecated and only kept for retro-compatilibity. Parents of
        a node are dependant on a given tree. This implementation keeps the previous behavior by keeping returning
        bpointer of first declared tree.
        """
        if self._initial_tree_id not in self._predecessor.keys():
            return None
        return self._predecessor[self._initial_tree_id]

    @bpointer.setter
    @deprecated(alias="node.set_predecessor")
    def bpointer(self, value) -> None:
        self.set_predecessor(value, self._initial_tree_id)

    @deprecated(alias="node.set_predecessor")
    def update_bpointer(self, nid) -> None:
        self.set_predecessor(nid, self._initial_tree_id)

    @property
    @deprecated(alias="node.successors")
    def fpointer(self):
        """Use successors method, this property is deprecated and only kept for retro-compatilibity. Children of
        a node are dependant on a given tree. This implementation keeps the previous behavior by keeping returning
        fpointer of first declared tree.
        """
        if self._initial_tree_id not in self._successors:
            return []
        return self._successors[self._initial_tree_id]

    @fpointer.setter
    @deprecated(alias="node.update_successors")
    def fpointer(self, value: Union[None, list, dict, set]) -> None:
        self.set_successors(value, tree_id=self._initial_tree_id)

    @deprecated(alias="node.update_successors")
    def update_fpointer(self, nid, mode=ADD, replace=None):
        """Deprecated"""
        self.update_successors(nid, mode, replace, self._initial_tree_id)

    def predecessor(self, tree_id):
        """
        The parent ID of a node in a given tree.
        """
        return self._predecessor[tree_id]

    def set_predecessor(self, nid: Optional[str], tree_id: Optional[str]) -> None:
        """Set the value of `_predecessor`."""
        self._predecessor[tree_id] = nid

    def successors(self, tree_id: Optional[str]) -> StrList:
        """
        With a getting operator, a list of IDs of node's children is obtained. With
        a setting operator, the value can be list, set, or dict. For list or set,
        it is converted to a list type by the package; for dict, the keys are
        treated as the node IDs.
        """
        return self._successors[tree_id]

    def set_successors(
        self, value: Union[None, list, dict, set], tree_id: Optional[str] = None
    ) -> None:
        """Set the value of `_successors`."""
        setter_lookup = {
            "NoneType": lambda x: list(),
            "list": lambda x: x,
            "dict": lambda x: list(x.keys()),
            "set": lambda x: list(x),
        }

        t = value.__class__.__name__
        if t in setter_lookup:
            f_setter = setter_lookup[t]
            self._successors[tree_id] = f_setter(value)
        else:
            raise NotImplementedError("Unsupported value type %s" % t)

    def update_successors(
        self,
        nid: Optional[str],
        mode: int = ADD,
        replace: Optional[str] = None,
        tree_id: Optional[str] = None,
    ) -> None:
        """
        Update the children list with different modes: addition (Node.ADD or
        Node.INSERT) and deletion (Node.DELETE).
        """
        if nid is None:
            return

        def _manipulator_append() -> None:
            self.successors(tree_id).append(nid)

        def _manipulator_delete() -> None:
            if nid in self.successors(tree_id):
                self.successors(tree_id).remove(nid)
            else:
                warn("Nid %s wasn't present in fpointer" % nid)

        def _manipulator_insert() -> None:
            warn("WARNING: INSERT is deprecated to ADD mode")
            self.update_successors(nid, tree_id=tree_id)

        def _manipulator_replace() -> None:
            if replace is None:
                raise NodePropertyError(
                    'Argument "replace" should be provided when mode is {}'.format(mode)
                )
            ind = self.successors(tree_id).index(nid)
            self.successors(tree_id)[ind] = replace

        manipulator_lookup = {
            self.ADD: "_manipulator_append",
            self.DELETE: "_manipulator_delete",
            self.INSERT: "_manipulator_insert",
            self.REPLACE: "_manipulator_replace",
        }

        if mode not in manipulator_lookup:
            raise NotImplementedError("Unsupported node updating mode %s" % str(mode))

        f_name = cast(str, manipulator_lookup.get(mode))
        f = locals()[f_name]
        return f()

    @property
    def identifier(self) -> str:
        """
        The unique ID of a node within the scope of a tree. This attribute can be
        accessed and modified with ``.`` and ``=`` operator respectively.
        """
        return cast(str, self._identifier)

    def clone_pointers(self, former_tree_id: str, new_tree_id: str) -> None:
        former_bpointer = self.predecessor(former_tree_id)
        self.set_predecessor(former_bpointer, new_tree_id)
        former_fpointer = self.successors(former_tree_id)
        # fpointer is a list and would be copied by reference without deepcopy
        self.set_successors(copy.deepcopy(former_fpointer), tree_id=new_tree_id)

    def reset_pointers(self, tree_id) -> None:
        self.set_predecessor(None, tree_id)
        self.set_successors([], tree_id=tree_id)

    @identifier.setter  # type: ignore
    def identifier(self, value: str) -> None:
        """Set the value of `_identifier`."""
        if value is None:
            print("WARNING: node ID can not be None")
        else:
            self._set_identifier(value)

    def is_leaf(self, tree_id: Optional[str] = None) -> bool:
        """Return true if current node has no children."""
        if tree_id is None:
            # for retro-compatilibity
            if self._initial_tree_id not in self._successors.keys():
                return True
            else:
                tree_id = self._initial_tree_id

        if len(self.successors(tree_id)) == 0:
            return True
        else:
            return False

    def is_root(self, tree_id: Optional[str] = None) -> bool:
        """Return true if self has no parent, i.e. as root."""
        if tree_id is None:
            # for retro-compatilibity
            if self._initial_tree_id not in self._predecessor.keys():
                return True
            else:
                tree_id = self._initial_tree_id

        return self.predecessor(tree_id) is None

    @property
    def tag(self) -> str:
        """
        The readable node name for human. This attribute can be accessed and
        modified with ``.`` and ``=`` operator respectively.
        """
        return cast(str, self._tag)

    @tag.setter
    def tag(self, value: Optional[str]) -> None:
        """Set the value of `_tag`."""
        self._tag = value if value is not None else None

    def __repr__(self) -> str:
        name = self.__class__.__name__
        kwargs = [
            "tag={0}".format(self.tag),
            "identifier={0}".format(self.identifier),
            "data={0}".format(self.data),
        ]
        return "%s(%s)" % (name, ", ".join(kwargs))
