# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
import math
from collections.abc import Sequence
from typing import TYPE_CHECKING, Optional, TypeVar

import bpy
from bpy.props import (
    CollectionProperty,
    EnumProperty,
    IntVectorProperty,
    PointerProperty,
    StringProperty,
)
from bpy.types import (
    Armature,
    Bone,
    Context,
    Material,
    NodeTree,
    Object,
    PropertyGroup,
    Scene,
)
from mathutils import Matrix, Quaternion

from ..common.logger import get_logger
from ..common.preferences import VrmAddonPreferences
from .mtoon1.property_group import Mtoon1MaterialPropertyGroup
from .node_constraint1.property_group import NodeConstraint1NodeConstraintPropertyGroup
from .property_group import StringPropertyGroup, property_group_enum
from .spring_bone1.property_group import SpringBone1SpringBonePropertyGroup
from .vrm0.property_group import Vrm0HumanoidPropertyGroup, Vrm0PropertyGroup
from .vrm1.property_group import Vrm1HumanBonesPropertyGroup, Vrm1PropertyGroup

if TYPE_CHECKING:
    from .property_group import CollectionPropertyProtocol

logger = get_logger(__name__)


class VrmAddonSceneExtensionPropertyGroup(PropertyGroup):
    vrm0_material_gltf_property_names: CollectionProperty(  # type: ignore[valid-type]
        type=StringPropertyGroup
    )

    vrm0_material_mtoon0_property_names: CollectionProperty(  # type: ignore[valid-type]
        type=StringPropertyGroup
    )

    @staticmethod
    def update_vrm0_material_property_names_timer_callback() -> Optional[float]:
        context = bpy.context

        VrmAddonSceneExtensionPropertyGroup.update_vrm0_material_property_names(context)
        return None

    @staticmethod
    def defer_update_vrm0_material_property_names() -> None:
        s = VrmAddonSceneExtensionPropertyGroup
        bpy.app.timers.register(s.update_vrm0_material_property_names_timer_callback)

    @staticmethod
    def update_vrm0_material_property_names(
        context: Context, scene_name: Optional[str] = None
    ) -> None:
        # Unity 2022.3.4 + UniVRM 0.112.0
        gltf_property_names = [
            "_Color",
            "_MainTex_ST",
            "_MainTex_ST_S",
            "_MainTex_ST_T",
            "_MetallicGlossMap_ST",
            "_MetallicGlossMap_ST_S",
            "_MetallicGlossMap_ST_T",
            "_BumpMap_ST",
            "_BumpMap_ST_S",
            "_BumpMap_ST_T",
            "_ParallaxMap_ST",
            "_ParallaxMap_ST_S",
            "_ParallaxMap_ST_T",
            "_OcclusionMap_ST",
            "_OcclusionMap_ST_S",
            "_OcclusionMap_ST_T",
            "_EmissionColor",
            "_EmissionMap_ST",
            "_EmissionMap_ST_S",
            "_EmissionMap_ST_T",
            "_DetailMask_ST",
            "_DetailMask_ST_S",
            "_DetailMask_ST_T",
            "_DetailAlbedoMap_ST",
            "_DetailAlbedoMap_ST_S",
            "_DetailAlbedoMap_ST_T",
            "_DetailNormalMap_ST",
            "_DetailNormalMap_ST_S",
            "_DetailNormalMap_ST_T",
        ]

        # UniVRM 0.112.0
        mtoon0_property_names = [
            "_Color",
            "_ShadeColor",
            "_MainTex_ST",
            "_MainTex_ST_S",
            "_MainTex_ST_T",
            "_ShadeTexture_ST",
            "_ShadeTexture_ST_S",
            "_ShadeTexture_ST_T",
            "_BumpMap_ST",
            "_BumpMap_ST_S",
            "_BumpMap_ST_T",
            "_ReceiveShadowTexture_ST",
            "_ReceiveShadowTexture_ST_S",
            "_ReceiveShadowTexture_ST_T",
            "_ShadingGradeTexture_ST",
            "_ShadingGradeTexture_ST_S",
            "_ShadingGradeTexture_ST_T",
            "_RimColor",
            "_RimTexture_ST",
            "_RimTexture_ST_S",
            "_RimTexture_ST_T",
            "_SphereAdd_ST",
            "_SphereAdd_ST_S",
            "_SphereAdd_ST_T",
            "_EmissionColor",
            "_EmissionMap_ST",
            "_EmissionMap_ST_S",
            "_EmissionMap_ST_T",
            "_OutlineWidthTexture_ST",
            "_OutlineWidthTexture_ST_S",
            "_OutlineWidthTexture_ST_T",
            "_OutlineColor",
            "_UvAnimMaskTexture_ST",
            "_UvAnimMaskTexture_ST_S",
            "_UvAnimMaskTexture_ST_T",
        ]

        if scene_name is None:
            scenes: Sequence[Scene] = list(context.blend_data.scenes)
        else:
            scene = context.blend_data.scenes.get(scene_name)
            if not scene:
                logger.error('No scene "%s"', scene_name)
                return
            scenes = [scene]

        for scene in scenes:
            ext = get_scene_extension(scene)

            if gltf_property_names != [
                n.value for n in ext.vrm0_material_gltf_property_names
            ]:
                ext.vrm0_material_gltf_property_names.clear()
                for gltf_property_name in gltf_property_names:
                    n = ext.vrm0_material_gltf_property_names.add()
                    n.value = gltf_property_name

            if mtoon0_property_names != [
                n.value for n in ext.vrm0_material_mtoon0_property_names
            ]:
                ext.vrm0_material_mtoon0_property_names.clear()
                for mtoon0_property_name in mtoon0_property_names:
                    n = ext.vrm0_material_mtoon0_property_names.add()
                    n.value = mtoon0_property_name

    if TYPE_CHECKING:
        # This code is auto generated.
        # To regenerate, run the `uv run tools/property_typing.py` command.
        vrm0_material_gltf_property_names: CollectionPropertyProtocol[  # type: ignore[no-redef]
            StringPropertyGroup
        ]
        vrm0_material_mtoon0_property_names: CollectionPropertyProtocol[  # type: ignore[no-redef]
            StringPropertyGroup
        ]


class VrmAddonBoneExtensionPropertyGroup(PropertyGroup):
    uuid: StringProperty()  # type: ignore[valid-type]

    (
        axis_translation_enum,
        (
            AXIS_TRANSLATION_AUTO,
            AXIS_TRANSLATION_NONE,
            AXIS_TRANSLATION_X_TO_Y,
            AXIS_TRANSLATION_MINUS_X_TO_Y,
            AXIS_TRANSLATION_MINUS_Y_TO_Y_AROUND_Z,
            AXIS_TRANSLATION_Z_TO_Y,
            AXIS_TRANSLATION_MINUS_Z_TO_Y,
        ),
    ) = property_group_enum(
        ("AUTO", "Auto", "", "NONE", 0),
        ("NONE", "None", "", "NONE", 1),
        ("X_TO_Y", "X,Y to Y,-X", "", "NONE", 2),
        ("MINUS_X_TO_Y", "X,Y to -Y,X", "", "NONE", 3),
        ("MINUS_Y_TO_Y_AROUND_Z", "X,Y to -X,-Y", "", "NONE", 4),
        ("Z_TO_Y", "Y,Z to -Z,Y", "", "NONE", 5),
        ("MINUS_Z_TO_Y", "Y,Z to Z,-Y", "", "NONE", 6),
    )

    @classmethod
    def reverse_axis_translation(cls, axis_translation: str) -> str:
        return {
            cls.AXIS_TRANSLATION_AUTO.identifier: cls.AXIS_TRANSLATION_AUTO,
            cls.AXIS_TRANSLATION_NONE.identifier: cls.AXIS_TRANSLATION_NONE,
            cls.AXIS_TRANSLATION_X_TO_Y.identifier: (cls.AXIS_TRANSLATION_MINUS_X_TO_Y),
            cls.AXIS_TRANSLATION_MINUS_X_TO_Y.identifier: (cls.AXIS_TRANSLATION_X_TO_Y),
            cls.AXIS_TRANSLATION_Z_TO_Y.identifier: (cls.AXIS_TRANSLATION_MINUS_Z_TO_Y),
            cls.AXIS_TRANSLATION_MINUS_Z_TO_Y.identifier: (cls.AXIS_TRANSLATION_Z_TO_Y),
            cls.AXIS_TRANSLATION_MINUS_Y_TO_Y_AROUND_Z.identifier: (
                cls.AXIS_TRANSLATION_MINUS_Y_TO_Y_AROUND_Z
            ),
        }[axis_translation].identifier

    @classmethod
    def node_constraint_roll_axis_translation(
        cls, axis_translation: str, roll_axis: Optional[str]
    ) -> Optional[str]:
        if roll_axis is None:
            return None
        return {
            cls.AXIS_TRANSLATION_AUTO.identifier: {"X": "X", "Y": "Y", "Z": "Z"},
            cls.AXIS_TRANSLATION_NONE.identifier: {"X": "X", "Y": "Y", "Z": "Z"},
            cls.AXIS_TRANSLATION_X_TO_Y.identifier: {"X": "Y", "Y": "X", "Z": "Z"},
            cls.AXIS_TRANSLATION_MINUS_X_TO_Y.identifier: {
                "X": "Y",
                "Y": "X",
                "Z": "Z",
            },
            cls.AXIS_TRANSLATION_Z_TO_Y.identifier: {"X": "X", "Y": "Z", "Z": "Y"},
            cls.AXIS_TRANSLATION_MINUS_Z_TO_Y.identifier: {
                "X": "X",
                "Y": "Z",
                "Z": "Y",
            },
            cls.AXIS_TRANSLATION_MINUS_Y_TO_Y_AROUND_Z.identifier: {
                "X": "X",
                "Y": "Y",
                "Z": "Z",
            },
        }[axis_translation][roll_axis]

    @classmethod
    def node_constraint_aim_axis_translation(
        cls, axis_translation: str, aim_axis: Optional[str]
    ) -> Optional[str]:
        if aim_axis is None:
            return None
        return {
            cls.AXIS_TRANSLATION_AUTO.identifier: {
                "PositiveX": "PositiveX",
                "PositiveY": "PositiveY",
                "PositiveZ": "PositiveZ",
                "NegativeX": "NegativeX",
                "NegativeY": "NegativeY",
                "NegativeZ": "NegativeZ",
            },
            cls.AXIS_TRANSLATION_NONE.identifier: {
                "PositiveX": "PositiveX",
                "PositiveY": "PositiveY",
                "PositiveZ": "PositiveZ",
                "NegativeX": "NegativeX",
                "NegativeY": "NegativeY",
                "NegativeZ": "NegativeZ",
            },
            cls.AXIS_TRANSLATION_X_TO_Y.identifier: {
                "PositiveX": "PositiveY",
                "PositiveY": "NegativeX",
                "PositiveZ": "PositiveZ",
                "NegativeX": "NegativeY",
                "NegativeY": "PositiveX",
                "NegativeZ": "NegativeZ",
            },
            cls.AXIS_TRANSLATION_MINUS_X_TO_Y.identifier: {
                "PositiveY": "PositiveX",
                "NegativeX": "PositiveY",
                "PositiveZ": "PositiveZ",
                "NegativeY": "NegativeX",
                "PositiveX": "NegativeY",
                "NegativeZ": "NegativeZ",
            },
            cls.AXIS_TRANSLATION_Z_TO_Y.identifier: {
                "PositiveX": "PositiveX",
                "PositiveY": "NegativeZ",
                "PositiveZ": "PositiveY",
                "NegativeX": "NegativeX",
                "NegativeY": "PositiveZ",
                "NegativeZ": "NegativeY",
            },
            cls.AXIS_TRANSLATION_MINUS_Z_TO_Y.identifier: {
                "PositiveX": "PositiveX",
                "NegativeZ": "PositiveY",
                "PositiveY": "PositiveZ",
                "NegativeX": "NegativeX",
                "PositiveZ": "NegativeY",
                "NegativeY": "NegativeZ",
            },
            cls.AXIS_TRANSLATION_MINUS_Y_TO_Y_AROUND_Z.identifier: {
                "PositiveX": "NegativeX",
                "PositiveY": "NegativeY",
                "PositiveZ": "PositiveZ",
                "NegativeX": "PositiveX",
                "NegativeY": "PositiveY",
                "NegativeZ": "NegativeZ",
            },
        }[axis_translation][aim_axis]

    @classmethod
    def translate_axis(cls, matrix: Matrix, axis_translation: str) -> Matrix:
        location, rotation, scale = matrix.decompose()

        if axis_translation == cls.AXIS_TRANSLATION_X_TO_Y.identifier:
            rotation @= Quaternion((0, 0, 1), -math.pi / 2)
        elif axis_translation == cls.AXIS_TRANSLATION_MINUS_X_TO_Y.identifier:
            rotation @= Quaternion((0, 0, 1), math.pi / 2)
        elif axis_translation == cls.AXIS_TRANSLATION_MINUS_Y_TO_Y_AROUND_Z.identifier:
            rotation @= Quaternion((0, 0, 1), math.pi)
        elif axis_translation == cls.AXIS_TRANSLATION_Z_TO_Y.identifier:
            rotation @= Quaternion((1, 0, 0), math.pi / 2)
        elif axis_translation == cls.AXIS_TRANSLATION_MINUS_Z_TO_Y.identifier:
            rotation @= Quaternion((1, 0, 0), -math.pi / 2)

        # return Matrix.LocRotScale(location, rotation, scale)
        return (
            Matrix.Translation(location)
            @ rotation.to_matrix().to_4x4()
            @ Matrix.Diagonal(scale).to_4x4()
        )

    axis_translation: EnumProperty(  # type: ignore[valid-type]
        items=axis_translation_enum.items(),
        name="Axis Translation on Export",
    )

    if TYPE_CHECKING:
        # This code is auto generated.
        # To regenerate, run the `uv run tools/property_typing.py` command.
        uuid: str  # type: ignore[no-redef]
        axis_translation: str  # type: ignore[no-redef]


class VrmAddonObjectExtensionPropertyGroup(PropertyGroup):
    axis_translation: EnumProperty(  # type: ignore[valid-type]
        items=VrmAddonBoneExtensionPropertyGroup.axis_translation_enum.items(),
        name="Axis Translation on Export",
    )

    if TYPE_CHECKING:
        # This code is auto generated.
        # To regenerate, run the `uv run tools/property_typing.py` command.
        axis_translation: str  # type: ignore[no-redef]


class VrmAddonArmatureExtensionPropertyGroup(PropertyGroup):
    INITIAL_ADDON_VERSION = VrmAddonPreferences.INITIAL_ADDON_VERSION

    addon_version: IntVectorProperty(  # type: ignore[valid-type]
        size=3,
        default=INITIAL_ADDON_VERSION,
    )

    vrm0: PointerProperty(  # type: ignore[valid-type]
        type=Vrm0PropertyGroup
    )

    vrm1: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1PropertyGroup
    )

    spring_bone1: PointerProperty(  # type: ignore[valid-type]
        type=SpringBone1SpringBonePropertyGroup
    )

    node_constraint1: PointerProperty(  # type: ignore[valid-type]
        type=NodeConstraint1NodeConstraintPropertyGroup
    )

    SPEC_VERSION_VRM0 = "0.0"
    SPEC_VERSION_VRM1 = "1.0"
    spec_version_items = (
        (SPEC_VERSION_VRM0, "VRM 0.0", "", "NONE", 0),
        (SPEC_VERSION_VRM1, "VRM 1.0", "", "NONE", 1),
    )

    def update_spec_version(self, _context: Context) -> None:
        for blend_shape_group in self.vrm0.blend_shape_master.blend_shape_groups:
            blend_shape_group.preview = 0

        if self.spec_version == self.SPEC_VERSION_VRM0:
            vrm0_hidden = False
            vrm1_hidden = True
        elif self.spec_version == self.SPEC_VERSION_VRM1:
            vrm0_hidden = True
            vrm1_hidden = False
        else:
            return

        for vrm0_collider in [
            collider.bpy_object
            for collider_group in self.vrm0.secondary_animation.collider_groups
            for collider in collider_group.colliders
            if collider.bpy_object
        ]:
            vrm0_collider.hide_set(vrm0_hidden)

        for vrm1_collider in [
            collider.bpy_object
            for collider in self.spring_bone1.colliders
            if collider.bpy_object
        ]:
            vrm1_collider.hide_set(vrm1_hidden)
            for child in vrm1_collider.children:
                child.hide_set(vrm1_hidden)

    spec_version: EnumProperty(  # type: ignore[valid-type]
        items=spec_version_items,
        name="Spec Version",
        update=update_spec_version,
        default=SPEC_VERSION_VRM1,
    )

    def is_vrm0(self) -> bool:
        return str(self.spec_version) == self.SPEC_VERSION_VRM0

    def is_vrm1(self) -> bool:
        return str(self.spec_version) == self.SPEC_VERSION_VRM1

    @staticmethod
    def has_vrm_model_metadata(obj: Object) -> bool:
        if obj.type != "ARMATURE":
            return False
        armature = obj.data
        if not isinstance(armature, Armature):
            return False

        # https://github.com/saturday06/VRM-Addon-for-Blender/blob/2_0_3/io_scene_vrm/editor/migration.py#L372-L373
        ext = get_armature_extension(armature)
        if tuple(ext.addon_version) > (2, 0, 1):
            return True

        # https://github.com/saturday06/VRM-Addon-for-Blender/blob/0_79/importer/model_build.py#L731
        humanoid_params_key = obj.get("humanoid_params")
        if not isinstance(humanoid_params_key, str):
            return False

        # https://github.com/saturday06/VRM-Addon-for-Blender/blob/0_79/importer/model_build.py#L706
        return "hips" in armature

    if TYPE_CHECKING:
        # This code is auto generated.
        # To regenerate, run the `uv run tools/property_typing.py` command.
        addon_version: Sequence[int]  # type: ignore[no-redef]
        vrm0: Vrm0PropertyGroup  # type: ignore[no-redef]
        vrm1: Vrm1PropertyGroup  # type: ignore[no-redef]
        spring_bone1: SpringBone1SpringBonePropertyGroup  # type: ignore[no-redef]
        node_constraint1: (  # type: ignore[no-redef]
            NodeConstraint1NodeConstraintPropertyGroup
        )
        spec_version: str  # type: ignore[no-redef]


def update_internal_cache(context: Context) -> None:
    for armature in context.blend_data.armatures:
        Vrm0HumanoidPropertyGroup.update_all_node_candidates(context, armature.name)
        Vrm1HumanBonesPropertyGroup.update_all_node_candidates(context, armature.name)
    VrmAddonSceneExtensionPropertyGroup.update_vrm0_material_property_names(context)


class VrmAddonMaterialExtensionPropertyGroup(PropertyGroup):
    mtoon1: PointerProperty(  # type: ignore[valid-type]
        type=Mtoon1MaterialPropertyGroup
    )
    if TYPE_CHECKING:
        # This code is auto generated.
        # To regenerate, run the `uv run tools/property_typing.py` command.
        mtoon1: Mtoon1MaterialPropertyGroup  # type: ignore[no-redef]


class VrmAddonNodeTreeExtensionPropertyGroup(PropertyGroup):
    INITIAL_ADDON_VERSION = VrmAddonPreferences.INITIAL_ADDON_VERSION

    addon_version: IntVectorProperty(  # type: ignore[valid-type]
        size=3,
        default=INITIAL_ADDON_VERSION,
    )
    if TYPE_CHECKING:
        # This code is auto generated.
        # To regenerate, run the `uv run tools/property_typing.py` command.
        addon_version: Sequence[int]  # type: ignore[no-redef]


__Extension = TypeVar("__Extension")


def get_vrm_addon_extension_or_raise(
    obj: object, expected_type: type[__Extension]
) -> __Extension:
    extension = getattr(obj, "vrm_addon_extension", None)
    if isinstance(extension, expected_type):
        return extension

    message = f"{extension} is not a {expected_type} but {type(extension)}"
    raise TypeError(message)


def get_material_extension(
    material: Material,
) -> VrmAddonMaterialExtensionPropertyGroup:
    return get_vrm_addon_extension_or_raise(
        material, VrmAddonMaterialExtensionPropertyGroup
    )


def get_armature_extension(
    armature: Armature,
) -> VrmAddonArmatureExtensionPropertyGroup:
    return get_vrm_addon_extension_or_raise(
        armature, VrmAddonArmatureExtensionPropertyGroup
    )


def get_node_tree_extension(
    node_tree: NodeTree,
) -> VrmAddonNodeTreeExtensionPropertyGroup:
    return get_vrm_addon_extension_or_raise(
        node_tree, VrmAddonNodeTreeExtensionPropertyGroup
    )


def get_scene_extension(scene: Scene) -> VrmAddonSceneExtensionPropertyGroup:
    return get_vrm_addon_extension_or_raise(scene, VrmAddonSceneExtensionPropertyGroup)


def get_bone_extension(bone: Bone) -> VrmAddonBoneExtensionPropertyGroup:
    return get_vrm_addon_extension_or_raise(bone, VrmAddonBoneExtensionPropertyGroup)


def get_object_extension(obj: Object) -> VrmAddonObjectExtensionPropertyGroup:
    return get_vrm_addon_extension_or_raise(obj, VrmAddonObjectExtensionPropertyGroup)
