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

import bpy
from bpy.app.translations import pgettext
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 Matrix, Quaternion, Vector

from ...common import convert
from ...common.char import DISABLE_TRANSLATION
from ...common.logger import get_logger
from ...common.rotation import set_rotation_without_mode_change
from ...common.vrm1.human_bone import (
    HumanBoneName,
    HumanBoneSpecification,
    HumanBoneSpecifications,
)
from ..property_group import (
    BonePropertyGroup,
    MaterialPropertyGroup,
    MeshObjectPropertyGroup,
    StringPropertyGroup,
    property_group_enum,
)

if TYPE_CHECKING:
    from ..property_group import CollectionPropertyProtocol


logger = get_logger(__name__)


# https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_vrm-1.0-beta/schema/VRMC_vrm.humanoid.humanBones.humanBone.schema.json
class Vrm1HumanBonePropertyGroup(PropertyGroup):
    node: PointerProperty(  # type: ignore[valid-type]
        type=BonePropertyGroup
    )

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

    def update_node_candidates(
        self,
        armature_data: Armature,
        target: HumanBoneSpecification,
        bpy_bone_name_to_human_bone_specification: dict[str, HumanBoneSpecification],
        error_bpy_bone_names: Sequence[str],
    ) -> bool:
        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

    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]
        node_candidates: CollectionPropertyProtocol[  # type: ignore[no-redef]
            StringPropertyGroup
        ]


# https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_vrm-1.0-beta/schema/VRMC_vrm.humanoid.humanBones.schema.json
class Vrm1HumanBonesPropertyGroup(PropertyGroup):
    hips: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    spine: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    chest: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    upper_chest: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    neck: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    head: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    left_eye: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    right_eye: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    jaw: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    left_upper_leg: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    left_lower_leg: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    left_foot: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    left_toes: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    right_upper_leg: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    right_lower_leg: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    right_foot: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    right_toes: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    left_shoulder: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    left_upper_arm: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    left_lower_arm: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    left_hand: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    right_shoulder: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    right_upper_arm: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    right_lower_arm: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    right_hand: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    left_thumb_metacarpal: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    left_thumb_proximal: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    left_thumb_distal: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    left_index_proximal: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    left_index_intermediate: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    left_index_distal: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    left_middle_proximal: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    left_middle_intermediate: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    left_middle_distal: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    left_ring_proximal: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    left_ring_intermediate: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    left_ring_distal: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    left_little_proximal: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    left_little_intermediate: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    left_little_distal: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    right_thumb_metacarpal: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    right_thumb_proximal: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    right_thumb_distal: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    right_index_proximal: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    right_index_intermediate: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    right_index_distal: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    right_middle_proximal: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    right_middle_intermediate: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    right_middle_distal: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    right_ring_proximal: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    right_ring_intermediate: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    right_ring_distal: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    right_little_proximal: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    right_little_intermediate: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )
    right_little_distal: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanBonePropertyGroup
    )

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

    allow_non_humanoid_rig: BoolProperty(  # type: ignore[valid-type]
        name="Allow Non-Humanoid Rig",
    )

    def human_bone_name_to_human_bone(
        self,
    ) -> dict[HumanBoneName, Vrm1HumanBonePropertyGroup]:
        return {
            HumanBoneName.HIPS: self.hips,
            HumanBoneName.SPINE: self.spine,
            HumanBoneName.CHEST: self.chest,
            HumanBoneName.UPPER_CHEST: self.upper_chest,
            HumanBoneName.NECK: self.neck,
            HumanBoneName.HEAD: self.head,
            HumanBoneName.LEFT_EYE: self.left_eye,
            HumanBoneName.RIGHT_EYE: self.right_eye,
            HumanBoneName.JAW: self.jaw,
            HumanBoneName.LEFT_UPPER_LEG: self.left_upper_leg,
            HumanBoneName.LEFT_LOWER_LEG: self.left_lower_leg,
            HumanBoneName.LEFT_FOOT: self.left_foot,
            HumanBoneName.LEFT_TOES: self.left_toes,
            HumanBoneName.RIGHT_UPPER_LEG: self.right_upper_leg,
            HumanBoneName.RIGHT_LOWER_LEG: self.right_lower_leg,
            HumanBoneName.RIGHT_FOOT: self.right_foot,
            HumanBoneName.RIGHT_TOES: self.right_toes,
            HumanBoneName.LEFT_SHOULDER: self.left_shoulder,
            HumanBoneName.LEFT_UPPER_ARM: self.left_upper_arm,
            HumanBoneName.LEFT_LOWER_ARM: self.left_lower_arm,
            HumanBoneName.LEFT_HAND: self.left_hand,
            HumanBoneName.RIGHT_SHOULDER: self.right_shoulder,
            HumanBoneName.RIGHT_UPPER_ARM: self.right_upper_arm,
            HumanBoneName.RIGHT_LOWER_ARM: self.right_lower_arm,
            HumanBoneName.RIGHT_HAND: self.right_hand,
            HumanBoneName.LEFT_THUMB_METACARPAL: self.left_thumb_metacarpal,
            HumanBoneName.LEFT_THUMB_PROXIMAL: self.left_thumb_proximal,
            HumanBoneName.LEFT_THUMB_DISTAL: self.left_thumb_distal,
            HumanBoneName.LEFT_INDEX_PROXIMAL: self.left_index_proximal,
            HumanBoneName.LEFT_INDEX_INTERMEDIATE: self.left_index_intermediate,
            HumanBoneName.LEFT_INDEX_DISTAL: self.left_index_distal,
            HumanBoneName.LEFT_MIDDLE_PROXIMAL: self.left_middle_proximal,
            HumanBoneName.LEFT_MIDDLE_INTERMEDIATE: self.left_middle_intermediate,
            HumanBoneName.LEFT_MIDDLE_DISTAL: self.left_middle_distal,
            HumanBoneName.LEFT_RING_PROXIMAL: self.left_ring_proximal,
            HumanBoneName.LEFT_RING_INTERMEDIATE: self.left_ring_intermediate,
            HumanBoneName.LEFT_RING_DISTAL: self.left_ring_distal,
            HumanBoneName.LEFT_LITTLE_PROXIMAL: self.left_little_proximal,
            HumanBoneName.LEFT_LITTLE_INTERMEDIATE: self.left_little_intermediate,
            HumanBoneName.LEFT_LITTLE_DISTAL: self.left_little_distal,
            HumanBoneName.RIGHT_THUMB_METACARPAL: self.right_thumb_metacarpal,
            HumanBoneName.RIGHT_THUMB_PROXIMAL: self.right_thumb_proximal,
            HumanBoneName.RIGHT_THUMB_DISTAL: self.right_thumb_distal,
            HumanBoneName.RIGHT_INDEX_PROXIMAL: self.right_index_proximal,
            HumanBoneName.RIGHT_INDEX_INTERMEDIATE: self.right_index_intermediate,
            HumanBoneName.RIGHT_INDEX_DISTAL: self.right_index_distal,
            HumanBoneName.RIGHT_MIDDLE_PROXIMAL: self.right_middle_proximal,
            HumanBoneName.RIGHT_MIDDLE_INTERMEDIATE: self.right_middle_intermediate,
            HumanBoneName.RIGHT_MIDDLE_DISTAL: self.right_middle_distal,
            HumanBoneName.RIGHT_RING_PROXIMAL: self.right_ring_proximal,
            HumanBoneName.RIGHT_RING_INTERMEDIATE: self.right_ring_intermediate,
            HumanBoneName.RIGHT_RING_DISTAL: self.right_ring_distal,
            HumanBoneName.RIGHT_LITTLE_PROXIMAL: self.right_little_proximal,
            HumanBoneName.RIGHT_LITTLE_INTERMEDIATE: self.right_little_intermediate,
            HumanBoneName.RIGHT_LITTLE_DISTAL: self.right_little_distal,
        }

    def error_messages(self) -> list[str]:
        messages: list[str] = []
        human_bone_name_to_human_bone = self.human_bone_name_to_human_bone()

        for name, human_bone in human_bone_name_to_human_bone.items():
            specification = HumanBoneSpecifications.get(name)
            if not human_bone.node.bone_name:
                if specification.requirement:
                    messages.append(
                        pgettext(
                            'Please assign Required VRM Human Bone "{name}".'
                        ).format(name=specification.title)
                    )
                continue
            if not specification.parent_requirement:
                continue
            if not specification.parent_name:
                logger.error("No parent for '%s' in spec", name)
                continue
            parent = human_bone_name_to_human_bone.get(specification.parent_name)
            if not parent:
                logger.error("No parent for '%s' in dict", name)
                continue
            parent_specification = specification.parent()
            if not parent_specification:
                logger.error("No parent specification for '%s'", name)
                continue
            if not parent.node.bone_name:
                messages.append(
                    pgettext(
                        'Please assign "{parent_name}"'
                        + ' because "{name}" requires it as its child bone.'
                    ).format(
                        name=specification.title,
                        parent_name=parent_specification.title,
                    )
                )

        return messages

    def all_required_bones_are_assigned(self) -> bool:
        return len(self.error_messages()) == 0

    @staticmethod
    def fixup_human_bones(obj: Object) -> None:
        armature_data = obj.data
        if obj.type != "ARMATURE" or not isinstance(armature_data, Armature):
            return

        human_bones = get_armature_vrm1_extension(armature_data).humanoid.human_bones

        # If the same Blender bone is set in multiple bone maps, delete one of them
        fixup = True
        while fixup:
            fixup = False
            found_node_bone_names: list[str] = []
            for human_bone in human_bones.human_bone_name_to_human_bone().values():
                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 = ""
                fixup = True
                break

    @staticmethod
    def defer_update_all_node_candidates(
        armature_data_name: str,
        *,
        force: bool = False,
    ) -> None:
        bpy.app.timers.register(
            functools.partial(
                Vrm1HumanBonesPropertyGroup.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 to match the type of update_all_node_candidates() to timers.register."""
        # Context cannot be passed across frames, so get a new one
        context = bpy.context
        Vrm1HumanBonesPropertyGroup.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:
        if context is None:
            context = bpy.context
        armature_data = context.blend_data.armatures.get(armature_data_name)
        if not isinstance(armature_data, Armature):
            return
        human_bones = get_armature_vrm1_extension(armature_data).humanoid.human_bones

        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 human_bones.last_bone_names_str == bone_names_str:
            return
        human_bones.last_bone_names_str = bone_names_str

        BonePropertyGroup.update_all_vrm1_node_candidates(armature_data)

    if TYPE_CHECKING:
        # This code is auto generated.
        # To regenerate, run the `uv run tools/property_typing.py` command.
        hips: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        spine: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        chest: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        upper_chest: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        neck: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        head: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        left_eye: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        right_eye: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        jaw: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        left_upper_leg: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        left_lower_leg: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        left_foot: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        left_toes: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        right_upper_leg: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        right_lower_leg: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        right_foot: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        right_toes: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        left_shoulder: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        left_upper_arm: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        left_lower_arm: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        left_hand: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        right_shoulder: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        right_upper_arm: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        right_lower_arm: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        right_hand: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        left_thumb_metacarpal: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        left_thumb_proximal: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        left_thumb_distal: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        left_index_proximal: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        left_index_intermediate: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        left_index_distal: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        left_middle_proximal: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        left_middle_intermediate: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        left_middle_distal: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        left_ring_proximal: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        left_ring_intermediate: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        left_ring_distal: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        left_little_proximal: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        left_little_intermediate: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        left_little_distal: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        right_thumb_metacarpal: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        right_thumb_proximal: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        right_thumb_distal: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        right_index_proximal: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        right_index_intermediate: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        right_index_distal: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        right_middle_proximal: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        right_middle_intermediate: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        right_middle_distal: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        right_ring_proximal: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        right_ring_intermediate: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        right_ring_distal: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        right_little_proximal: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        right_little_intermediate: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        right_little_distal: Vrm1HumanBonePropertyGroup  # type: ignore[no-redef]
        last_bone_names_str: str  # type: ignore[no-redef]
        initial_automatic_bone_assignment: bool  # type: ignore[no-redef]
        allow_non_humanoid_rig: bool  # type: ignore[no-redef]


# https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_vrm-1.0-beta/schema/VRMC_vrm.humanoid.schema.json
class Vrm1HumanoidPropertyGroup(PropertyGroup):
    human_bones: PointerProperty(type=Vrm1HumanBonesPropertyGroup)  # type: ignore[valid-type]

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

    (
        pose_enum,
        (
            POSE_AUTO_POSE,
            POSE_REST_POSITION_POSE,
            POSE_CURRENT_POSE,
            POSE_CUSTOM_POSE,
        ),
    ) = property_group_enum(
        (
            "autoPose",
            "Auto",
            "Auto Pose",
            "ARMATURE_DATA",
            3,
        ),
        (
            "restPositionPose",
            "Rest Position",
            "Rest Position Pose",
            "ARMATURE_DATA",
            0,
        ),
        (
            "currentPose",
            "Current Pose",
            "Current Pose",
            "ARMATURE_DATA",
            1,
        ),
        (
            "customPose",
            "Custom Pose",
            "Custom Pose",
            "ARMATURE_DATA",
            2,
        ),
    )

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

    pose_library: PointerProperty(  # type: ignore[valid-type]
        type=Action,
        update=update_pose_library,
    )
    pose_marker_name: StringProperty()  # type: ignore[valid-type]

    if TYPE_CHECKING:
        # This code is auto generated.
        # To regenerate, run the `uv run tools/property_typing.py` command.
        human_bones: Vrm1HumanBonesPropertyGroup  # 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]


# https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_vrm-1.0-beta/schema/VRMC_vrm.lookAt.rangeMap.schema.json
class Vrm1LookAtRangeMapPropertyGroup(PropertyGroup):
    input_max_value: FloatProperty(  # type: ignore[valid-type]
        name="Input Max Value",
        min=0.0001,  # https://github.com/pixiv/three-vrm/issues/1197#issuecomment-1498492002
        default=90.0,
        max=180.0,
    )
    output_scale: FloatProperty(  # type: ignore[valid-type]
        name="Output Scale",
        default=10.0,
    )

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


# https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_vrm-1.0-beta/schema/VRMC_vrm.lookAt.schema.json
class Vrm1LookAtPropertyGroup(PropertyGroup):
    offset_from_head_bone: FloatVectorProperty(  # type: ignore[valid-type]
        name="Offset From Head Bone",
        size=3,
        subtype="TRANSLATION",
        unit="LENGTH",
        default=(0, 0, 0),
    )

    (
        type_enum,
        (
            TYPE_BONE,
            TYPE_EXPRESSION,
        ),
    ) = property_group_enum(
        ("bone", "Bone", "Bone", "BONE_DATA", 0),
        (
            "expression",
            "Expression" + DISABLE_TRANSLATION,
            "Expression",
            "SHAPEKEY_DATA",
            1,
        ),
    )

    type: EnumProperty(  # type: ignore[valid-type]
        name="Type",
        items=type_enum.items(),
    )

    range_map_horizontal_inner: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1LookAtRangeMapPropertyGroup,
    )
    range_map_horizontal_outer: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1LookAtRangeMapPropertyGroup,
    )
    range_map_vertical_down: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1LookAtRangeMapPropertyGroup,
    )
    range_map_vertical_up: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1LookAtRangeMapPropertyGroup,
    )

    enable_preview: BoolProperty(  # type: ignore[valid-type]
        name="Enable Preview",
    )
    preview_target_bpy_object: PointerProperty(  # type: ignore[valid-type]
        type=Object,
        name="Preview Target",
    )
    previous_preview_target_bpy_object_location: FloatVectorProperty(  # type: ignore[valid-type]
        size=3,
    )

    @staticmethod
    def update_all_previews(context: Context) -> None:
        for armature_object in context.blend_data.objects:
            if armature_object.type != "ARMATURE":
                continue
            armature_data = armature_object.data
            if not isinstance(armature_data, Armature):
                continue
            look_at = get_armature_vrm1_extension(armature_data).look_at
            look_at.update_preview(context, armature_object, armature_data)

    def update_preview(
        self,
        _context: Context,
        armature_object: Object,
        armature_data: Armature,
    ) -> None:
        vrm1 = get_armature_vrm1_extension(armature_data)
        preview_target_bpy_object = self.preview_target_bpy_object
        if not preview_target_bpy_object:
            return
        to_translation = preview_target_bpy_object.matrix_world.to_translation()
        if (
            Vector(self.previous_preview_target_bpy_object_location) - to_translation
        ).length_squared < float_info.epsilon:
            return
        self.previous_preview_target_bpy_object_location = to_translation
        head_bone_name = vrm1.humanoid.human_bones.head.node.bone_name
        head_pose_bone = armature_object.pose.bones.get(head_bone_name)
        if not head_pose_bone:
            return

        # TODO: Honor t-pose action
        rest_head_bone_matrix = (
            armature_object.matrix_world
            @ head_pose_bone.bone.convert_local_to_pose(
                Matrix(),
                head_pose_bone.bone.matrix_local,
            )
        )
        rest_head_bone_inverted_rotation = (
            rest_head_bone_matrix.to_quaternion().inverted()
        )

        head_parent_pose_bone = head_pose_bone.parent
        if not head_parent_pose_bone:
            return

        head_bone_without_rotation_matrix = (
            armature_object.matrix_world
            @ head_pose_bone.bone.convert_local_to_pose(
                Matrix(),
                head_pose_bone.bone.matrix_local,
                parent_matrix=head_parent_pose_bone.matrix,
                parent_matrix_local=head_parent_pose_bone.bone.matrix_local,
            )
        )

        local_target_translation = (
            head_bone_without_rotation_matrix
        ).inverted_safe() @ to_translation - Vector(self.offset_from_head_bone)
        forward_vector = rest_head_bone_inverted_rotation @ Vector((0, -1, 0))
        right_vector = rest_head_bone_inverted_rotation @ Vector((-1, 0, 0))
        up_vector = rest_head_bone_inverted_rotation @ Vector((0, 0, 1))

        # logger.warning(f"local_target_translation={dump(local_target_translation)}")
        # logger.warning(f"forward={dump(forward_vector)}")
        # logger.warning(f"right={dump(right_vector)}")
        # logger.warning(f"up={dump(up_vector)}")

        forward_length = local_target_translation.dot(forward_vector)
        right_length = local_target_translation.dot(right_vector)
        up_length = local_target_translation.dot(up_vector)

        # https://github.com/vrm-c/vrm-specification/blob/0861a66eb2f2b76835322d775678047d616536b3/specification/VRMC_vrm-1.0/lookAt.md?plain=1#L180
        if abs(forward_length) < float_info.epsilon:
            return

        yaw_degrees = math.degrees(math.atan2(right_length, forward_length))
        pitch_degrees = math.degrees(
            math.atan2(
                up_length,
                math.sqrt(math.pow(right_length, 2) + math.pow(forward_length, 2)),
            )
        )
        if vrm1.look_at.type == vrm1.look_at.TYPE_BONE.identifier:
            self.apply_eye_bone_preview(
                vrm1,
                yaw_degrees,
                pitch_degrees,
                armature_object,
                HumanBoneName.RIGHT_EYE,
            )
            self.apply_eye_bone_preview(
                vrm1,
                yaw_degrees,
                pitch_degrees,
                armature_object,
                HumanBoneName.LEFT_EYE,
            )
        elif vrm1.look_at.type == vrm1.look_at.TYPE_EXPRESSION.identifier:
            self.apply_expression_preview(vrm1, yaw_degrees, pitch_degrees)

    # https://github.com/vrm-c/vrm-specification/blob/0861a66eb2f2b76835322d775678047d616536b3/specification/VRMC_vrm-1.0/lookAt.md?plain=1#L230
    def apply_eye_bone_preview(
        self,
        vrm1: "Vrm1PropertyGroup",
        yaw_degrees: float,
        pitch_degrees: float,
        armature_object: Object,
        human_bone_name: HumanBoneName,
    ) -> None:
        if human_bone_name == HumanBoneName.RIGHT_EYE:
            range_map_horizontal_right = vrm1.look_at.range_map_horizontal_outer
            range_map_horizontal_left = vrm1.look_at.range_map_horizontal_inner
            pose_bone = armature_object.pose.bones.get(
                vrm1.humanoid.human_bones.right_eye.node.bone_name
            )
        elif human_bone_name == HumanBoneName.LEFT_EYE:
            range_map_horizontal_right = vrm1.look_at.range_map_horizontal_inner
            range_map_horizontal_left = vrm1.look_at.range_map_horizontal_outer
            pose_bone = armature_object.pose.bones.get(
                vrm1.humanoid.human_bones.left_eye.node.bone_name
            )
        else:
            return

        range_map_vertical_up = vrm1.look_at.range_map_vertical_up
        range_map_vertical_down = vrm1.look_at.range_map_vertical_down

        if not pose_bone:
            return

        parent_pose_bone = pose_bone.parent
        if not parent_pose_bone:
            return

        if yaw_degrees < 0:
            yaw_degrees = (
                -min(abs(yaw_degrees), range_map_horizontal_left.input_max_value)
                / range_map_horizontal_left.input_max_value
                * range_map_horizontal_left.output_scale
            )
        else:
            yaw_degrees = (
                min(abs(yaw_degrees), range_map_horizontal_right.input_max_value)
                / range_map_horizontal_right.input_max_value
                * range_map_horizontal_right.output_scale
            )

        if pitch_degrees < 0:
            # down
            pitch_degrees = (
                -min(abs(pitch_degrees), range_map_vertical_down.input_max_value)
                / range_map_vertical_down.input_max_value
                * range_map_vertical_down.output_scale
            )
        else:
            # up
            pitch_degrees = (
                min(abs(pitch_degrees), range_map_vertical_up.input_max_value)
                / range_map_vertical_up.input_max_value
                * range_map_vertical_up.output_scale
            )

        # TODO: Honor t-pose action
        rest_bone_matrix = (
            armature_object.matrix_world
            @ pose_bone.bone.convert_local_to_pose(
                Matrix(),
                pose_bone.bone.matrix_local,
            )
        )
        reset_bone_inverted_rotation = rest_bone_matrix.to_quaternion().inverted()

        forward_vector = reset_bone_inverted_rotation @ Vector((0, -1, 0))
        right_vector = reset_bone_inverted_rotation @ Vector((-1, 0, 0))
        up_vector = reset_bone_inverted_rotation @ Vector((0, 0, 1))

        # logger.warning(f"{yaw_degrees=} {pitch_degrees=}")
        # logger.warning(f"forward={dump(forward_vector)}")
        # logger.warning(f"right={dump(right_vector)}")
        # logger.warning(f"up={dump(up_vector)}")

        rotation = Quaternion(
            forward_vector.cross(right_vector), math.radians(yaw_degrees)
        ) @ Quaternion(forward_vector.cross(up_vector), math.radians(pitch_degrees))
        # logger.warning(f"rotation={dump(rotation)}")

        set_rotation_without_mode_change(pose_bone, rotation)

    # https://github.com/vrm-c/vrm-specification/blob/0861a66eb2f2b76835322d775678047d616536b3/specification/VRMC_vrm-1.0/lookAt.md?plain=1#L258
    def apply_expression_preview(
        self,
        vrm1: "Vrm1PropertyGroup",
        yaw_degrees: float,
        pitch_degrees: float,
    ) -> None:
        range_map_horizontal_outer = vrm1.look_at.range_map_horizontal_outer
        range_map_vertical_up = vrm1.look_at.range_map_vertical_up
        range_map_vertical_down = vrm1.look_at.range_map_vertical_down
        look_left = vrm1.expressions.preset.look_left
        look_right = vrm1.expressions.preset.look_right
        look_up = vrm1.expressions.preset.look_up
        look_down = vrm1.expressions.preset.look_down
        # horizontal
        if yaw_degrees < 0:
            # left
            yaw_weight = (
                min(abs(yaw_degrees), range_map_horizontal_outer.input_max_value)
                / range_map_horizontal_outer.input_max_value
                * range_map_horizontal_outer.output_scale
            )
            look_left.preview = yaw_weight
            look_right.preview = 0
        else:
            # right
            yaw_weight = (
                min(yaw_degrees, range_map_horizontal_outer.input_max_value)
                / range_map_horizontal_outer.input_max_value
                * range_map_horizontal_outer.output_scale
            )
            look_left.preview = 0
            look_right.preview = yaw_weight

        # vertical
        if pitch_degrees < 0:
            # down
            pitch_weight = (
                min(abs(pitch_degrees), range_map_vertical_down.input_max_value)
                / range_map_vertical_down.input_max_value
                * range_map_vertical_down.output_scale
            )
            look_down.preview = pitch_weight
            look_up.preview = 0
        else:
            # up
            pitch_weight = (
                min(pitch_degrees, range_map_vertical_up.input_max_value)
                / range_map_vertical_up.input_max_value
                * range_map_vertical_up.output_scale
            )
            look_down.preview = 0
            look_up.preview = pitch_weight

    if TYPE_CHECKING:
        # This code is auto generated.
        # To regenerate, run the `uv run tools/property_typing.py` command.
        offset_from_head_bone: Sequence[float]  # type: ignore[no-redef]
        type: str  # type: ignore[no-redef]
        range_map_horizontal_inner: (  # type: ignore[no-redef]
            Vrm1LookAtRangeMapPropertyGroup
        )
        range_map_horizontal_outer: (  # type: ignore[no-redef]
            Vrm1LookAtRangeMapPropertyGroup
        )
        range_map_vertical_down: (  # type: ignore[no-redef]
            Vrm1LookAtRangeMapPropertyGroup
        )
        range_map_vertical_up: Vrm1LookAtRangeMapPropertyGroup  # type: ignore[no-redef]
        enable_preview: bool  # type: ignore[no-redef]
        preview_target_bpy_object: Optional[Object]  # type: ignore[no-redef]
        previous_preview_target_bpy_object_location: (  # type: ignore[no-redef]
            Sequence[float]
        )


# https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_vrm-1.0-beta/schema/VRMC_vrm.firstPerson.meshAnnotation.schema.json
class Vrm1MeshAnnotationPropertyGroup(PropertyGroup):
    node: PointerProperty(  # type: ignore[valid-type]
        type=MeshObjectPropertyGroup
    )
    type_enum, __types = property_group_enum(
        ("auto", "Auto", "", "NONE", 0),
        ("both", "Both", "", "NONE", 1),
        ("thirdPersonOnly", "Third-Person Only", "", "NONE", 2),
        ("firstPersonOnly", "First-Person Only", "", "NONE", 3),
    )
    type: EnumProperty(  # type: ignore[valid-type]
        items=type_enum.items(),
        name="First Person Type",
    )

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


# https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_vrm-1.0-beta/schema/VRMC_vrm.firstPerson.schema.json
class Vrm1FirstPersonPropertyGroup(PropertyGroup):
    mesh_annotations: CollectionProperty(  # type: ignore[valid-type]
        name="Mesh Annotations",
        type=Vrm1MeshAnnotationPropertyGroup,
    )

    # 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.
        mesh_annotations: CollectionPropertyProtocol[  # type: ignore[no-redef]
            Vrm1MeshAnnotationPropertyGroup
        ]
        active_mesh_annotation_index: int  # type: ignore[no-redef]


# https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_vrm-1.0-beta/schema/VRMC_vrm.expressions.expression.morphTargetBind.schema.json
class Vrm1MorphTargetBindPropertyGroup(PropertyGroup):
    node: PointerProperty(  # type: ignore[valid-type]
        type=MeshObjectPropertyGroup
    )
    index: StringProperty(  # type: ignore[valid-type]
    )
    weight: FloatProperty(  # type: ignore[valid-type]
        min=0,
        default=1,
        max=1,
    )

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


# https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_vrm-1.0-beta/schema/VRMC_vrm.expressions.expression.materialColorBind.schema.json
class Vrm1MaterialColorBindPropertyGroup(PropertyGroup):
    material: PointerProperty(  # type: ignore[valid-type]
        name="Material",
        type=Material,
    )

    type_enum, __types = property_group_enum(
        ("color", "Color", "", "NONE", 0),
        ("emissionColor", "Emission Color", "", "NONE", 1),
        ("shadeColor", "Shade Color", "", "NONE", 2),
        ("matcapColor", "Matcap Color", "", "NONE", 5),
        ("rimColor", "Rim Color", "", "NONE", 3),
        ("outlineColor", "Outline Color", "", "NONE", 4),
    )
    type: EnumProperty(  # type: ignore[valid-type]
        name="Type",
        items=type_enum.items(),
    )
    target_value: FloatVectorProperty(  # type: ignore[valid-type]
        name="Target Value",
        size=4,
        subtype="COLOR",
        min=0,
        max=1,  # TODO: hdr emission color?
    )

    def get_target_value_as_rgb(self) -> tuple[float, float, float]:
        return (
            self.target_value[0],
            self.target_value[1],
            self.target_value[2],
        )

    def set_target_value_as_rgb(self, value: Sequence[float]) -> None:
        if len(value) < 3:
            return
        self.target_value = (
            value[0],
            value[1],
            value[2],
            self.target_value[3],
        )

    target_value_as_rgb: FloatVectorProperty(  # type: ignore[valid-type]
        name="Target Value",
        size=3,
        subtype="COLOR",
        get=get_target_value_as_rgb,
        set=set_target_value_as_rgb,
    )

    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]
        type: str  # type: ignore[no-redef]
        target_value: Sequence[float]  # type: ignore[no-redef]
        target_value_as_rgb: Sequence[float]  # type: ignore[no-redef]


# https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_vrm-1.0-beta/schema/VRMC_vrm.expressions.expression.textureTransformBind.schema.json
class Vrm1TextureTransformBindPropertyGroup(PropertyGroup):
    material: PointerProperty(  # type: ignore[valid-type]
        name="Material",
        type=Material,
    )
    scale: FloatVectorProperty(  # type: ignore[valid-type]
        size=2,
        default=(1, 1),
    )
    offset: FloatVectorProperty(  # type: ignore[valid-type]
        size=2,
        default=(0, 0),
    )
    show_experimental_preview_feature: BoolProperty(  # type: ignore[valid-type]
        name="[Experimental] Show Preview Feature"
    )

    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]
        scale: Sequence[float]  # type: ignore[no-redef]
        offset: Sequence[float]  # type: ignore[no-redef]
        show_experimental_preview_feature: bool  # type: ignore[no-redef]


# https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_vrm-1.0-beta/schema/VRMC_vrm.expressions.expression.schema.json
class Vrm1ExpressionPropertyGroup(PropertyGroup):
    morph_target_binds: CollectionProperty(  # type: ignore[valid-type]
        type=Vrm1MorphTargetBindPropertyGroup
    )
    material_color_binds: CollectionProperty(  # type: ignore[valid-type]
        type=Vrm1MaterialColorBindPropertyGroup
    )
    texture_transform_binds: CollectionProperty(  # type: ignore[valid-type]
        type=Vrm1TextureTransformBindPropertyGroup
    )
    is_binary: BoolProperty(  # type: ignore[valid-type]
        name="Is Binary"
    )
    materials_to_update: CollectionProperty(  # type: ignore[valid-type]
        type=MaterialPropertyGroup, options={"HIDDEN"}
    )

    expression_override_type_enum, __expression_override_types = property_group_enum(
        ("none", "None", "", "NONE", 0),
        ("block", "Block", "", "NONE", 1),
        ("blend", "Blend", "", "NONE", 2),
    )

    def update_preview(self, context: Context) -> None:
        self.update_previews(self, context)

    @staticmethod
    def update_previews(
        triggered_expression: "Vrm1ExpressionPropertyGroup",
        context: Context,
    ) -> None:
        if not triggered_expression.name:
            logger.debug("Unnamed expression: %s", type(triggered_expression))
            return

        armature = triggered_expression.id_data
        if not isinstance(armature, Armature):
            # This is getting triggered after importing VRMA files
            logger.error("No armature for %s", triggered_expression.name)
            return

        expressions = get_armature_vrm1_extension(armature).expressions
        name_to_expression_dict = expressions.all_name_to_expression_dict()

        mouth_block_rate = 0.0
        blink_block_rate = 0.0
        look_at_block_rate = 0.0
        for expression in name_to_expression_dict.values():
            if expression.preview < float_info.epsilon:
                continue

            if expression.override_mouth == "blend":
                mouth_block_rate += expression.preview
            if expression.override_blink == "blend":
                blink_block_rate += expression.preview
            if expression.override_look_at == "blend":
                look_at_block_rate += expression.preview

            if expression.override_mouth == "block":
                mouth_block_rate = 1.0
            if expression.override_blink == "block":
                blink_block_rate = 1.0
            if expression.override_look_at == "block":
                look_at_block_rate = 1.0

        mouth_blend_factor = max(0, min(1 - mouth_block_rate, 1))
        blink_blend_factor = max(0, min(1 - blink_block_rate, 1))
        look_at_blend_factor = max(0, min(1 - look_at_block_rate, 1))

        shape_key_name_and_key_block_name_to_value: dict[tuple[str, str], float] = {}
        for name, expression in name_to_expression_dict.items():
            if expression.is_binary:
                # https://github.com/vrm-c/vrm-specification/blob/5b1071b9da1d60bb1bab0b70754615127fc43e9e/specification/VRMC_vrm-1.0/expressions.md?plain=1#L46
                preview = 1.0 if expression.preview > 0.5 else 0.0
            else:
                preview = expression.preview

            if expressions.preset.is_mouth_expression(name):
                preview *= mouth_blend_factor
            elif expressions.preset.is_blink_expression(name):
                preview *= blink_blend_factor
            elif expressions.preset.is_look_at_expression(name):
                preview *= look_at_blend_factor

            for texture_transform_bind in expression.texture_transform_binds:
                material = texture_transform_bind.material
                if not material:
                    continue

            for material_color_bind in expression.material_color_binds:
                material = material_color_bind.material
                if not material:
                    continue

            for morph_target_bind in expression.morph_target_binds:
                mesh_object = context.blend_data.objects.get(
                    morph_target_bind.node.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
                key_block = key_blocks.get(morph_target_bind.index)
                if not key_block:
                    continue

                value = morph_target_bind.weight * preview
                shape_key_name_and_key_block_name = (shape_key.name, key_block.name)
                shape_key_name_and_key_block_name_to_value[
                    shape_key_name_and_key_block_name
                ] = (
                    shape_key_name_and_key_block_name_to_value.get(
                        shape_key_name_and_key_block_name, 0.0
                    )
                    + value
                )

        for (
            shape_key_name,
            key_block_name,
        ), value in shape_key_name_and_key_block_name_to_value.items():
            shape_key = context.blend_data.shape_keys.get(shape_key_name)
            if not shape_key:
                continue
            key_blocks = shape_key.key_blocks
            if not key_blocks:
                continue
            key_block = key_blocks.get(key_block_name)
            if not key_block:
                continue
            key_block.value = value

        Vrm1ExpressionPropertyGroup.frame_change_post_shape_key_updates.update(
            shape_key_name_and_key_block_name_to_value
        )

    override_blink: EnumProperty(  # type: ignore[valid-type]
        name="Override Blink",
        items=expression_override_type_enum.items(),
        update=update_preview,
    )
    override_look_at: EnumProperty(  # type: ignore[valid-type]
        name="Override Look At",
        items=expression_override_type_enum.items(),
        update=update_preview,
    )
    override_mouth: EnumProperty(  # type: ignore[valid-type]
        name="Override Mouth",
        items=expression_override_type_enum.items(),
        update=update_preview,
    )

    # for UI
    show_expanded: BoolProperty()  # type: ignore[valid-type]
    show_expanded_morph_target_binds: BoolProperty(  # type: ignore[valid-type]
        name="Morph Target Binds"
    )
    show_expanded_material_color_binds: BoolProperty(  # type: ignore[valid-type]
        name="Material Color Binds"
    )
    show_expanded_texture_transform_binds: BoolProperty(  # type: ignore[valid-type]
        name="Texture Transform Binds"
    )

    # Since the value of the shape key can only be changed in
    # frame_change_pre/frame_change_post during animation playback, the
    # changed value is saved 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
        Vrm1ExpressionPropertyGroup.update_preview(self, context)

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

    @classmethod
    def update_materials(cls, context: Context) -> None:
        for armature in context.blend_data.armatures:
            ext = get_armature_vrm1_extension(armature)
            expressions = ext.expressions
            for expression in expressions.all_name_to_expression_dict().values():
                materials_to_update = expression.materials_to_update
                if not materials_to_update:
                    continue
                for material_property_group in materials_to_update:
                    material = material_property_group.material
                    if not material:
                        continue
                    node_tree = material.node_tree
                    if not node_tree:
                        continue

                    # The update() method exists in the documentation, but
                    # the call fails
                    # https://docs.blender.org/api/4.2/bpy.types.NodeTree.html#bpy.types.NodeTree.update
                    # node_tree.update()
                materials_to_update.clear()

    active_morph_target_bind_index: IntProperty(min=0)  # type: ignore[valid-type]
    active_material_color_bind_index: IntProperty(min=0)  # type: ignore[valid-type]
    active_texture_transform_bind_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.
        morph_target_binds: CollectionPropertyProtocol[  # type: ignore[no-redef]
            Vrm1MorphTargetBindPropertyGroup
        ]
        material_color_binds: CollectionPropertyProtocol[  # type: ignore[no-redef]
            Vrm1MaterialColorBindPropertyGroup
        ]
        texture_transform_binds: CollectionPropertyProtocol[  # type: ignore[no-redef]
            Vrm1TextureTransformBindPropertyGroup
        ]
        is_binary: bool  # type: ignore[no-redef]
        materials_to_update: CollectionPropertyProtocol[  # type: ignore[no-redef]
            MaterialPropertyGroup
        ]
        override_blink: str  # type: ignore[no-redef]
        override_look_at: str  # type: ignore[no-redef]
        override_mouth: str  # type: ignore[no-redef]
        show_expanded: bool  # type: ignore[no-redef]
        show_expanded_morph_target_binds: bool  # type: ignore[no-redef]
        show_expanded_material_color_binds: bool  # type: ignore[no-redef]
        show_expanded_texture_transform_binds: bool  # type: ignore[no-redef]
        preview: float  # type: ignore[no-redef]
        active_morph_target_bind_index: int  # type: ignore[no-redef]
        active_material_color_bind_index: int  # type: ignore[no-redef]
        active_texture_transform_bind_index: int  # type: ignore[no-redef]


class Vrm1CustomExpressionPropertyGroup(Vrm1ExpressionPropertyGroup):
    def get_custom_name(self) -> str:
        return str(self.get("custom_name", ""))

    def set_custom_name(self, value: str) -> None:
        if not value or self.get("custom_name") == value:
            return

        armature_data = self.id_data
        if not isinstance(armature_data, Armature):
            logger.error("No armature for %s", self)
            return

        expressions = get_armature_vrm1_extension(armature_data).expressions
        all_expression_names = expressions.all_name_to_expression_dict().keys()
        custom_name = value
        for index in range(sys.maxsize):
            if index > 0:
                custom_name = f"{value}.{index:03}"
            if custom_name not in all_expression_names:
                break

        self["custom_name"] = custom_name
        self.name = custom_name

    custom_name: StringProperty(  # type: ignore[valid-type]
        name="Name",
        get=get_custom_name,
        set=set_custom_name,
    )

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


@dataclass(frozen=True)
class ExpressionPreset:
    name: str
    icon: str
    mouth: bool
    blink: bool
    look_at: bool


# https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_vrm-1.0-beta/schema/VRMC_vrm.expressions.schema.json
class Vrm1ExpressionsPresetPropertyGroup(PropertyGroup):
    happy: PointerProperty(type=Vrm1ExpressionPropertyGroup)  # type: ignore[valid-type]
    angry: PointerProperty(type=Vrm1ExpressionPropertyGroup)  # type: ignore[valid-type]
    sad: PointerProperty(type=Vrm1ExpressionPropertyGroup)  # type: ignore[valid-type]
    relaxed: PointerProperty(type=Vrm1ExpressionPropertyGroup)  # type: ignore[valid-type]
    surprised: PointerProperty(type=Vrm1ExpressionPropertyGroup)  # type: ignore[valid-type]
    neutral: PointerProperty(type=Vrm1ExpressionPropertyGroup)  # type: ignore[valid-type]
    aa: PointerProperty(type=Vrm1ExpressionPropertyGroup)  # type: ignore[valid-type]
    ih: PointerProperty(type=Vrm1ExpressionPropertyGroup)  # type: ignore[valid-type]
    ou: PointerProperty(type=Vrm1ExpressionPropertyGroup)  # type: ignore[valid-type]
    ee: PointerProperty(type=Vrm1ExpressionPropertyGroup)  # type: ignore[valid-type]
    oh: PointerProperty(type=Vrm1ExpressionPropertyGroup)  # type: ignore[valid-type]
    blink: PointerProperty(type=Vrm1ExpressionPropertyGroup)  # type: ignore[valid-type]
    blink_left: PointerProperty(type=Vrm1ExpressionPropertyGroup)  # type: ignore[valid-type]
    blink_right: PointerProperty(type=Vrm1ExpressionPropertyGroup)  # type: ignore[valid-type]
    look_up: PointerProperty(type=Vrm1ExpressionPropertyGroup)  # type: ignore[valid-type]
    look_down: PointerProperty(type=Vrm1ExpressionPropertyGroup)  # type: ignore[valid-type]
    look_left: PointerProperty(type=Vrm1ExpressionPropertyGroup)  # type: ignore[valid-type]
    look_right: PointerProperty(type=Vrm1ExpressionPropertyGroup)  # type: ignore[valid-type]

    def expression_preset_and_expressions(
        self,
    ) -> tuple[tuple[ExpressionPreset, Vrm1ExpressionPropertyGroup], ...]:
        return (
            (
                ExpressionPreset(
                    "happy", "HEART", mouth=False, blink=False, look_at=False
                ),
                self.happy,
            ),
            (
                ExpressionPreset(
                    "angry", "ORPHAN_DATA", mouth=False, blink=False, look_at=False
                ),
                self.angry,
            ),
            (
                ExpressionPreset(
                    "sad", "MOD_FLUIDSIM", mouth=False, blink=False, look_at=False
                ),
                self.sad,
            ),
            (
                ExpressionPreset(
                    "relaxed", "LIGHT_SUN", mouth=False, blink=False, look_at=False
                ),
                self.relaxed,
            ),
            (
                ExpressionPreset(
                    "surprised", "LIGHT_SUN", mouth=False, blink=False, look_at=False
                ),
                self.surprised,
            ),
            (
                ExpressionPreset(
                    "neutral", "VIEW_ORTHO", mouth=False, blink=False, look_at=False
                ),
                self.neutral,
            ),
            (
                ExpressionPreset(
                    "aa", "EVENT_A", mouth=True, blink=False, look_at=False
                ),
                self.aa,
            ),
            (
                ExpressionPreset(
                    "ih", "EVENT_I", mouth=True, blink=False, look_at=False
                ),
                self.ih,
            ),
            (
                ExpressionPreset(
                    "ou", "EVENT_U", mouth=True, blink=False, look_at=False
                ),
                self.ou,
            ),
            (
                ExpressionPreset(
                    "ee", "EVENT_E", mouth=True, blink=False, look_at=False
                ),
                self.ee,
            ),
            (
                ExpressionPreset(
                    "oh", "EVENT_O", mouth=True, blink=False, look_at=False
                ),
                self.oh,
            ),
            (
                ExpressionPreset(
                    "blink", "HIDE_ON", mouth=False, blink=True, look_at=False
                ),
                self.blink,
            ),
            (
                ExpressionPreset(
                    "blinkLeft", "HIDE_ON", mouth=False, blink=True, look_at=False
                ),
                self.blink_left,
            ),
            (
                ExpressionPreset(
                    "blinkRight", "HIDE_ON", mouth=False, blink=True, look_at=False
                ),
                self.blink_right,
            ),
            (
                ExpressionPreset(
                    "lookUp", "ANCHOR_TOP", mouth=False, blink=False, look_at=True
                ),
                self.look_up,
            ),
            (
                ExpressionPreset(
                    "lookDown", "ANCHOR_BOTTOM", mouth=False, blink=False, look_at=True
                ),
                self.look_down,
            ),
            (
                ExpressionPreset(
                    "lookLeft", "ANCHOR_RIGHT", mouth=False, blink=False, look_at=True
                ),
                self.look_left,
            ),
            (
                ExpressionPreset(
                    "lookRight", "ANCHOR_LEFT", mouth=False, blink=False, look_at=True
                ),
                self.look_right,
            ),
        )

    def name_to_expression_dict(self) -> Mapping[str, Vrm1ExpressionPropertyGroup]:
        return {
            preset.name: property_group
            for preset, property_group in self.expression_preset_and_expressions()
        }

    def is_mouth_expression(self, expression_name: str) -> bool:
        return next(
            (
                preset.mouth
                for preset, _ in self.expression_preset_and_expressions()
                if preset.name == expression_name
            ),
            False,
        )

    def is_blink_expression(self, expression_name: str) -> bool:
        return next(
            (
                preset.blink
                for preset, _ in self.expression_preset_and_expressions()
                if preset.name == expression_name
            ),
            False,
        )

    def get_icon(self, expression_name: str) -> Optional[str]:
        return next(
            (
                preset.icon
                for preset, _ in self.expression_preset_and_expressions()
                if preset.name == expression_name
            ),
            None,
        )

    def is_look_at_expression(self, expression_name: str) -> bool:
        return next(
            (
                preset.look_at
                for preset, _ in self.expression_preset_and_expressions()
                if preset.name == expression_name
            ),
            False,
        )

    if TYPE_CHECKING:
        # This code is auto generated.
        # To regenerate, run the `uv run tools/property_typing.py` command.
        happy: Vrm1ExpressionPropertyGroup  # type: ignore[no-redef]
        angry: Vrm1ExpressionPropertyGroup  # type: ignore[no-redef]
        sad: Vrm1ExpressionPropertyGroup  # type: ignore[no-redef]
        relaxed: Vrm1ExpressionPropertyGroup  # type: ignore[no-redef]
        surprised: Vrm1ExpressionPropertyGroup  # type: ignore[no-redef]
        neutral: Vrm1ExpressionPropertyGroup  # type: ignore[no-redef]
        aa: Vrm1ExpressionPropertyGroup  # type: ignore[no-redef]
        ih: Vrm1ExpressionPropertyGroup  # type: ignore[no-redef]
        ou: Vrm1ExpressionPropertyGroup  # type: ignore[no-redef]
        ee: Vrm1ExpressionPropertyGroup  # type: ignore[no-redef]
        oh: Vrm1ExpressionPropertyGroup  # type: ignore[no-redef]
        blink: Vrm1ExpressionPropertyGroup  # type: ignore[no-redef]
        blink_left: Vrm1ExpressionPropertyGroup  # type: ignore[no-redef]
        blink_right: Vrm1ExpressionPropertyGroup  # type: ignore[no-redef]
        look_up: Vrm1ExpressionPropertyGroup  # type: ignore[no-redef]
        look_down: Vrm1ExpressionPropertyGroup  # type: ignore[no-redef]
        look_left: Vrm1ExpressionPropertyGroup  # type: ignore[no-redef]
        look_right: Vrm1ExpressionPropertyGroup  # type: ignore[no-redef]


# https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_vrm-1.0-beta/schema/VRMC_vrm.expressions.schema.json
class Vrm1ExpressionsPropertyGroup(PropertyGroup):
    preset: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1ExpressionsPresetPropertyGroup,
    )

    custom: CollectionProperty(  # type: ignore[valid-type]
        type=Vrm1CustomExpressionPropertyGroup,
    )

    def all_name_to_expression_dict(self) -> Mapping[str, Vrm1ExpressionPropertyGroup]:
        result: dict[str, Vrm1ExpressionPropertyGroup] = dict(
            self.preset.name_to_expression_dict()
        )
        for custom_expression in self.custom:
            result[custom_expression.custom_name] = custom_expression
        return result

    # An empty element is held for the number of expressions for UIList
    # display of expressions
    expression_ui_list_elements: CollectionProperty(  # type: ignore[valid-type]
        type=StringPropertyGroup
    )
    active_expression_ui_list_element_index: IntProperty(min=0)  # type: ignore[valid-type]

    def restore_expression_morph_target_bind_object_assignments(
        self, context: Context
    ) -> None:
        for expression in self.all_name_to_expression_dict().values():
            for morph_target_bind in expression.morph_target_binds:
                morph_target_bind.node.restore_object_assignment(context)

    if TYPE_CHECKING:
        # This code is auto generated.
        # To regenerate, run the `uv run tools/property_typing.py` command.
        preset: Vrm1ExpressionsPresetPropertyGroup  # type: ignore[no-redef]
        custom: CollectionPropertyProtocol[  # type: ignore[no-redef]
            Vrm1CustomExpressionPropertyGroup
        ]
        expression_ui_list_elements: CollectionPropertyProtocol[  # type: ignore[no-redef]
            StringPropertyGroup
        ]
        active_expression_ui_list_element_index: int  # type: ignore[no-redef]


# https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_vrm-1.0-beta/schema/VRMC_vrm.meta.schema.json
class Vrm1MetaPropertyGroup(PropertyGroup):
    avatar_permission_enum, __avatar_permissions = property_group_enum(
        ("onlyAuthor", "Only Author", "", "NONE", 0),
        (
            "onlySeparatelyLicensedPerson",
            "Only Separately Licensed Person",
            "",
            "NONE",
            1,
        ),
        ("everyone", "Everyone", "", "NONE", 2),
    )

    commercial_usage_enum, __commercial_usage = property_group_enum(
        ("personalNonProfit", "Personal Non-Profit", "", "NONE", 0),
        ("personalProfit", "Personal Profit", "", "NONE", 1),
        ("corporation", "Corporation", "", "NONE", 2),
    )

    credit_notation_enum, __credit_notation = property_group_enum(
        ("required", "Required", "", "NONE", 0),
        ("unnecessary", "Unnecessary", "", "NONE", 1),
    )

    modification_enum, __modification = property_group_enum(
        ("prohibited", "Prohibited", "", "NONE", 0),
        ("allowModification", "Allow Modification", "", "NONE", 1),
        (
            "allowModificationRedistribution",
            "Allow Modification Redistribution",
            "",
            "NONE",
            2,
        ),
    )

    vrm_name: StringProperty(  # type: ignore[valid-type]
        name="Name"
    )
    version: StringProperty(  # type: ignore[valid-type]
        name="Version"
    )
    authors: CollectionProperty(  # type: ignore[valid-type]
        type=StringPropertyGroup
    )
    copyright_information: StringProperty(  # type: ignore[valid-type]
        name="Copyright Information"
    )
    contact_information: StringProperty(  # type: ignore[valid-type]
        name="Contact Information"
    )
    references: CollectionProperty(  # type: ignore[valid-type]
        type=StringPropertyGroup
    )
    third_party_licenses: StringProperty(  # type: ignore[valid-type]
        name="Third Party Licenses"
    )
    thumbnail_image: PointerProperty(  # type: ignore[valid-type]
        name="Thumbnail Image",
        type=Image,
    )
    # license_url: StringProperty(  # type: ignore[valid-type]
    #     name="License URL"
    # )
    avatar_permission: EnumProperty(  # type: ignore[valid-type]
        name="Avatar Permission",
        items=avatar_permission_enum.items(),
    )
    allow_excessively_violent_usage: BoolProperty(  # type: ignore[valid-type]
        name="Allow Excessively Violent Usage"
    )
    allow_excessively_sexual_usage: BoolProperty(  # type: ignore[valid-type]
        name="Allow Excessively Sexual Usage"
    )
    commercial_usage: EnumProperty(  # type: ignore[valid-type]
        name="Commercial Usage",
        items=commercial_usage_enum.items(),
    )
    allow_political_or_religious_usage: BoolProperty(  # type: ignore[valid-type]
        name="Allow Political or Religious Usage"
    )
    allow_antisocial_or_hate_usage: BoolProperty(  # type: ignore[valid-type]
        name="Allow Antisocial or Hate Usage"
    )
    credit_notation: EnumProperty(  # type: ignore[valid-type]
        name="Credit Notation",
        items=credit_notation_enum.items(),
    )
    allow_redistribution: BoolProperty(  # type: ignore[valid-type]
        name="Allow Redistribution"
    )
    modification: EnumProperty(  # type: ignore[valid-type]
        name="Modification",
        items=modification_enum.items(),
    )
    other_license_url: StringProperty(  # type: ignore[valid-type]
        name="Other License URL"
    )

    # for UI
    active_author_index: IntProperty(min=0)  # type: ignore[valid-type]
    active_reference_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.
        vrm_name: str  # type: ignore[no-redef]
        version: str  # type: ignore[no-redef]
        authors: CollectionPropertyProtocol[  # type: ignore[no-redef]
            StringPropertyGroup
        ]
        copyright_information: str  # type: ignore[no-redef]
        contact_information: str  # type: ignore[no-redef]
        references: CollectionPropertyProtocol[  # type: ignore[no-redef]
            StringPropertyGroup
        ]
        third_party_licenses: str  # type: ignore[no-redef]
        thumbnail_image: Optional[Image]  # type: ignore[no-redef]
        avatar_permission: str  # type: ignore[no-redef]
        allow_excessively_violent_usage: bool  # type: ignore[no-redef]
        allow_excessively_sexual_usage: bool  # type: ignore[no-redef]
        commercial_usage: str  # type: ignore[no-redef]
        allow_political_or_religious_usage: bool  # type: ignore[no-redef]
        allow_antisocial_or_hate_usage: bool  # type: ignore[no-redef]
        credit_notation: str  # type: ignore[no-redef]
        allow_redistribution: bool  # type: ignore[no-redef]
        modification: str  # type: ignore[no-redef]
        other_license_url: str  # type: ignore[no-redef]
        active_author_index: int  # type: ignore[no-redef]
        active_reference_index: int  # type: ignore[no-redef]


# https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_vrm-1.0-beta/schema/VRMC_vrm.schema.json
class Vrm1PropertyGroup(PropertyGroup):
    meta: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1MetaPropertyGroup
    )
    humanoid: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1HumanoidPropertyGroup
    )
    first_person: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1FirstPersonPropertyGroup
    )
    look_at: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1LookAtPropertyGroup
    )
    expressions: PointerProperty(  # type: ignore[valid-type]
        type=Vrm1ExpressionsPropertyGroup
    )
    if TYPE_CHECKING:
        # This code is auto generated.
        # To regenerate, run the `uv run tools/property_typing.py` command.
        meta: Vrm1MetaPropertyGroup  # type: ignore[no-redef]
        humanoid: Vrm1HumanoidPropertyGroup  # type: ignore[no-redef]
        first_person: Vrm1FirstPersonPropertyGroup  # type: ignore[no-redef]
        look_at: Vrm1LookAtPropertyGroup  # type: ignore[no-redef]
        expressions: Vrm1ExpressionsPropertyGroup  # type: ignore[no-redef]


def get_armature_vrm1_extension(armature: Armature) -> Vrm1PropertyGroup:
    from ..extension import get_armature_extension

    vrm1: Vrm1PropertyGroup = get_armature_extension(armature).vrm1
    return vrm1
