# Copyright (C) 2026 Henrik Wilhelmsen.
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
import importlib
import logging
import os
import sys

from maya import cmds, mel

from hw_maya_toolbox.constants import (
    DEBUG_PORT,
    JOINT_RADIUS,
    LOCATOR_SCALE,
    MODULE_RELOAD_NAMES,
)

logger = logging.getLogger(__name__)


def toggle_selected_joints_local_axis_vis_off() -> None:
    selected_joints = cmds.ls(selection=True, type="joint")

    for jnt in selected_joints:
        cmds.setAttr(jnt + ".displayLocalAxis", False)  # noqa: FBT003 # ty: ignore[invalid-argument-type]


def toggle_selected_joints_local_axis_vis_on() -> None:
    selected_joints = cmds.ls(selection=True, type="joint")

    for jnt in selected_joints:
        cmds.setAttr(jnt + ".displayLocalAxis", True)  # noqa: FBT003 # ty: ignore[invalid-argument-type]


def insert_roll_joint_for_selection() -> None:
    selected_joints = cmds.ls(selection=True, type="joint")

    for jnt in selected_joints:
        # duplicate selected joint and store it in a variable 'roll_joint'
        roll_joint = cmds.duplicate(jnt, parentOnly=True, name=f"{jnt}Roll")[0]

        # parent roll_joint to world
        cmds.parent(roll_joint, absolute=True, world=True)

        # get joint child and store it in a variable
        try:
            jnt_child = cmds.listRelatives(jnt, type="joint")[0]
        except (KeyError, TypeError):
            jnt_child = None

        target = [jnt, jnt_child] if jnt_child is not None else [jnt]

        constraint_name = f"{roll_joint}TmpPointConstraint"
        cmds.pointConstraint(
            *target,
            roll_joint,
            name=constraint_name,
            maintainOffset=False,
        )

        # delete point constraint
        cmds.delete(constraint_name)

        # parent roll joint to selected, maintaining position
        cmds.parent(roll_joint, jnt)


def orient_selected_joint_to_parent() -> None:
    """Orient the selected joint to its parent."""
    # get selected joint and store it in a variable
    selection = cmds.ls(selection=True, type="joint")
    if len(selection) != 1:
        cmds.error("Orient to parent only supports one selected joint at a time")

    joint = selection[0]

    # get joint child and store it in a variable
    try:
        child = cmds.listRelatives(children=True, type="joint")[0]
    except (KeyError, TypeError):
        child = None

    if child is not None:
        # un-parent the child joint
        cmds.parent(child, world=True, absolute=True)

    # orient selected joint to its parent (world)
    cmds.joint(
        joint,
        edit=True,
        orientJoint="none",
        children=True,
        zeroScaleOrient=True,
    )

    # re-parent the child joint
    if child is not None:
        cmds.parent(child, joint)


def reset_draw_style_on_selected_joints() -> None:
    """Reset the select joints' draw style and radius."""
    for jnt in cmds.ls(selection=True, type="joint"):
        cmds.setAttr(f"{jnt}.drawStyle", 0)  # ty: ignore[invalid-argument-type]
        cmds.setAttr(f"{jnt}.radius", JOINT_RADIUS)  # ty: ignore[invalid-argument-type]


def zero_scale_orient_on_selected_joints() -> None:
    selection = cmds.ls(selection=True, type="joint")
    cmds.joint(*selection, edit=True, zeroScaleOrient=True)


def warn_if_joint_hierarchy_has_rotation() -> None:
    cmds.select(add=True, hierarchy=True)
    selection = cmds.ls(selection=True, type="joint")
    for jnt in selection:
        rotation = (
            cmds.getAttr(f"{jnt}.rotateX", channelBox=True),
            cmds.getAttr(f"{jnt}.rotateY", channelBox=True),
            cmds.getAttr(f"{jnt}.rotateZ", channelBox=True),
        )
        if rotation != (0, 0, 0):
            cmds.warning(f"joint '{jnt}' has non-zero rotation: {rotation}")


def print_name_and_type_for_selection() -> None:
    """Print out the name and type of every object in selection."""
    objects_and_types = cmds.ls(selection=True, showType=True)
    objects = objects_and_types[::2]
    types = objects_and_types[1::2]

    for n in range(len(objects)):
        print(f"{objects[n]}: {types[n]}")  # noqa: T201


def print_selection() -> None:
    for x in cmds.ls(selection=True):
        print(x)  # noqa: T201


def buffer_selection() -> None:
    """Create a buffer group for selected objects."""
    selection = cmds.ls(selection=True)

    for obj in selection:
        buffer = f"{obj}_buffer"
        obj_parent: list[str] | None = cmds.listRelatives(obj, parent=True)

        # stubs are wrong, listRelatives returns None if it can't find a parent...
        if obj_parent is not None:
            cmds.group(empty=True, name=buffer, parent=obj_parent[0])
        else:
            cmds.group(empty=True, name=buffer)

        cmds.matchTransform(buffer, obj, position=True, rotation=True, scale=False)
        cmds.parent(obj, buffer)


def create_circle_at_selected_objects() -> None:
    """Create a circle at the selected object(s)."""
    for x in cmds.ls(selection=True):
        circle = f"{x}_CTRL"
        cmds.circle(name=circle)
        cmds.matchTransform(circle, x, position=True, rotation=True, scale=False)


def create_locator_at_selected_objects() -> None:
    """Create a locator at the selected object(s)."""
    for x in cmds.ls(selection=True):
        locator = f"{x}_locator"
        cmds.spaceLocator(name=locator)
        cmds.setAttr(f"{locator}.scale", *LOCATOR_SCALE)  # ty: ignore[invalid-argument-type]
        cmds.matchTransform(locator, x, position=True, rotation=True, scale=False)


def match_translation_and_rotation() -> None:
    """Match translation and rotation for selection.

    Works the same way as Maya's match transform tool, but with rotation and translation
    and no scale.
    """
    selection = cmds.ls(selection=True)
    cmds.matchTransform(*selection, position=True, rotation=True, scale=False)


def reset_trs_for_selection() -> None:
    for x in cmds.ls(selection=True):
        cmds.setAttr(f"{x}.translate", 0, 0, 0)  # ty: ignore[invalid-argument-type]
        cmds.setAttr(f"{x}.rotate", 0, 0, 0)  # ty: ignore[invalid-argument-type]
        cmds.setAttr(f"{x}.scale", 1, 1, 1)  # ty: ignore[invalid-argument-type]


def freeze_del_history_for_selection() -> None:
    for x in cmds.ls(selection=True):
        # freeze transform with default settings
        cmds.makeIdentity(
            x,
            apply=True,
            rotate=True,
            translate=True,
            scale=True,
            jointOrient=False,
            preserveNormals=True,
            normal=0,
        )
        # delete history
        cmds.delete(x, constructionHistory=True)


def freeze_selected_joints() -> None:
    for jnt in cmds.ls(selection=True, type="joint"):
        cmds.makeIdentity(
            jnt,
            apply=True,
            rotate=True,
            translate=False,
            scale=False,
            jointOrient=False,
            preserveNormals=True,
            normal=0,
        )


def _set_reset_mode(reset_mode: int = 0) -> None:
    """Set the TRS reset mode. 0 = center, 1 = zero."""
    setting_cmd = f"setTRSResetPivotMode {reset_mode};"
    mel.eval(setting_cmd)


def center_pivot_for_selection() -> None:
    _set_reset_mode(0)
    reset_cmd = "manipPivotReset true true;"
    mel.eval(reset_cmd)


def zero_pivot_for_selection() -> None:
    # cmds.manipPivot with the reset flag does not seem to do anything and documentation
    # is non-existent. Easier to just use the mel command (also missing from docs...)
    # This seems to be what the menu button uses, as it behaves the same. E.g. it does
    # nothing if object does not have frozen translation.
    _set_reset_mode(1)
    reset_cmd = "manipPivotReset true true;"
    mel.eval(reset_cmd)


def delete_unknown_nodes() -> None:
    """Find and remove unknown nodes and plugins in the current scene.

    Originally found here, updated with minor fixes
    https://www.aaronstrasbourgvfx.ca/blog/maya-remove-unknown-nodes-plugins/
    """
    # Find and remove unknown nodes
    unknown_nodes = cmds.ls(type="unknown")
    if unknown_nodes:
        cmds.delete(*unknown_nodes)
        logger.info("Deleted unknown nodes: %s", unknown_nodes)

    # Find and remove unknown plugins
    unknown_plugins: list[str] = cmds.unknownPlugin(query=True, list=True)  # ty: ignore[invalid-assignment]
    if unknown_plugins:
        for plugin in unknown_plugins:
            try:
                cmds.unknownPlugin(plugin, remove=True)
                logger.info("Deleted unknown plugin: %s", plugin)

            # Note, command docs does not say what kind of error it might raise...
            # May need to update this with other exception types.
            except RuntimeError as e:
                logger.warning(
                    "Unknown plugin cannot be removed due to ERROR: %",
                    e,
                )


def start_debugpy() -> None:
    # Stops debugpy complaining about Maya frozen modules
    os.environ["PYDEVD_DISABLE_FILE_VALIDATION"] = "1"

    try:
        import debugpy  # noqa: PLC0415, T100
    except ImportError as e:
        msg = "Failed to import debugpy, make sure it is installed"
        raise ImportError(msg) from e

    try:
        debugpy.listen(DEBUG_PORT, in_process_debug_adapter=True)  # noqa: T100
    except RuntimeError:
        logger.exception(
            "Failed to start debugpy on port %s, it may already be running",
            DEBUG_PORT,
        )
    else:
        logger.info(
            "Debugpy listening on port %s",
            DEBUG_PORT,
        )


def reload_modules() -> None:
    for module_name, module in sys.modules.copy().items():
        for reload_name in MODULE_RELOAD_NAMES:
            if module_name.startswith(reload_name):
                importlib.reload(module)
                logger.debug("Reloaded module %s", module_name)
