"""Helpers for modifying the contents of a node"""
from typing import Any, Dict, List, Optional

import omni.graph.core as og

from .object_lookup import ObjectLookup
from .typing import Attribute_t, AttributeType_t, ExtendedAttribute_t, Node_t
from .utils import _flatten_arguments, _Unspecified


# ==============================================================================================================
class NodeController:
    """Helper class that provides a simple interface to modifying the contents of a node"""

    # --------------------------------------------------------------------------------------------------------------
    def __init__(self, *args, **kwargs):
        """Initializes the class with a particular configuration.
        The arguments are flexible so that classes can initialize only what they need for the calls they
        will be making. The argument is optional and there may be other arguments present that will be ignored

        Args:
            undoable (bool): If True the operations performed with this instance of the class are to be added to the
                             undo queue, else they are done immediately and forgotten
        """
        (self.__undoable,) = _flatten_arguments(
            optional=[("undoable", None)],
            args=args,
            kwargs=kwargs,
        )

        # Dual function methods that can be called either from an object or directly from the class
        self.create_attribute = self.__create_attribute_obj
        self.remove_attribute = self.__remove_attribute_obj

    # ----------------------------------------------------------------------
    # begin-create-attribute-function
    @classmethod
    def create_attribute(obj, *args, **kwargs) -> Optional[og.Attribute]:  # noqa: N804, PLE0202, PLC0202
        """Create a new dynamic attribute on the node

        This function can be called either from the class or using an instantiated object. The first argument is
        positional, being either the class or object. All others are by keyword and optional, defaulting to the value
        set in the constructor in the object context and the function defaults in the class context.

        Args:
            obj: Either cls or self depending on how the function was called
            node: Node on which to create the attribute (path or og.Node)
            attr_name: Name of the new attribute, either with or without the port namespace
            attr_type: Type of the new attribute, as an OGN type string or og.Type
            attr_port: Port type of the new attribute, default is og.AttributePortType.ATTRIBUTE_PORT_TYPE_INPUT
            attr_default: The initial value to set on the attribute, default is None which means use the type's default
            attr_extended_type: The extended type of the attribute, default is
                                og.ExtendedAttributeType.EXTENDED_ATTR_TYPE_REGULAR. If the extended type is
                                og.ExtendedAttributeType.EXTENDED_ATTR_TYPE_UNION then this parameter will be a
                                2-tuple with the second element being a list or comma-separated string of union types
            undoable: If True the operation is added to the undo queue, else it is done immediately and forgotten

        Returns:
            The newly created attribute, None if there was a problem creating it
        """
        # end-create-attribute-function
        return obj.__create_attribute(obj, args=args, kwargs=kwargs)

    def __create_attribute_obj(self, *args, **kwargs) -> Optional[og.Attribute]:
        """Implements :py:meth:`.NodeController.create_attribute` when called as an object method"""
        return self.__create_attribute(self, undoable=self.__undoable, args=args, kwargs=kwargs)

    @staticmethod
    def __create_attribute(
        obj,
        node: Node_t = _Unspecified,
        attr_name: str = _Unspecified,
        attr_type: AttributeType_t = _Unspecified,
        attr_port: Optional[og.AttributePortType] = og.AttributePortType.ATTRIBUTE_PORT_TYPE_INPUT,
        attr_default: Optional[Any] = None,
        attr_extended_type: Optional[ExtendedAttribute_t] = og.ExtendedAttributeType.EXTENDED_ATTR_TYPE_REGULAR,
        undoable: bool = True,
        args: List[Any] = None,
        kwargs: Dict[str, Any] = None,
    ) -> Optional[og.Attribute]:
        """Implements :py:meth:`.NodeController.create_attribute`"""
        (node, attr_name, attr_type, attr_port, attr_default, attr_extended_type, undoable) = _flatten_arguments(
            mandatory=[("node", node), ("attr_name", attr_name), ("attr_type", attr_type)],
            optional=[
                ("attr_port", attr_port),
                ("attr_default", attr_default),
                ("attr_extended_type", attr_extended_type),
                ("undoable", undoable),
            ],
            args=args,
            kwargs=kwargs,
        )

        omg_node = ObjectLookup.node(node)
        omg_attr_type = ObjectLookup.attribute_type(attr_type)
        if undoable:
            (success, _) = og.cmds.CreateAttr(
                node=omg_node,
                attr_name=attr_name,
                attr_type=omg_attr_type,
                attr_port=attr_port,
                attr_default=attr_default,
                attr_extended_type=attr_extended_type,
            )
        else:
            success = True
            og.cmds.imm.CreateAttr(
                node=omg_node,
                attr_name=attr_name,
                attr_type=omg_attr_type,
                attr_port=attr_port,
                attr_default=attr_default,
                attr_extended_type=attr_extended_type,
            )
        if not success:
            return None

        # The command didn't return the new attribute so it has to be looked up in the node
        namespace = og.get_port_type_namespace(attr_port)
        if not attr_name.startswith(namespace):
            if (
                omg_attr_type.get_ogn_type_name() == "bundle"
                and attr_port != og.AttributePortType.ATTRIBUTE_PORT_TYPE_INPUT
            ):
                separator = "_"
            else:
                separator = ":"
            attr_name = f"{namespace}{separator}{attr_name}"
        return omg_node.get_attribute(attr_name)

    # ----------------------------------------------------------------------
    # begin-remove-attribute-function
    @classmethod
    def remove_attribute(obj, *args, **kwargs) -> bool:  # noqa: N804,PLC0202,PLE0202
        """Removes an existing dynamic attribute from a node.

        This function can be called either from the class or using an instantiated object. The first argument is
        positional, being either the class or object. All others are by keyword and optional, defaulting to the value
        set in the constructor in the object context and the function defaults in the class context.

        Args:
            obj: Either cls or self depending on how the function was called
            attribute: Reference to the attribute to be removed
            node: If the attribute reference is a string the node is used to find the attribute to be removed
            undoable: If True the operation is added to the undo queue, else it is done immediately and forgotten

        Returns:
            True if the attribute was successfully removed, else False (including when the attribute wasn't found)
        """
        # end-remove-attribute-function
        return obj.__remove_attribute(obj, args=args, kwargs=kwargs)

    def __remove_attribute_obj(self, *args, **kwargs) -> og.Graph:
        """Implements :py:meth:`.NodeController.remove_attribute` when called as an object method"""
        return self.__remove_attribute(self, undoable=self.__undoable, args=args, kwargs=kwargs)

    @staticmethod
    def __remove_attribute(
        obj,
        attribute: Attribute_t = _Unspecified,
        node: Node_t = None,
        undoable: bool = True,
        args: List[Any] = None,
        kwargs: Dict[str, Any] = None,
    ) -> bool:
        """Implements :py:meth:`.NodeController.remove_attribute`"""
        (attribute, node, undoable) = _flatten_arguments(
            mandatory=[("attribute", attribute)],
            optional=[("node", node), ("undoable", undoable)],
            args=args,
            kwargs=kwargs,
        )
        omg_attribute = ObjectLookup.attribute(attribute, node)
        if undoable:
            (success, result) = og.cmds.RemoveAttr(attribute=omg_attribute)
            success = success and result
        else:
            success = True
            og.cmds.imm.RemoveAttr(attribute=omg_attribute)
        return success

    # ----------------------------------------------------------------------
    @classmethod
    def safe_node_name(cls, node_type_name: str, abbreviated: bool = False) -> str:
        """Returns a USD-safe node name derived from the node_type_name

        Args:
            node_type_name: Fully namespaced name of the node type (e.g. omni.graph.nodes.Clamp)
            abbreviated: If True then remove the namespace, else just make the separators into underscores

        Returns:
            A safe node name that roughly corresponds to the given node type name
        """
        if abbreviated:
            last_namespace = node_type_name.rfind(".")
            if last_namespace < 0:
                return node_type_name
            return node_type_name[last_namespace + 1 :]
        return node_type_name.replace(".", "_")
