# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
import functools
from collections.abc import Sequence
from sys import float_info
from typing import TYPE_CHECKING, ClassVar, Optional

import bpy
from bpy.props import (
    BoolProperty,
    CollectionProperty,
    EnumProperty,
    FloatProperty,
    FloatVectorProperty,
    IntProperty,
    PointerProperty,
    StringProperty,
)
from bpy.types import (
    Action,
    Armature,
    Context,
    Image,
    Material,
    Mesh,
    Object,
    PropertyGroup,
)
from mathutils import Vector

from ...common import convert
from ...common.logger import get_logger
from ...common.vrm0.human_bone import (
    HumanBoneName,
    HumanBoneSpecification,
    HumanBoneSpecifications,
)
from ..property_group import (
    BonePropertyGroup,
    FloatPropertyGroup,
    MeshObjectPropertyGroup,
    StringPropertyGroup,
    property_group_enum,
)
from ..vrm1.property_group import Vrm1HumanoidPropertyGroup

if TYPE_CHECKING:
    from ..property_group import CollectionPropertyProtocol


logger = get_logger(__name__)


# https://github.com/vrm-c/UniVRM/blob/v0.91.1/Assets/VRM/Runtime/Format/glTF_VRM_Humanoid.cs#L70-L164
class Vrm0HumanoidBonePropertyGroup(PropertyGroup):
    bone: StringProperty(  # type: ignore[valid-type]
        name="VRM Humanoid Bone Name"
    )
    node: PointerProperty(  # type: ignore[valid-type]
        name="Bone Name",
        type=BonePropertyGroup,
    )
    use_default_values: BoolProperty(  # type: ignore[valid-type]
        name="Unity's HumanLimit.useDefaultValues",
        default=True,
    )
    min: FloatVectorProperty(  # type: ignore[valid-type]
        size=3,
        name="Unity's HumanLimit.min",
    )
    max: FloatVectorProperty(  # type: ignore[valid-type]
        size=3,
        name="Unity's HumanLimit.max",
    )
    center: FloatVectorProperty(  # type: ignore[valid-type]
        size=3,
        name="Unity's HumanLimit.center",
    )
    axis_length: FloatProperty(  # type: ignore[valid-type]
        name="Unity's HumanLimit.axisLength"
    )

    # for UI
    node_candidates: CollectionProperty(  # type: ignore[valid-type]
        type=StringPropertyGroup
    )

    def update_node_candidates(
        self,
        armature_data: Armature,
        bpy_bone_name_to_human_bone_specification: dict[str, HumanBoneSpecification],
        error_bpy_bone_names: Sequence[str],
    ) -> bool:
        human_bone_name = HumanBoneName.from_str(self.bone)
        if human_bone_name is None:
            logger.warning("Bone name '%s' is invalid", self.bone)
            return False
        target = HumanBoneSpecifications.get(human_bone_name)
        new_candidates = BonePropertyGroup.find_bone_candidates(
            armature_data,
            target,
            bpy_bone_name_to_human_bone_specification,
            error_bpy_bone_names,
        )
        if {n.value for n in self.node_candidates} == new_candidates:
            return False

        self.node_candidates.clear()
        # Preserving list order
        for bone_name in armature_data.bones.keys():
            if bone_name not in new_candidates:
                continue
            candidate = self.node_candidates.add()
            candidate.value = bone_name

        return True

    def specification(self) -> HumanBoneSpecification:
        name = HumanBoneName.from_str(self.bone)
        if name is None:
            message = f'HumanBone "{self.bone}" is invalid'
            raise ValueError(message)
        return HumanBoneSpecifications.get(name)

    if TYPE_CHECKING:
        # This code is auto generated.
        # To regenerate, run the `uv run tools/property_typing.py` command.
        bone: str  # type: ignore[no-redef]
        node: BonePropertyGroup  # type: ignore[no-redef]
        use_default_values: bool  # type: ignore[no-redef]
        min: Sequence[float]  # type: ignore[no-redef]
        max: Sequence[float]  # type: ignore[no-redef]
        center: Sequence[float]  # type: ignore[no-redef]
        axis_length: float  # type: ignore[no-redef]
        node_candidates: CollectionPropertyProtocol[  # type: ignore[no-redef]
            StringPropertyGroup
        ]


# https://github.com/vrm-c/UniVRM/blob/v0.91.1/Assets/VRM/Runtime/Format/glTF_VRM_Humanoid.cs#L166-L195
class Vrm0HumanoidPropertyGroup(PropertyGroup):
    human_bones: CollectionProperty(  # type: ignore[valid-type]
        name="Human Bones",
        type=Vrm0HumanoidBonePropertyGroup,
    )
    arm_stretch: FloatProperty(  # type: ignore[valid-type]
        name="Arm Stretch",
        default=0.05,
    )
    leg_stretch: FloatProperty(  # type: ignore[valid-type]
        name="Leg Stretch",
        default=0.05,
    )
    upper_arm_twist: FloatProperty(  # type: ignore[valid-type]
        name="Upper Arm Twist",
        default=0.5,
    )
    lower_arm_twist: FloatProperty(  # type: ignore[valid-type]
        name="Lower Arm Twist",
        default=0.5,
    )
    upper_leg_twist: FloatProperty(  # type: ignore[valid-type]
        name="Upper Leg Twist",
        default=0.5,
    )
    lower_leg_twist: FloatProperty(  # type: ignore[valid-type]
        name="Lower Leg Twist",
        default=0.5,
    )
    feet_spacing: FloatProperty(  # type: ignore[valid-type]
        name="Feet Spacing",
        default=0,
    )
    has_translation_dof: BoolProperty(  # type: ignore[valid-type]
        name="Has Translation DoF",
        default=False,
    )

    POSE_AUTO_POSE = Vrm1HumanoidPropertyGroup.POSE_AUTO_POSE
    POSE_REST_POSITION_POSE = Vrm1HumanoidPropertyGroup.POSE_REST_POSITION_POSE
    POSE_CURRENT_POSE = Vrm1HumanoidPropertyGroup.POSE_CURRENT_POSE
    POSE_CUSTOM_POSE = Vrm1HumanoidPropertyGroup.POSE_CUSTOM_POSE

    pose: bpy.props.EnumProperty(  # type: ignore[valid-type]
        items=Vrm1HumanoidPropertyGroup.pose_enum.items(),
        name="T-Pose",
        description="T-Pose",
        default=POSE_AUTO_POSE.identifier,
    )

    # for T-Pose
    def update_pose_library(self, _context: Context) -> None:
        self.pose_marker_name = ""

    pose_library: PointerProperty(  # type: ignore[valid-type]
        type=Action,
        name="Pose Library",
        description="Pose library for T Pose",
        update=update_pose_library,
    )
    pose_marker_name: StringProperty()  # type: ignore[valid-type]

    # for UI
    last_bone_names_str: StringProperty()  # type: ignore[valid-type]
    initial_automatic_bone_assignment: BoolProperty(  # type: ignore[valid-type]
        default=True
    )

    def all_required_bones_are_assigned(self) -> bool:
        for name in HumanBoneSpecifications.required_names:
            for human_bone in self.human_bones:
                if human_bone.bone != name:
                    continue
                if human_bone.node.bone_name not in human_bone.node_candidates:
                    return False
        return True

    @staticmethod
    def defer_update_all_node_candidates(
        armature_data_name: str,
        *,
        force: bool = False,
    ) -> None:
        bpy.app.timers.register(
            functools.partial(
                Vrm0HumanoidPropertyGroup.update_all_node_candidates_timer_callback,
                armature_data_name,
                force=force,
            )
        )

    @staticmethod
    def update_all_node_candidates_timer_callback(
        armature_object_name: str, *, force: bool = False
    ) -> None:
        """Wrap update_all_node_candidates() to match bpy.app.timers.register."""
        context = bpy.context  # Context cannot cross frames so we need to get a new one
        Vrm0HumanoidPropertyGroup.update_all_node_candidates(
            context, armature_object_name, force=force
        )

    @staticmethod
    def update_all_node_candidates(
        context: Optional[Context],
        armature_data_name: str,
        *,
        force: bool = False,
    ) -> None:
        from ..extension import get_armature_extension

        if context is None:
            context = bpy.context

        armature_data = context.blend_data.armatures.get(armature_data_name)
        if not armature_data:
            return

        humanoid = get_armature_extension(armature_data).vrm0.humanoid

        bone_names_str = "\n".join(
            bone.name + "\n" + (parent.name if (parent := bone.parent) else "")
            for bone in sorted(armature_data.bones.values(), key=lambda b: b.name)
        )

        if not force and humanoid.last_bone_names_str == bone_names_str:
            return
        humanoid.last_bone_names_str = bone_names_str

        BonePropertyGroup.update_all_vrm0_node_candidates(armature_data)

    @staticmethod
    def fixup_human_bones(obj: Object) -> None:
        from ..extension import get_armature_extension

        armature_data = obj.data
        if not isinstance(armature_data, Armature):
            return

        humanoid = get_armature_extension(armature_data).vrm0.humanoid

        # Add bone maps that don't exist
        refresh = False
        for human_bone_name in HumanBoneSpecifications.all_names:
            if any(
                human_bone.bone == human_bone_name
                for human_bone in humanoid.human_bones
            ):
                continue
            human_bone = humanoid.human_bones.add()
            human_bone.bone = human_bone_name
            refresh = True

        # Remove duplicate bone maps
        fixup = True
        while fixup:
            fixup = False
            found_bones: list[str] = []
            for i, human_bone in enumerate(list(humanoid.human_bones)):
                if (
                    human_bone.bone in HumanBoneSpecifications.all_names
                    and human_bone.bone not in found_bones
                ):
                    found_bones.append(human_bone.bone)
                    continue
                humanoid.human_bones.remove(i)
                refresh = True
                fixup = True
                break

        # If multiple bone maps have the same Blender bone assigned, remove one of them
        fixup = True
        while fixup:
            fixup = False
            found_node_bone_names: list[str] = []
            for human_bone in humanoid.human_bones:
                if not human_bone.node.bone_name:
                    continue
                if human_bone.node.bone_name not in found_node_bone_names:
                    found_node_bone_names.append(human_bone.node.bone_name)
                    continue
                human_bone.node.bone_name = ""
                refresh = True
                fixup = True
                break

        if not refresh:
            return

        secondary_animation = get_armature_extension(
            armature_data
        ).vrm0.secondary_animation
        for collider_group in secondary_animation.collider_groups:
            collider_group.refresh(obj)
        for bone_group in secondary_animation.bone_groups:
            bone_group.refresh(obj)

    if TYPE_CHECKING:
        # This code is auto generated.
        # To regenerate, run the `uv run tools/property_typing.py` command.
        human_bones: CollectionPropertyProtocol[  # type: ignore[no-redef]
            Vrm0HumanoidBonePropertyGroup
        ]
        arm_stretch: float  # type: ignore[no-redef]
        leg_stretch: float  # type: ignore[no-redef]
        upper_arm_twist: float  # type: ignore[no-redef]
        lower_arm_twist: float  # type: ignore[no-redef]
        upper_leg_twist: float  # type: ignore[no-redef]
        lower_leg_twist: float  # type: ignore[no-redef]
        feet_spacing: float  # type: ignore[no-redef]
        has_translation_dof: bool  # type: ignore[no-redef]
        pose: str  # type: ignore[no-redef]
        pose_library: Optional[Action]  # type: ignore[no-redef]
        pose_marker_name: str  # type: ignore[no-redef]
        last_bone_names_str: str  # type: ignore[no-redef]
        initial_automatic_bone_assignment: bool  # type: ignore[no-redef]


# https://github.com/vrm-c/UniVRM/blob/v0.91.1/Assets/VRM/Runtime/Format/glTF_VRM_FirstPerson.cs#L10-L22
class Vrm0DegreeMapPropertyGroup(PropertyGroup):
    curve: FloatVectorProperty(  # type: ignore[valid-type]
        size=8,
        name="Curve",
        default=(0, 0, 0, 1, 1, 1, 1, 0),
    )
    x_range: FloatProperty(  # type: ignore[valid-type]
        name="X Range",
        default=90,
    )
    y_range: FloatProperty(  # type: ignore[valid-type]
        name="Y Range",
        default=10,
    )

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


# https://github.com/vrm-c/UniVRM/blob/v0.91.1/Assets/VRM/Runtime/Format/glTF_VRM_FirstPerson.cs#L32-L41
class Vrm0MeshAnnotationPropertyGroup(PropertyGroup):
    mesh: PointerProperty(  # type: ignore[valid-type]
        name="Mesh",
        type=MeshObjectPropertyGroup,
        description="Mesh on restrict render in the first person camera",
    )

    first_person_flag_enum, *__first_person_flags = property_group_enum(
        ("Auto", "Auto", "Auto restrict render", "NONE", 0),
        (
            "FirstPersonOnly",
            "First-Person Only",
            "(Maybe needless) Restrict render in the third person camera",
            "NONE",
            1,
        ),
        (
            "ThirdPersonOnly",
            "Third-Person Only",
            "Restrict render in the first person camera for face, hairs or hat",
            "NONE",
            2,
        ),
        ("Both", "Both", "No restrict render for body, arms or legs", "NONE", 3),
    )

    first_person_flag: EnumProperty(  # type: ignore[valid-type]
        items=first_person_flag_enum.items(),
        name="First Person Flag",
        description="Restrict render in the first person camera",
    )

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


# https://github.com/vrm-c/UniVRM/blob/v0.91.1/Assets/VRM/Runtime/Format/glTF_VRM_FirstPerson.cs#L50-L91
class Vrm0FirstPersonPropertyGroup(PropertyGroup):
    first_person_bone: PointerProperty(  # type: ignore[valid-type]
        name="First Person Bone",
        type=BonePropertyGroup,
        description="Bone to follow the first person camera",
    )
    first_person_bone_offset: FloatVectorProperty(  # type: ignore[valid-type]
        size=3,
        name="First Person Bone Offset",
        description=(
            "Offset from the first person bone to follow the first person camera"
        ),
        subtype="TRANSLATION",
        unit="LENGTH",
        default=(0, 0, 0),
    )
    mesh_annotations: CollectionProperty(  # type: ignore[valid-type]
        name="Mesh Annotations",
        type=Vrm0MeshAnnotationPropertyGroup,
    )
    look_at_type_name_enum, *__look_at_types = property_group_enum(
        ("Bone", "Bone", "Use bones to eye movement", "BONE_DATA", 0),
        (
            "BlendShape",
            "Blend Shape",
            "Use blend Shapes of VRM Blend Shape Proxy to eye movement.",
            "SHAPEKEY_DATA",
            1,
        ),
    )
    look_at_type_name: EnumProperty(  # type: ignore[valid-type]
        items=look_at_type_name_enum.items(),
        name="Look At Type Name",
        description="How to eye movement",
    )
    look_at_horizontal_inner: PointerProperty(  # type: ignore[valid-type]
        type=Vrm0DegreeMapPropertyGroup,
        name="Look At Horizontal Inner",
    )
    look_at_horizontal_outer: PointerProperty(  # type: ignore[valid-type]
        type=Vrm0DegreeMapPropertyGroup,
        name="Look At Horizontal Outer",
    )
    look_at_vertical_down: PointerProperty(  # type: ignore[valid-type]
        type=Vrm0DegreeMapPropertyGroup,
        name="Look At Vertical Down",
    )
    look_at_vertical_up: PointerProperty(  # type: ignore[valid-type]
        type=Vrm0DegreeMapPropertyGroup,
        name="lookAt Vertical Up",
    )

    # for UI
    active_mesh_annotation_index: IntProperty(min=0)  # type: ignore[valid-type]

    if TYPE_CHECKING:
        # This code is auto generated.
        # To regenerate, run the `uv run tools/property_typing.py` command.
        first_person_bone: BonePropertyGroup  # type: ignore[no-redef]
        first_person_bone_offset: Sequence[float]  # type: ignore[no-redef]
        mesh_annotations: CollectionPropertyProtocol[  # type: ignore[no-redef]
            Vrm0MeshAnnotationPropertyGroup
        ]
        look_at_type_name: str  # type: ignore[no-redef]
        look_at_horizontal_inner: Vrm0DegreeMapPropertyGroup  # type: ignore[no-redef]
        look_at_horizontal_outer: Vrm0DegreeMapPropertyGroup  # type: ignore[no-redef]
        look_at_vertical_down: Vrm0DegreeMapPropertyGroup  # type: ignore[no-redef]
        look_at_vertical_up: Vrm0DegreeMapPropertyGroup  # type: ignore[no-redef]
        active_mesh_annotation_index: int  # type: ignore[no-redef]


# https://github.com/vrm-c/UniVRM/blob/v0.91.1/Assets/VRM/Runtime/Format/glTF_VRM_BlendShape.cs#L18-L30
class Vrm0BlendShapeBindPropertyGroup(PropertyGroup):
    mesh: PointerProperty(  # type: ignore[valid-type]
        name="Mesh",
        type=MeshObjectPropertyGroup,
    )
    index: StringProperty(  # type: ignore[valid-type]
        name="Index"
    )
    weight: FloatProperty(  # type: ignore[valid-type]
        name="Weight",
        min=0,
        default=1,
        max=1,
        subtype="FACTOR",
    )

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


# https://github.com/vrm-c/UniVRM/blob/v0.91.1/Assets/VRM/Runtime/Format/glTF_VRM_BlendShape.cs#L9-L16
class Vrm0MaterialValueBindPropertyGroup(PropertyGroup):
    material: PointerProperty(  # type: ignore[valid-type]
        name="Material",
        type=Material,
    )

    # Use StringProperty instead of EnumProperty for property_name.
    # This is necessary to allow arbitrary values to be entered.
    property_name: StringProperty(  # type: ignore[valid-type]
        name="Property Name"
    )

    target_value: CollectionProperty(  # type: ignore[valid-type]
        name="Target Value",
        type=FloatPropertyGroup,
    )

    if TYPE_CHECKING:
        # This code is auto generated.
        # To regenerate, run the `uv run tools/property_typing.py` command.
        material: Optional[Material]  # type: ignore[no-redef]
        property_name: str  # type: ignore[no-redef]
        target_value: CollectionPropertyProtocol[  # type: ignore[no-redef]
            FloatPropertyGroup
        ]


# https://github.com/vrm-c/UniVRM/blob/v0.91.1/Assets/VRM/Runtime/Format/glTF_VRM_BlendShape.cs#L62-L99
class Vrm0BlendShapeGroupPropertyGroup(PropertyGroup):
    name: StringProperty(  # type: ignore[valid-type]
        name="Name",
        description="Name of the blendshape group",
    )

    preset_name_enum, (PRESET_NAME_UNKNOWN, *__preset_names) = property_group_enum(
        ("unknown", "Unknown", "", "SHAPEKEY_DATA", 0),
        ("neutral", "Neutral", "", "VIEW_ORTHO", 1),
        ("a", "A", "", "EVENT_A", 2),
        ("i", "I", "", "EVENT_I", 3),
        ("u", "U", "", "EVENT_U", 4),
        ("e", "E", "", "EVENT_E", 5),
        ("o", "O", "", "EVENT_O", 6),
        ("blink", "Blink", "", "HIDE_ON", 7),
        ("joy", "Joy", "", "HEART", 8),
        ("angry", "Angry", "", "ORPHAN_DATA", 9),
        ("sorrow", "Sorrow", "", "MOD_FLUIDSIM", 10),
        ("fun", "Fun", "", "LIGHT_SUN", 11),
        ("lookup", "Look Up", "", "ANCHOR_TOP", 12),
        ("lookdown", "Look Down", "", "ANCHOR_BOTTOM", 13),
        ("lookleft", "Look Left", "", "ANCHOR_RIGHT", 14),
        ("lookright", "Look Right", "", "ANCHOR_LEFT", 15),
        ("blink_l", "Blink_L", "", "HIDE_ON", 16),
        ("blink_r", "Blink_R", "", "HIDE_ON", 17),
    )

    preset_name: EnumProperty(  # type: ignore[valid-type]
        items=preset_name_enum.items(),
        name="Preset",
        description="Preset name in VRM avatar",
    )
    binds: CollectionProperty(  # type: ignore[valid-type]
        type=Vrm0BlendShapeBindPropertyGroup,
        name="Binds",
    )
    material_values: CollectionProperty(  # type: ignore[valid-type]
        type=Vrm0MaterialValueBindPropertyGroup,
        name="Material Values",
    )
    is_binary: BoolProperty(  # type: ignore[valid-type]
        name="Is Binary",
        description="Use binary change in the blendshape group",
    )

    # for UI
    active_bind_index: IntProperty(min=0)  # type: ignore[valid-type]
    active_material_value_index: IntProperty(min=0)  # type: ignore[valid-type]

    # During animation playback, shape key values can only be changed
    # in frame_change_pre/frame_change_post, so we store the changed values here
    frame_change_post_shape_key_updates: ClassVar[dict[tuple[str, str], float]] = {}

    def get_preview(self) -> float:
        value = self.get("preview")
        if isinstance(value, (float, int)):
            return float(value)
        return 0.0

    def set_preview(self, value_obj: object) -> None:
        context = bpy.context

        value = convert.float_or_none(value_obj)
        if value is None:
            return

        current_value = convert.float_or_none(self.get("preview"))
        if (
            current_value is not None
            and abs(current_value - value) < float_info.epsilon
        ):
            return

        self["preview"] = value

        for bind in self.binds:
            mesh_object = context.blend_data.objects.get(bind.mesh.mesh_object_name)
            if not mesh_object or mesh_object.type != "MESH":
                continue
            mesh = mesh_object.data
            if not isinstance(mesh, Mesh):
                continue
            mesh_shape_keys = mesh.shape_keys
            if not mesh_shape_keys:
                continue
            shape_key = context.blend_data.shape_keys.get(mesh_shape_keys.name)
            if not shape_key:
                continue
            key_blocks = shape_key.key_blocks
            if not key_blocks:
                continue
            if bind.index not in key_blocks:
                continue
            if self.is_binary:
                # https://github.com/vrm-c/UniVRM/blob/38ccb92300c9ab41c72eb3d5b8dc8ce664a659d5/Assets/VRM/Runtime/BlendShape/BlendShapeMerger.cs#L89-L92
                preview = 0.0 if self.preview < 0.5 else 1.0
            else:
                preview = self.preview
            key_block_value = bind.weight * preview  # Lerp 0.0 * (1 - a) + weight * a
            key_blocks[bind.index].value = key_block_value
            Vrm0BlendShapeGroupPropertyGroup.frame_change_post_shape_key_updates[
                (shape_key.name, bind.index)
            ] = key_block_value

    preview: FloatProperty(  # type: ignore[valid-type]
        name="Blend Shape Proxy",
        min=0,
        max=1,
        subtype="FACTOR",
        get=get_preview,
        set=set_preview,
    )

    if TYPE_CHECKING:
        # This code is auto generated.
        # To regenerate, run the `uv run tools/property_typing.py` command.
        name: str  # type: ignore[no-redef]
        preset_name: str  # type: ignore[no-redef]
        binds: CollectionPropertyProtocol[  # type: ignore[no-redef]
            Vrm0BlendShapeBindPropertyGroup
        ]
        material_values: CollectionPropertyProtocol[  # type: ignore[no-redef]
            Vrm0MaterialValueBindPropertyGroup
        ]
        is_binary: bool  # type: ignore[no-redef]
        active_bind_index: int  # type: ignore[no-redef]
        active_material_value_index: int  # type: ignore[no-redef]
        preview: float  # type: ignore[no-redef]


# https://github.com/vrm-c/UniVRM/blob/v0.91.1/Assets/VRM/Runtime/Format/glTF_VRM_SecondaryAnimation.cs#L10-L18
class Vrm0SecondaryAnimationColliderPropertyGroup(PropertyGroup):
    bpy_object: PointerProperty(  # type: ignore[valid-type]
        type=Object
    )

    def refresh(self, armature: Object, bone_name: str) -> None:
        if not self.bpy_object or not self.bpy_object.name:
            return

        if self.bpy_object.parent != armature:
            self.bpy_object.parent = armature
        if self.bpy_object.empty_display_type != "SPHERE":
            self.bpy_object.empty_display_type = "SPHERE"

        if bone_name:
            if self.bpy_object.parent_type != "BONE":
                self.bpy_object.parent_type = "BONE"
            if self.bpy_object.parent_bone != bone_name:
                self.bpy_object.parent_bone = bone_name
        elif self.bpy_object.parent_type != "OBJECT":
            self.bpy_object.parent_type = "OBJECT"

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


# https://github.com/vrm-c/vrm-specification/blob/f2d8f158297fc883aef9c3071ca68fbe46b03f45/specification/0.0/schema/vrm.secondaryanimation.collidergroup.schema.json
# https://github.com/vrm-c/UniVRM/blob/v0.91.1/Assets/VRM/Runtime/Format/glTF_VRM_SecondaryAnimation.cs#L21-L29
class Vrm0SecondaryAnimationColliderGroupPropertyGroup(PropertyGroup):
    node: PointerProperty(  # type: ignore[valid-type]
        name="Node",
        type=BonePropertyGroup,
    )
    # Use the collider's own data for offset and radius
    colliders: CollectionProperty(  # type: ignore[valid-type]
        name="Colliders",
        type=Vrm0SecondaryAnimationColliderPropertyGroup,
    )

    def refresh(self, armature: Object) -> None:
        from ..extension import get_armature_extension

        name = (
            str(self.node.bone_name) if self.node and self.node.bone_name else ""
        ) + f"#{self.uuid}"

        if self.name != name:
            self.name = name

        for index, collider in reversed(
            tuple((index, collider) for index, collider in enumerate(self.colliders))
        ):
            if not collider.bpy_object or not collider.bpy_object.name:
                self.colliders.remove(index)
            else:
                collider.refresh(armature, self.node.bone_name)

        armature_data = armature.data
        if not isinstance(armature_data, Armature):
            return

        for bone_group in get_armature_extension(
            armature_data
        ).vrm0.secondary_animation.bone_groups:
            bone_group.refresh(armature)

    # for UI
    show_expanded: BoolProperty()  # type: ignore[valid-type]

    active_collider_index: IntProperty(min=0)  # type: ignore[valid-type]

    # for reference from Vrm0SecondaryAnimationGroupPropertyGroup
    name: StringProperty()  # type: ignore[valid-type]
    uuid: StringProperty()  # type: ignore[valid-type]

    if TYPE_CHECKING:
        # This code is auto generated.
        # To regenerate, run the `uv run tools/property_typing.py` command.
        node: BonePropertyGroup  # type: ignore[no-redef]
        colliders: CollectionPropertyProtocol[  # type: ignore[no-redef]
            Vrm0SecondaryAnimationColliderPropertyGroup
        ]
        show_expanded: bool  # type: ignore[no-redef]
        active_collider_index: int  # type: ignore[no-redef]
        name: str  # type: ignore[no-redef]
        uuid: str  # type: ignore[no-redef]


# https://github.com/vrm-c/UniVRM/blob/v0.91.1/Assets/VRM/Runtime/Format/glTF_VRM_SecondaryAnimation.cs#L32-L67
class Vrm0SecondaryAnimationGroupPropertyGroup(PropertyGroup):
    comment: StringProperty(  # type: ignore[valid-type]
        name="Comment",
        description="Comment about the purpose of springs",
    )

    # typo in VRM 0.0 specification
    # https://github.com/vrm-c/vrm-specification/blob/1723a45abfb4f12ac5d3635a3f66dc45e2f93c83/specification/0.0/schema/vrm.secondaryanimation.spring.schema.json#L9-L12
    stiffiness: FloatProperty(  # type: ignore[valid-type]
        name="Stiffness",
        min=0.0,
        soft_max=4.0,
        subtype="FACTOR",
        description="Stiffness of springs",
    )

    gravity_power: FloatProperty(  # type: ignore[valid-type]
        name="Gravity Power",
        min=0.0,
        soft_max=2.0,
        subtype="FACTOR",
        description="Gravity power of springs",
    )

    def update_gravity_dir(self, _context: Context) -> None:
        gravity_dir = Vector(self.gravity_dir)
        normalized_gravity_dir = gravity_dir.normalized()
        if (gravity_dir - normalized_gravity_dir).length > 0.0001:
            self.gravity_dir = normalized_gravity_dir

    gravity_dir: FloatVectorProperty(  # type: ignore[valid-type]
        size=3,
        min=-1,
        max=1,
        subtype="XYZ",
        name="Gravity Direction",
        description="Gravity direction of springs",
        update=update_gravity_dir,
    )
    drag_force: FloatProperty(  # type: ignore[valid-type]
        name="Drag Force",
        min=0.0,
        max=1.0,
        subtype="FACTOR",
        description="Drag Force of springs",
    )
    center: PointerProperty(  # type: ignore[valid-type]
        name="Center",
        type=BonePropertyGroup,
        description="Origin of Physics simulation to stop springs on moving",
    )
    hit_radius: FloatProperty(  # type: ignore[valid-type]
        name="Hit Radius",
        min=0.0,
        soft_max=0.5,
        subtype="DISTANCE",
        description="Hit Radius of springs",
    )
    bones: CollectionProperty(  # type: ignore[valid-type]
        name="Bones",
        type=BonePropertyGroup,
        description="Bones of the spring roots",
    )
    collider_groups: CollectionProperty(  # type: ignore[valid-type]
        name="Collider Group",
        type=StringPropertyGroup,
        description="Enabled collider Groups of springs",
    )

    # for UI
    show_expanded: BoolProperty()  # type: ignore[valid-type]
    show_expanded_bones: BoolProperty(  # type: ignore[valid-type]
        name="Bones"
    )
    show_expanded_collider_groups: BoolProperty(  # type: ignore[valid-type]
        name="Collider Groups"
    )

    active_bone_index: IntProperty(min=0)  # type: ignore[valid-type]
    active_collider_group_index: IntProperty(min=0)  # type: ignore[valid-type]

    def refresh(self, armature: Object) -> None:
        from ..extension import get_armature_extension

        armature_data = armature.data
        if not isinstance(armature_data, Armature):
            return
        ext = get_armature_extension(armature_data)
        collider_group_uuid_to_name = {
            collider_group.uuid: collider_group.name
            for collider_group in ext.vrm0.secondary_animation.collider_groups
        }
        for index, collider_group in reversed(list(enumerate(self.collider_groups))):
            uuid_str = collider_group.value.split("#")[-1]
            if not uuid_str:
                self.collider_groups.remove(index)
                continue

            name = collider_group_uuid_to_name.get(uuid_str)
            if not name:
                self.collider_groups.remove(index)
                continue

            if collider_group.value != name:
                collider_group.value = name

    if TYPE_CHECKING:
        # This code is auto generated.
        # To regenerate, run the `uv run tools/property_typing.py` command.
        comment: str  # type: ignore[no-redef]
        stiffiness: float  # type: ignore[no-redef]
        gravity_power: float  # type: ignore[no-redef]
        gravity_dir: Sequence[float]  # type: ignore[no-redef]
        drag_force: float  # type: ignore[no-redef]
        center: BonePropertyGroup  # type: ignore[no-redef]
        hit_radius: float  # type: ignore[no-redef]
        bones: CollectionPropertyProtocol[  # type: ignore[no-redef]
            BonePropertyGroup
        ]
        collider_groups: CollectionPropertyProtocol[  # type: ignore[no-redef]
            StringPropertyGroup
        ]
        show_expanded: bool  # type: ignore[no-redef]
        show_expanded_bones: bool  # type: ignore[no-redef]
        show_expanded_collider_groups: bool  # type: ignore[no-redef]
        active_bone_index: int  # type: ignore[no-redef]
        active_collider_group_index: int  # type: ignore[no-redef]


# https://github.com/vrm-c/UniVRM/blob/v0.91.1/Assets/VRM/Runtime/Format/glTF_VRM_Meta.cs#L33-L149
class Vrm0MetaPropertyGroup(PropertyGroup):
    allowed_user_name_enum, __allowed_user_names = property_group_enum(
        ("OnlyAuthor", "Only Author", "", "NONE", 0),
        ("ExplicitlyLicensedPerson", "Explicitly Licensed Person", "", "NONE", 1),
        ("Everyone", "Everyone", "", "NONE", 2),
    )

    violent_ussage_name_enum, __violent_ussage_names = property_group_enum(
        ("Disallow", "Disallow", "", "NONE", 0),
        ("Allow", "Allow", "", "NONE", 1),
    )

    sexual_ussage_name_enum, __sexual_ussage_names = property_group_enum(
        ("Disallow", "Disallow", "", "NONE", 0),
        ("Allow", "Allow", "", "NONE", 1),
    )

    commercial_ussage_name_enum, __commercial_ussage_names = property_group_enum(
        ("Disallow", "Disallow", "", "NONE", 0),
        ("Allow", "Allow", "", "NONE", 1),
    )

    (
        license_name_enum,
        (
            LICENSE_NAME_REDISTRIBUTION_PROHIBITED,
            LICENSE_NAME_CC0,
            LICENSE_NAME_CC_BY,
            LICENSE_NAME_CC_BY_NC,
            LICENSE_NAME_CC_BY_SA,
            LICENSE_NAME_CC_BY_NC_SA,
            LICENSE_NAME_CC_BY_ND,
            LICENSE_NAME_CC_BY_NC_ND,
            LICENSE_NAME_OTHER,
        ),
    ) = property_group_enum(
        ("Redistribution_Prohibited", "Redistribution Prohibited", "", "NONE", 0),
        ("CC0", "CC0", "", "NONE", 1),
        ("CC_BY", "CC BY", "", "NONE", 2),
        ("CC_BY_NC", "CC BY NC", "", "NONE", 3),
        ("CC_BY_SA", "CC BY SA", "", "NONE", 4),
        ("CC_BY_NC_SA", "CC BY NC SA", "", "NONE", 5),
        ("CC_BY_ND", "CC BY ND", "", "NONE", 6),  # codespell:ignore nd
        ("CC_BY_NC_ND", "CC BY NC ND", "", "NONE", 7),  # codespell:ignore nd
        ("Other", "Other", "", "NONE", 8),
    )

    title: StringProperty(  # type: ignore[valid-type]
        name="Title",
        description="Title of the avatar",
    )
    version: StringProperty(  # type: ignore[valid-type]
        name="Version",
        description="Version of the avatar",
    )
    author: StringProperty(  # type: ignore[valid-type]
        name="Author",
        description="Author of the avatar",
    )
    contact_information: StringProperty(  # type: ignore[valid-type]
        name="Contact Information",
        description="Contact Information about the avatar",
    )
    reference: StringProperty(  # type: ignore[valid-type]
        name="Reference",
        description="Referenced works about the avatar",
    )
    allowed_user_name: EnumProperty(  # type: ignore[valid-type]
        items=allowed_user_name_enum.items(),
        name="Allowed User",
        description="Allowed user of the avatar",
    )
    violent_ussage_name: EnumProperty(  # type: ignore[valid-type]
        items=violent_ussage_name_enum.items(),
        name="Violent Usage",
        description="Violent usage of the avatar",
    )
    sexual_ussage_name: EnumProperty(  # type: ignore[valid-type]
        items=sexual_ussage_name_enum.items(),
        name="Sexual Usage",
        description="Sexual Usage of the avatar",
    )
    commercial_ussage_name: EnumProperty(  # type: ignore[valid-type]
        items=commercial_ussage_name_enum.items(),
        name="Commercial Usage",
        description="Commercial Usage of the avatar",
    )
    other_permission_url: StringProperty(  # type: ignore[valid-type]
        name="Other Permission URL",
        description="URL about other permissions of the avatar",
    )
    license_name: EnumProperty(  # type: ignore[valid-type]
        items=license_name_enum.items(),
        name="License",
        description="License of the avatar",
    )
    other_license_url: StringProperty(  # type: ignore[valid-type]
        name="Other License URL",
        description="URL about other License of the avatar",
    )
    texture: PointerProperty(  # type: ignore[valid-type]
        name="Thumbnail",
        type=Image,
        description="Thumbnail of the avatar",
    )

    if TYPE_CHECKING:
        # This code is auto generated.
        # To regenerate, run the `uv run tools/property_typing.py` command.
        title: str  # type: ignore[no-redef]
        version: str  # type: ignore[no-redef]
        author: str  # type: ignore[no-redef]
        contact_information: str  # type: ignore[no-redef]
        reference: str  # type: ignore[no-redef]
        allowed_user_name: str  # type: ignore[no-redef]
        violent_ussage_name: str  # type: ignore[no-redef]
        sexual_ussage_name: str  # type: ignore[no-redef]
        commercial_ussage_name: str  # type: ignore[no-redef]
        other_permission_url: str  # type: ignore[no-redef]
        license_name: str  # type: ignore[no-redef]
        other_license_url: str  # type: ignore[no-redef]
        texture: Optional[Image]  # type: ignore[no-redef]


# https://github.com/vrm-c/UniVRM/blob/v0.91.1/Assets/VRM/Runtime/Format/glTF_VRM_BlendShape.cs#L101-L106
class Vrm0BlendShapeMasterPropertyGroup(PropertyGroup):
    blend_shape_groups: CollectionProperty(  # type: ignore[valid-type]
        name="Blend Shape Group",
        type=Vrm0BlendShapeGroupPropertyGroup,
    )

    def restore_blend_shape_group_bind_object_assignments(
        self, context: Context
    ) -> None:
        for blend_shape_group in self.blend_shape_groups:
            for bind in blend_shape_group.binds:
                bind.mesh.restore_object_assignment(context)

    # for UI
    active_blend_shape_group_index: IntProperty(min=0)  # type: ignore[valid-type]

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


# https://github.com/vrm-c/UniVRM/blob/v0.91.1/Assets/VRM/Runtime/Format/glTF_VRM_SecondaryAnimation.cs#L69-L78
class Vrm0SecondaryAnimationPropertyGroup(PropertyGroup):
    bone_groups: CollectionProperty(  # type: ignore[valid-type]
        name="Secondary Animation Groups",
        type=Vrm0SecondaryAnimationGroupPropertyGroup,
    )
    collider_groups: CollectionProperty(  # type: ignore[valid-type]
        name="Collider Groups",
        type=Vrm0SecondaryAnimationColliderGroupPropertyGroup,
    )

    # for UI
    show_expanded_bone_groups: BoolProperty(  # type: ignore[valid-type]
        name="Spring Bone Groups",
    )
    show_expanded_collider_groups: BoolProperty(  # type: ignore[valid-type]
        name="Collider Groups",
    )

    active_bone_group_index: IntProperty(min=0)  # type: ignore[valid-type]
    active_collider_group_index: IntProperty(min=0)  # type: ignore[valid-type]

    if TYPE_CHECKING:
        # This code is auto generated.
        # To regenerate, run the `uv run tools/property_typing.py` command.
        bone_groups: CollectionPropertyProtocol[  # type: ignore[no-redef]
            Vrm0SecondaryAnimationGroupPropertyGroup
        ]
        collider_groups: CollectionPropertyProtocol[  # type: ignore[no-redef]
            Vrm0SecondaryAnimationColliderGroupPropertyGroup
        ]
        show_expanded_bone_groups: bool  # type: ignore[no-redef]
        show_expanded_collider_groups: bool  # type: ignore[no-redef]
        active_bone_group_index: int  # type: ignore[no-redef]
        active_collider_group_index: int  # type: ignore[no-redef]


# https://github.com/vrm-c/UniVRM/blob/v0.91.1/Assets/VRM/Runtime/Format/glTF_VRM_extensions.cs#L8-L48
class Vrm0PropertyGroup(PropertyGroup):
    meta: PointerProperty(  # type: ignore[valid-type]
        name="VRM Meta",
        type=Vrm0MetaPropertyGroup,
    )
    humanoid: PointerProperty(  # type: ignore[valid-type]
        name="VRM Humanoid",
        type=Vrm0HumanoidPropertyGroup,
    )
    first_person: PointerProperty(  # type: ignore[valid-type]
        name="VRM First Person",
        type=Vrm0FirstPersonPropertyGroup,
    )
    blend_shape_master: PointerProperty(  # type: ignore[valid-type]
        name="VRM Blend Shape Master",
        type=Vrm0BlendShapeMasterPropertyGroup,
    )
    secondary_animation: PointerProperty(  # type: ignore[valid-type]
        name="VRM Secondary Animation",
        type=Vrm0SecondaryAnimationPropertyGroup,
    )
    if TYPE_CHECKING:
        # This code is auto generated.
        # To regenerate, run the `uv run tools/property_typing.py` command.
        meta: Vrm0MetaPropertyGroup  # type: ignore[no-redef]
        humanoid: Vrm0HumanoidPropertyGroup  # type: ignore[no-redef]
        first_person: Vrm0FirstPersonPropertyGroup  # type: ignore[no-redef]
        blend_shape_master: Vrm0BlendShapeMasterPropertyGroup  # type: ignore[no-redef]
        secondary_animation: (  # type: ignore[no-redef]
            Vrm0SecondaryAnimationPropertyGroup
        )
