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

import bpy
from bpy.props import (
    BoolProperty,
    CollectionProperty,
    EnumProperty,
    FloatProperty,
    FloatVectorProperty,
    IntProperty,
    PointerProperty,
    StringProperty,
)
from bpy.types import Armature, Context, Object, PropertyGroup
from mathutils import Matrix, Quaternion, Vector

from ...common import convert, safe_removal
from ...common.logger import get_logger
from ...common.rotation import (
    get_rotation_as_quaternion,
    set_rotation_without_mode_change,
)
from ..property_group import BonePropertyGroup, property_group_enum

if TYPE_CHECKING:
    from ..property_group import CollectionPropertyProtocol

logger = get_logger(__name__)


def find_armature_and_collider(
    context: Context,
    match_collider: Callable[["SpringBone1ColliderPropertyGroup"], bool],
) -> tuple[Object, "SpringBone1ColliderPropertyGroup"]:
    for obj in context.blend_data.objects:
        if obj.type != "ARMATURE":
            continue
        armature_data = obj.data
        if not isinstance(armature_data, Armature):
            continue
        for collider in get_armature_spring_bone1_extension(armature_data).colliders:
            if match_collider(collider):
                return (obj, collider)
    message = "No armature"
    raise ValueError(message)


# https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_springBone-1.0-beta/schema/VRMC_springBone.shape.schema.json#L7-L27
class SpringBone1ColliderShapeSpherePropertyGroup(PropertyGroup):
    def find_armature_and_collider(
        self,
        context: Context,
    ) -> tuple[Object, "SpringBone1ColliderPropertyGroup"]:
        return find_armature_and_collider(
            context, lambda collider: collider.shape.sphere == self
        )

    def get_offset(self) -> tuple[float, float, float]:
        context = bpy.context

        armature, collider = self.find_armature_and_collider(context)
        if not collider.bpy_object:
            logger.error(
                'Failed to get bpy object of "%s" in sphere.get_offset()', collider.name
            )
            return (0, 0, 0)
        bone = armature.pose.bones.get(collider.node.bone_name)
        if bone:
            matrix = (
                bone.matrix.inverted()
                @ armature.matrix_world.inverted()
                @ collider.bpy_object.matrix_world
            )
        else:
            matrix = armature.matrix_world.inverted() @ collider.bpy_object.matrix_world
        return convert.float3_or(matrix.to_translation(), (0.0, 0.0, 0.0))

    def set_offset(self, offset: Sequence[float]) -> None:
        context = bpy.context

        armature, collider = self.find_armature_and_collider(context)
        collider.reset_bpy_object(context, armature)
        bone = armature.pose.bones.get(collider.node.bone_name)
        if not bone:
            if collider.bpy_object:
                collider.bpy_object.matrix_world = (
                    armature.matrix_world @ Matrix.Translation(offset)
                )
            return
        if collider.bpy_object:
            collider.bpy_object.matrix_world = (
                armature.matrix_world @ bone.matrix @ Matrix.Translation(offset)
            )

    def update_offset(self, _context: Context) -> None:
        self.fallback_offset = self.offset

    def get_radius(self) -> float:
        context = bpy.context

        _, collider = self.find_armature_and_collider(context)
        if not collider.bpy_object:
            logger.error(
                'Failed to get bpy object of "%s" in sphere.get_radius()', collider.name
            )
            return 0.0
        mean_scale = statistics.mean(
            abs(s) for s in collider.bpy_object.matrix_basis.to_scale()
        )
        empty_display_size: float = collider.bpy_object.empty_display_size
        return float(mean_scale) * empty_display_size

    def set_radius(self, v: float) -> None:
        context = bpy.context

        armature, collider = self.find_armature_and_collider(context)
        collider.reset_bpy_object(context, armature)
        if not collider.bpy_object:
            logger.error(
                'Failed to reset bpy object of "%s" in sphere.set_radius()',
                collider.name,
            )
            return
        location, rotation, _ = collider.bpy_object.matrix_basis.decompose()
        collider.bpy_object.matrix_basis = (
            Matrix.Translation(location) @ rotation.to_matrix().to_4x4()
        )
        collider.bpy_object.empty_display_size = v

    def update_radius(self, _context: Context) -> None:
        self.fallback_radius = self.radius

    offset: FloatVectorProperty(  # type: ignore[valid-type]
        size=3,
        subtype="TRANSLATION",
        unit="LENGTH",
        default=(0, 0, 0),
        get=get_offset,
        set=set_offset,
        update=update_offset,
    )

    radius: FloatProperty(  # type: ignore[valid-type]
        name="Radius",
        min=0.0,
        default=0.0,
        soft_max=1.0,
        unit="LENGTH",
        get=get_radius,
        set=set_radius,
        update=update_radius,
    )

    fallback_offset: FloatVectorProperty(  # type: ignore[valid-type]
        name="Fallback Sphere Offset",
        size=3,
        subtype="TRANSLATION",
        unit="LENGTH",
        default=(0, 0, 0),
    )

    fallback_radius: FloatProperty(  # type: ignore[valid-type]
        name="Fallback Sphere Radius",
        min=0.0,
        default=0.0,
        soft_max=1.0,
        unit="LENGTH",
    )

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


# https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_springBone-1.0-beta/schema/VRMC_springBone.shape.schema.json#L28-L58
class SpringBone1ColliderShapeCapsulePropertyGroup(PropertyGroup):
    def find_armature_and_collider(
        self,
        context: Context,
    ) -> tuple[Object, "SpringBone1ColliderPropertyGroup"]:
        return find_armature_and_collider(
            context, lambda collider: collider.shape.capsule == self
        )

    def get_offset(self) -> tuple[float, float, float]:
        context = bpy.context

        armature, collider = self.find_armature_and_collider(context)
        if not collider.bpy_object:
            logger.error(
                'Failed to get bpy object of "%s" in capsule.get_offset()',
                collider.name,
            )
            return (0, 0, 0)
        bone = armature.pose.bones.get(collider.node.bone_name)
        if bone:
            matrix = (
                bone.matrix.inverted()
                @ armature.matrix_world.inverted()
                @ collider.bpy_object.matrix_world
            )
        else:
            matrix = armature.matrix_world.inverted() @ collider.bpy_object.matrix_world
        return convert.float3_or(matrix.to_translation(), (0.0, 0.0, 0.0))

    def set_offset(self, offset: Sequence[float]) -> None:
        context = bpy.context

        armature, collider = self.find_armature_and_collider(context)
        collider.reset_bpy_object(context, armature)
        bone = armature.pose.bones.get(collider.node.bone_name)
        if not bone:
            if collider.bpy_object:
                collider.bpy_object.matrix_world = (
                    armature.matrix_world @ Matrix.Translation(offset)
                )
            return
        if collider.bpy_object:
            collider.bpy_object.matrix_world = (
                armature.matrix_world @ bone.matrix @ Matrix.Translation(offset)
            )

    def update_offset(self, _context: Context) -> None:
        self.fallback_offset = self.offset

    def get_tail(self) -> tuple[float, float, float]:
        context = bpy.context

        armature, collider = self.find_armature_and_collider(context)
        if not collider.bpy_object or not collider.bpy_object.children:
            logger.error(
                'Failed to get bpy object of "%s" in capsule.get_tail()', collider.name
            )
            return (0, 0, 0)
        bone = armature.pose.bones.get(collider.node.bone_name)
        if bone:
            matrix = (
                bone.matrix.inverted()
                @ armature.matrix_world.inverted()
                @ collider.bpy_object.children[0].matrix_world
            )
        else:
            matrix = (
                armature.matrix_world.inverted()
                @ collider.bpy_object.children[0].matrix_world
            )
        return convert.float3_or(matrix.to_translation(), (0.0, 0.0, 0.0))

    def set_tail(self, offset: Sequence[float]) -> None:
        context = bpy.context

        armature, collider = self.find_armature_and_collider(context)
        collider.reset_bpy_object(context, armature)
        bone = armature.pose.bones.get(collider.node.bone_name)
        if not bone:
            if collider.bpy_object:
                collider.bpy_object.children[0].matrix_world = (
                    armature.matrix_world @ Matrix.Translation(offset)
                )
            return
        if collider.bpy_object:
            if not collider.bpy_object.children:
                logger.error(
                    'Failed to set tail bpy object matrix of "%s"'
                    " in capsule.set_tail()",
                    collider.name,
                )
                return
            collider.bpy_object.children[0].matrix_world = (
                armature.matrix_world @ bone.matrix @ Matrix.Translation(offset)
            )

    def update_tail(self, _context: Context) -> None:
        self.fallback_tail = self.tail

    def get_radius(self) -> float:
        context = bpy.context

        _, collider = self.find_armature_and_collider(context)
        if not collider.bpy_object:
            logger.error(
                'Failed to get bpy object of "%s" in capsule.get_radius()',
                collider.name,
            )
            return 0.0
        mean_scale: float = statistics.mean(
            abs(s) for s in collider.bpy_object.matrix_basis.to_scale()
        )
        empty_display_size: float = collider.bpy_object.empty_display_size
        return mean_scale * empty_display_size

    def set_radius(self, v: float) -> None:
        context = bpy.context

        armature, collider = self.find_armature_and_collider(context)
        collider.reset_bpy_object(context, armature)
        if not collider.bpy_object:
            logger.error(
                'Failed to reset bpy object of "%s" in capsule.set_radius()',
                collider.name,
            )
            return
        location, rotation, _ = collider.bpy_object.matrix_basis.decompose()
        collider.bpy_object.matrix_basis = (
            Matrix.Translation(location) @ rotation.to_matrix().to_4x4()
        )
        collider.bpy_object.empty_display_size = v
        if not collider.bpy_object.children:
            logger.error(
                'Failed to set tail bpy object size of "%s" in capsule.set_radius()',
                collider.name,
            )
            return
        collider.bpy_object.children[0].empty_display_size = v

    def update_radius(self, _context: Context) -> None:
        self.fallback_radius = self.radius

    offset: FloatVectorProperty(  # type: ignore[valid-type]
        size=3,
        subtype="TRANSLATION",
        unit="LENGTH",
        default=(0, 0, 0),
        get=get_offset,
        set=set_offset,
        update=update_offset,
    )

    radius: FloatProperty(  # type: ignore[valid-type]
        name="Radius",
        min=0.0,
        default=0.0,
        soft_max=1.0,
        unit="LENGTH",
        get=get_radius,
        set=set_radius,
        update=update_radius,
    )

    tail: FloatVectorProperty(  # type: ignore[valid-type]
        size=3,
        subtype="TRANSLATION",
        unit="LENGTH",
        default=(0, 0, 0),
        get=get_tail,
        set=set_tail,
        update=update_tail,
    )

    fallback_offset: FloatVectorProperty(  # type: ignore[valid-type]
        name="Fallback Capsule Head",
        size=3,
        subtype="TRANSLATION",
        unit="LENGTH",
        default=(0, 0, 0),
    )

    fallback_radius: FloatProperty(  # type: ignore[valid-type]
        name="Fallback Capsule Radius",
        min=0.0,
        default=0.0,
        soft_max=1.0,
        unit="LENGTH",
    )

    fallback_tail: FloatVectorProperty(  # type: ignore[valid-type]
        name="Fallback Capsule Tail",
        size=3,
        subtype="TRANSLATION",
        unit="LENGTH",
        default=(0, 0, 0),
    )

    if TYPE_CHECKING:
        # This code is auto generated.
        # To regenerate, run the `uv run tools/property_typing.py` command.
        offset: Sequence[float]  # type: ignore[no-redef]
        radius: float  # type: ignore[no-redef]
        tail: Sequence[float]  # type: ignore[no-redef]
        fallback_offset: Sequence[float]  # type: ignore[no-redef]
        fallback_radius: float  # type: ignore[no-redef]
        fallback_tail: Sequence[float]  # type: ignore[no-redef]


# https://github.com/vrm-c/vrm-specification/blob/9e8c886a2043639a3c166eb7cc93526be6313147/specification/VRMC_springBone_extended_collider-1.0/schema/VRMC_springBone_extended_collider.shape.schema.json#L9
class SpringBone1ExtendedColliderShapeSpherePropertyGroup(
    SpringBone1ColliderShapeSpherePropertyGroup
):
    def find_armature_and_collider(
        self,
        context: Context,
    ) -> tuple[Object, "SpringBone1ColliderPropertyGroup"]:
        return find_armature_and_collider(
            context,
            lambda collider: (
                collider.extensions.vrmc_spring_bone_extended_collider.shape.sphere
                == self
            ),
        )

    inside: BoolProperty()  # type: ignore[valid-type]

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


# https://github.com/vrm-c/vrm-specification/blob/9e8c886a2043639a3c166eb7cc93526be6313147/specification/VRMC_springBone_extended_collider-1.0/schema/VRMC_springBone_extended_collider.shape.schema.json#L36
class SpringBone1ExtendedColliderShapeCapsulePropertyGroup(
    SpringBone1ColliderShapeCapsulePropertyGroup
):
    def find_armature_and_collider(
        self,
        context: Context,
    ) -> tuple[Object, "SpringBone1ColliderPropertyGroup"]:
        return find_armature_and_collider(
            context,
            lambda collider: (
                collider.extensions.vrmc_spring_bone_extended_collider.shape.capsule
                == self
            ),
        )

    inside: BoolProperty()  # type: ignore[valid-type]

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


# https://github.com/vrm-c/vrm-specification/blob/9e8c886a2043639a3c166eb7cc93526be6313147/specification/VRMC_springBone_extended_collider-1.0/schema/VRMC_springBone_extended_collider.shape.schema.json#L73
class SpringBone1ExtendedColliderShapePlanePropertyGroup(PropertyGroup):
    def find_armature_and_collider(
        self,
        context: Context,
    ) -> tuple[Object, "SpringBone1ColliderPropertyGroup"]:
        return find_armature_and_collider(
            context,
            lambda collider: (
                collider.extensions.vrmc_spring_bone_extended_collider.shape.plane
                == self
            ),
        )

    def get_offset(self) -> tuple[float, float, float]:
        context = bpy.context

        armature, collider = self.find_armature_and_collider(context)
        if not collider.bpy_object:
            logger.error(
                'Failed to get bpy object of "%s" in extended_collider.get_offset()',
                collider.name,
            )
            return (0, 0, 0)
        bone = armature.pose.bones.get(collider.node.bone_name)
        if bone:
            matrix = (
                bone.matrix.inverted()
                @ armature.matrix_world.inverted()
                @ collider.bpy_object.matrix_world
            )
        else:
            matrix = armature.matrix_world.inverted() @ collider.bpy_object.matrix_world
        return convert.float3_or(matrix.to_translation(), (0.0, 0.0, 0.0))

    def set_offset(self, offset: Sequence[float]) -> None:
        context = bpy.context

        armature, collider = self.find_armature_and_collider(context)
        collider.reset_bpy_object(context, armature)
        bone = armature.pose.bones.get(collider.node.bone_name)
        if not bone:
            if collider.bpy_object:
                collider.bpy_object.matrix_world = (
                    armature.matrix_world @ Matrix.Translation(offset)
                )
            return
        if collider.bpy_object:
            collider.bpy_object.matrix_world = (
                armature.matrix_world @ bone.matrix @ Matrix.Translation(offset)
            )

    def get_normal(self) -> tuple[float, float, float]:
        context = bpy.context

        _, collider = self.find_armature_and_collider(context)
        bpy_object = collider.bpy_object
        if not bpy_object:
            return (0.0, 1.0, 0.0)

        result = Vector((0.0, 1.0, 0.0))
        result.rotate(get_rotation_as_quaternion(bpy_object))

        return (
            # use tuple initializer to make type checkers happy
            result.x,
            result.y,
            result.z,
        )

    def set_normal(self, normal: Sequence[float]) -> None:
        context = bpy.context

        armature, collider = self.find_armature_and_collider(context)
        collider.reset_bpy_object(context, armature)

        y_up_vec = Vector((0.0, 1.0, 0.0))
        normal_vec = Vector(normal)
        if normal_vec.length_squared > 0:
            rotation = y_up_vec.rotation_difference(normal_vec)
        else:
            rotation = Quaternion()

        bpy_object = collider.bpy_object
        if bpy_object:
            set_rotation_without_mode_change(bpy_object, rotation)

    offset: FloatVectorProperty(  # type: ignore[valid-type]
        size=3,
        subtype="TRANSLATION",
        unit="LENGTH",
        default=(0, 0, 0),
        get=get_offset,
        set=set_offset,
    )

    normal: FloatVectorProperty(  # type: ignore[valid-type]
        size=3,
        default=(0, 0, 1),
        get=get_normal,
        set=set_normal,
    )

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


# https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_springBone-1.0-beta/schema/VRMC_springBone.shape.schema.json
class SpringBone1ColliderShapePropertyGroup(PropertyGroup):
    sphere: PointerProperty(  # type: ignore[valid-type]
        type=SpringBone1ColliderShapeSpherePropertyGroup
    )
    capsule: PointerProperty(  # type: ignore[valid-type]
        type=SpringBone1ColliderShapeCapsulePropertyGroup
    )

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


# https://github.com/vrm-c/vrm-specification/blob/9e8c886a2043639a3c166eb7cc93526be6313147/specification/VRMC_springBone_extended_collider-1.0/schema/VRMC_springBone_extended_collider.shape.schema.json
class SpringBone1ExtendedColliderShapePropertyGroup(PropertyGroup):
    sphere: PointerProperty(  # type: ignore[valid-type]
        type=SpringBone1ExtendedColliderShapeSpherePropertyGroup
    )
    capsule: PointerProperty(  # type: ignore[valid-type]
        type=SpringBone1ExtendedColliderShapeCapsulePropertyGroup
    )
    plane: PointerProperty(  # type: ignore[valid-type]
        type=SpringBone1ExtendedColliderShapePlanePropertyGroup
    )

    if TYPE_CHECKING:
        # This code is auto generated.
        # To regenerate, run the `uv run tools/property_typing.py` command.
        sphere: (  # type: ignore[no-redef]
            SpringBone1ExtendedColliderShapeSpherePropertyGroup
        )
        capsule: (  # type: ignore[no-redef]
            SpringBone1ExtendedColliderShapeCapsulePropertyGroup
        )
        plane: (  # type: ignore[no-redef]
            SpringBone1ExtendedColliderShapePlanePropertyGroup
        )


# https://github.com/vrm-c/vrm-specification/blob/9e8c886a2043639a3c166eb7cc93526be6313147/specification/VRMC_springBone_extended_collider-1.0/schema/VRMC_springBone_extended_collider.schema.json
class SpringBone1VrmcSpringBoneExtendedColliderPropertyGroup(PropertyGroup):
    def update_shape_type(self, context: Context) -> None:
        armature, collider = find_armature_and_collider(
            context,
            lambda collider: (
                collider.extensions.vrmc_spring_bone_extended_collider == self
            ),
        )
        collider.reset_bpy_object(context, armature)

    enabled: BoolProperty(  # type: ignore[valid-type]
        name="Extended Collider",
        update=update_shape_type,
    )

    automatic_fallback_generation: BoolProperty(  # type: ignore[valid-type]
        name="Automatic Fallback Generation",
        default=True,
    )

    shape: PointerProperty(  # type: ignore[valid-type]
        type=SpringBone1ExtendedColliderShapePropertyGroup
    )

    (
        shape_type_enum,
        (
            SHAPE_TYPE_EXTENDED_SPHERE,
            SHAPE_TYPE_EXTENDED_CAPSULE,
            SHAPE_TYPE_EXTENDED_PLANE,
        ),
    ) = property_group_enum(
        ("extendedSphere", "Sphere", "", "NONE", 0),
        ("extendedCapsule", "Capsule", "", "NONE", 1),
        ("extendedPlane", "Plane", "", "NONE", 2),
    )

    shape_type: EnumProperty(  # type: ignore[valid-type]
        items=shape_type_enum.items(),
        name="Shape",
        update=update_shape_type,
    )

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


class SpringBone1ColliderExtensionsPropertyGroup(PropertyGroup):
    vrmc_spring_bone_extended_collider: PointerProperty(  # type: ignore[valid-type]
        type=SpringBone1VrmcSpringBoneExtendedColliderPropertyGroup
    )

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


# https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_springBone-1.0-beta/schema/VRMC_springBone.collider.schema.json
class SpringBone1ColliderPropertyGroup(PropertyGroup):
    def broadcast_bpy_object_name(self) -> None:
        armature = self.id_data
        if not isinstance(armature, Armature):
            message = (
                f"{type(self)}/{self}.id_data is not a {Armature}"
                + f" but {type(armature)}/{armature}"
            )
            raise TypeError(message)

        if not self.bpy_object or not self.bpy_object.name:
            self.name = ""
            return
        if self.name == self.bpy_object.name:
            return
        self.name = self.bpy_object.name

        spring_bone = get_armature_spring_bone1_extension(armature)
        for collider_group in spring_bone.collider_groups:
            for collider_reference in collider_group.colliders:
                if self.uuid != collider_reference.collider_uuid:
                    continue
                if collider_reference.collider_name != self.name:
                    collider_reference.collider_name = self.name

    node: PointerProperty(type=BonePropertyGroup)  # type: ignore[valid-type]
    shape: PointerProperty(type=SpringBone1ColliderShapePropertyGroup)  # type: ignore[valid-type]
    extensions: PointerProperty(type=SpringBone1ColliderExtensionsPropertyGroup)  # type: ignore[valid-type]

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

    (
        shape_type_enum,
        (
            SHAPE_TYPE_SPHERE,
            SHAPE_TYPE_CAPSULE,
        ),
    ) = property_group_enum(
        ("Sphere", "Sphere", "", "NONE", 0),
        ("Capsule", "Capsule", "", "NONE", 1),
    )

    (
        ui_collider_type_enum,
        (
            UI_COLLIDER_TYPE_SPHERE,
            UI_COLLIDER_TYPE_CAPSULE,
            UI_COLLIDER_TYPE_PLANE,
            UI_COLLIDER_TYPE_SPHERE_INSIDE,
            UI_COLLIDER_TYPE_CAPSULE_INSIDE,
        ),
    ) = property_group_enum(
        ("uiColliderTypeSphere", "Sphere", "", "NONE", 0),
        ("uiColliderTypeCapsule", "Capsule", "", "NONE", 1),
        ("uiColliderTypePlane", "Plane", "", "NONE", 2),
        ("uiColliderTypeSphereInside", "Sphere Inside", "", "NONE", 3),
        ("uiColliderTypeCapsuleInside", "Capsule Inside", "", "NONE", 4),
    )

    def get_ui_collider_type(self) -> int:
        extended = self.extensions.vrmc_spring_bone_extended_collider
        if not extended.enabled:
            if self.shape_type == self.SHAPE_TYPE_SPHERE.identifier:
                return self.UI_COLLIDER_TYPE_SPHERE.value
            if self.shape_type == self.SHAPE_TYPE_CAPSULE.identifier:
                return self.UI_COLLIDER_TYPE_CAPSULE.value
            return self.UI_COLLIDER_TYPE_SPHERE.value

        if extended.shape_type == extended.SHAPE_TYPE_EXTENDED_PLANE.identifier:
            return self.UI_COLLIDER_TYPE_PLANE.value
        if extended.shape_type == extended.SHAPE_TYPE_EXTENDED_SPHERE.identifier:
            if extended.shape.sphere.inside:
                return self.UI_COLLIDER_TYPE_SPHERE_INSIDE.value
            return self.UI_COLLIDER_TYPE_SPHERE.value
        if extended.shape_type == extended.SHAPE_TYPE_EXTENDED_CAPSULE.identifier:
            if extended.shape.capsule.inside:
                return self.UI_COLLIDER_TYPE_CAPSULE_INSIDE.value
            return self.UI_COLLIDER_TYPE_CAPSULE.value
        return self.UI_COLLIDER_TYPE_SPHERE.value

    def set_ui_collider_type(self, value: int) -> None:
        extended = self.extensions.vrmc_spring_bone_extended_collider

        if value in [
            self.UI_COLLIDER_TYPE_SPHERE.value,
            self.UI_COLLIDER_TYPE_SPHERE_INSIDE.value,
        ]:
            if self.shape_type != self.SHAPE_TYPE_SPHERE.identifier:
                self.shape_type = self.SHAPE_TYPE_SPHERE.identifier
            if extended.shape_type != extended.SHAPE_TYPE_EXTENDED_SPHERE.identifier:
                extended.shape_type = extended.SHAPE_TYPE_EXTENDED_SPHERE.identifier
            inside = self.UI_COLLIDER_TYPE_SPHERE_INSIDE.value == value
            if extended.shape.sphere.inside != inside:
                extended.shape.sphere.inside = inside
            if extended.enabled != inside:
                extended.enabled = inside
            return

        if value in [
            self.UI_COLLIDER_TYPE_CAPSULE.value,
            self.UI_COLLIDER_TYPE_CAPSULE_INSIDE.value,
        ]:
            if self.shape_type != self.SHAPE_TYPE_CAPSULE.identifier:
                self.shape_type = self.SHAPE_TYPE_CAPSULE.identifier
            if extended.shape_type != extended.SHAPE_TYPE_EXTENDED_CAPSULE.identifier:
                extended.shape_type = extended.SHAPE_TYPE_EXTENDED_CAPSULE.identifier
            inside = self.UI_COLLIDER_TYPE_CAPSULE_INSIDE.value == value
            if extended.shape.capsule.inside != inside:
                extended.shape.capsule.inside = inside
            if extended.enabled != inside:
                extended.enabled = inside
            return

        if self.UI_COLLIDER_TYPE_PLANE.value == value:
            if not extended.enabled:
                extended.enabled = True
            if extended.shape_type != extended.SHAPE_TYPE_EXTENDED_PLANE.identifier:
                extended.shape_type = extended.SHAPE_TYPE_EXTENDED_PLANE.identifier

    def update_shape_type(self, context: Context) -> None:
        if (
            self.bpy_object
            and self.bpy_object.parent
            and self.bpy_object.parent.type == "ARMATURE"
        ):
            self.reset_bpy_object(context, self.bpy_object.parent)

    shape_type: EnumProperty(  # type: ignore[valid-type]
        items=shape_type_enum.items(),
        name="Shape",
        update=update_shape_type,
    )

    ui_collider_type: EnumProperty(  # type: ignore[valid-type]
        items=ui_collider_type_enum.items(),
        name="Collider Type",
        get=get_ui_collider_type,
        set=set_ui_collider_type,
    )

    # for View3D
    bpy_object: PointerProperty(  # type: ignore[valid-type]
        type=Object
    )

    # for references
    uuid: StringProperty()  # type: ignore[valid-type]

    def reset_bpy_object(self, context: Context, armature: Object) -> None:
        armature_data = armature.data
        if not isinstance(armature_data, Armature):
            return
        collider_prefix = armature_data.name
        if self.node and self.node.bone_name:
            collider_prefix = self.node.bone_name

        if not self.bpy_object or not self.bpy_object.name:
            obj = context.blend_data.objects.new(
                name=f"{collider_prefix} Collider", object_data=None
            )
            obj.empty_display_size = 0.125
            context.scene.collection.objects.link(obj)
            self.bpy_object = obj
        self.bpy_object.select_set(True)

        if self.bpy_object.parent != armature:
            self.bpy_object.parent = armature

        extended = self.extensions.vrmc_spring_bone_extended_collider
        if (
            not extended.enabled
            and self.shape_type == self.SHAPE_TYPE_SPHERE.identifier
        ) or (
            extended.enabled
            and extended.shape_type
            in [
                extended.SHAPE_TYPE_EXTENDED_SPHERE.identifier,
                extended.SHAPE_TYPE_EXTENDED_PLANE.identifier,
            ]
        ):
            for child in list(self.bpy_object.children):
                if not safe_removal.remove_object(context, child):
                    logger.warning(
                        'Failed to remove "%s" with %d users'
                        " while changing spring bone collider type",
                        child.name,
                        child.users,
                    )
            empty_display_type = "SPHERE"
            if extended.shape_type == extended.SHAPE_TYPE_EXTENDED_PLANE.identifier:
                empty_display_type = "CIRCLE"
            if self.bpy_object.empty_display_type != empty_display_type:
                self.bpy_object.empty_display_type = empty_display_type
        elif (
            not extended.enabled
            and self.shape_type == self.SHAPE_TYPE_CAPSULE.identifier
        ) or (
            extended.enabled
            and extended.shape_type == extended.SHAPE_TYPE_EXTENDED_CAPSULE.identifier
        ):
            if self.bpy_object.children:
                end_object = self.bpy_object.children[0]
            else:
                end_object = context.blend_data.objects.new(
                    name=f"{self.bpy_object.name} End", object_data=None
                )
                end_object.empty_display_size = self.bpy_object.empty_display_size
                context.scene.collection.objects.link(end_object)
                end_object.parent = self.bpy_object
                end_object.matrix_local = Matrix.Translation(Vector((0.0, 0.2, 0.0)))
            end_object.select_set(True)
            if self.bpy_object.empty_display_type != "SPHERE":
                self.bpy_object.empty_display_type = "SPHERE"
            if end_object.empty_display_type != "SPHERE":
                end_object.empty_display_type = "SPHERE"
            if end_object.empty_display_size != self.bpy_object.empty_display_size:
                end_object.empty_display_size = self.bpy_object.empty_display_size
            if end_object.scale != Vector((1, 1, 1)):
                end_object.scale = Vector((1, 1, 1))

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

        self.broadcast_bpy_object_name()

    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]
        shape: SpringBone1ColliderShapePropertyGroup  # type: ignore[no-redef]
        extensions: SpringBone1ColliderExtensionsPropertyGroup  # type: ignore[no-redef]
        show_expanded: bool  # type: ignore[no-redef]
        shape_type: str  # type: ignore[no-redef]
        ui_collider_type: str  # type: ignore[no-redef]
        bpy_object: Optional[Object]  # type: ignore[no-redef]
        uuid: str  # type: ignore[no-redef]


class SpringBone1ColliderReferencePropertyGroup(PropertyGroup):
    def get_collider_name(self) -> str:
        value = self.get("collider_name", "")
        return value if isinstance(value, str) else str(value)

    def set_collider_name(self, value: object) -> None:
        armature = self.id_data
        if not isinstance(armature, Armature):
            message = (
                f"{type(self)}/{self}.id_data is not a {Armature}"
                + f" but {type(armature)}/{armature}"
            )
            raise TypeError(message)

        if not isinstance(value, str):
            value = str(value)
        self.name = value
        if self.get("collider_name") == value:
            return
        self["collider_name"] = value

        spring_bone = get_armature_spring_bone1_extension(armature)
        for collider in spring_bone.colliders:
            if collider.name != value:
                continue
            if self.collider_uuid != collider.uuid:
                self.collider_uuid = collider.uuid

    collider_name: StringProperty(  # type: ignore[valid-type]
        get=get_collider_name, set=set_collider_name
    )
    collider_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.
        collider_name: str  # type: ignore[no-redef]
        collider_uuid: str  # type: ignore[no-redef]


# https://github.com/vrm-c/vrm-specification/blob/f2d8f158297fc883aef9c3071ca68fbe46b03f45/specification/0.0/schema/vrm.secondaryanimation.collidergroup.schema.json
class SpringBone1ColliderGroupPropertyGroup(PropertyGroup):
    def get_vrm_name(self) -> str:
        value = self.get("vrm_name", "")
        return value if isinstance(value, str) else str(value)

    def set_vrm_name(self, vrm_name: object) -> None:
        if not isinstance(vrm_name, str):
            vrm_name = str(vrm_name)
        self["vrm_name"] = vrm_name
        self.fix_index()

    def fix_index(self) -> None:
        armature = self.id_data
        if not isinstance(armature, Armature):
            message = (
                f"{type(self)}/{self}.id_data is not a {Armature}"
                + f" but {type(armature)}/{armature}"
            )
            raise TypeError(message)

        spring_bone = get_armature_spring_bone1_extension(armature)

        for index, collider_group in enumerate(spring_bone.collider_groups):
            if collider_group.path_from_id() != self.path_from_id():
                continue

            name = f"{self.vrm_name} #{index + 1}"
            self.name = name

            for spring in spring_bone.springs:
                for collider_group_reference in spring.collider_groups:
                    if collider_group_reference.collider_group_uuid == self.uuid:
                        collider_group_reference.collider_group_name = name

            return

    vrm_name: StringProperty(  # type: ignore[valid-type]
        name="Name",
        get=get_vrm_name,
        set=set_vrm_name,
    )

    colliders: CollectionProperty(  # type: ignore[valid-type]
        name="Colliders",
        type=SpringBone1ColliderReferencePropertyGroup,
    )

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

    # for reference
    # When copying objects, there may be multiple objects with the same uuid
    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.
        vrm_name: str  # type: ignore[no-redef]
        colliders: CollectionPropertyProtocol[  # type: ignore[no-redef]
            SpringBone1ColliderReferencePropertyGroup
        ]
        show_expanded: bool  # type: ignore[no-redef]
        active_collider_index: int  # type: ignore[no-redef]
        uuid: str  # type: ignore[no-redef]


# https://github.com/vrm-c/vrm-specification/tree/993a90a5bda9025f3d9e2923ad6dea7506f88553/specification/VRMC_springBone-1.0#initialization
class SpringBone1JointAnimationStatePropertyGroup(PropertyGroup):
    initialized_as_tail: BoolProperty()  # type: ignore[valid-type]

    previous_world_translation: FloatVectorProperty(size=3)  # type: ignore[valid-type]
    current_world_translation: FloatVectorProperty(size=3)  # type: ignore[valid-type]

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


# https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_springBone-1.0-beta/schema/VRMC_springBone.joint.schema.json
class SpringBone1JointPropertyGroup(PropertyGroup):
    node: PointerProperty(type=BonePropertyGroup)  # type: ignore[valid-type]

    hit_radius: FloatProperty(  # type: ignore[valid-type]
        name="Joint Radius",
        min=0.0,
        default=0.0,
        soft_max=0.5,
    )

    stiffness: FloatProperty(  # type: ignore[valid-type]
        name="Stiffness Force",
        min=0.0,
        default=1.0,
        soft_max=4.0,
    )

    gravity_power: FloatProperty(  # type: ignore[valid-type]
        name="Gravity Power",
        min=0.0,
        default=0.0,
        soft_max=2.0,
    )

    def update_gravity_dir(self, _context: Context) -> None:
        gravity_dir = Vector(self.gravity_dir)
        normalized_gravity_dir = gravity_dir.normalized()
        if abs(normalized_gravity_dir.length) < float_info.epsilon:
            self.gravity_dir = (0, 0, -1)
        elif (gravity_dir - normalized_gravity_dir).length > 0.0001:
            self.gravity_dir = normalized_gravity_dir

    gravity_dir: FloatVectorProperty(  # type: ignore[valid-type]
        name="Gravity Dir",
        size=3,
        min=-1,
        max=1,
        default=(0, 0, -1),
        subtype="XYZ",
        update=update_gravity_dir,
    )

    drag_force: FloatProperty(  # type: ignore[valid-type]
        name="Drag Force",
        default=0.5,
        min=0,
        max=1.0,
    )

    animation_state: PointerProperty(  # type: ignore[valid-type]
        type=SpringBone1JointAnimationStatePropertyGroup,
    )

    # for UI
    show_expanded: BoolProperty()  # 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]
        hit_radius: float  # type: ignore[no-redef]
        stiffness: 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]
        animation_state: (  # type: ignore[no-redef]
            SpringBone1JointAnimationStatePropertyGroup
        )
        show_expanded: bool  # type: ignore[no-redef]


class SpringBone1ColliderGroupReferencePropertyGroup(PropertyGroup):
    def get_collider_group_name(self) -> str:
        value = self.get("collider_group_name", "")
        return value if isinstance(value, str) else str(value)

    def set_collider_group_name(self, value: object) -> None:
        armature = self.id_data
        if not isinstance(armature, Armature):
            message = (
                f"{type(self)}/{self}.id_data is not a {Armature}"
                + f" but {type(armature)}/{armature}"
            )
            raise TypeError(message)

        if not isinstance(value, str):
            value = str(value)
        self.name = value
        if self.get("collider_group_name") != value:
            self["collider_group_name"] = value

        spring_bone = get_armature_spring_bone1_extension(armature)
        for collider_group in spring_bone.collider_groups:
            if collider_group.name != value:
                continue
            if self.collider_group_uuid != collider_group.uuid:
                self.collider_group_uuid = collider_group.uuid
            break

    collider_group_name: StringProperty(  # type: ignore[valid-type]
        get=get_collider_group_name, set=set_collider_group_name
    )
    collider_group_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.
        collider_group_name: str  # type: ignore[no-redef]
        collider_group_uuid: str  # type: ignore[no-redef]


class SpringBone1SpringAnimationStatePropertyGroup(PropertyGroup):
    use_center_space: BoolProperty()  # type: ignore[valid-type]

    previous_center_world_translation: FloatVectorProperty(size=3)  # type: ignore[valid-type]

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


# https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_springBone-1.0-beta/schema/VRMC_springBone.spring.schema.json
class SpringBone1SpringPropertyGroup(PropertyGroup):
    vrm_name: StringProperty(  # type: ignore[valid-type]
        name="Name"
    )
    joints: CollectionProperty(  # type: ignore[valid-type]
        type=SpringBone1JointPropertyGroup
    )
    collider_groups: CollectionProperty(  # type: ignore[valid-type]
        type=SpringBone1ColliderGroupReferencePropertyGroup,
    )
    center: PointerProperty(  # type: ignore[valid-type]
        type=BonePropertyGroup,
    )

    # 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_joint_index: IntProperty(min=0)  # type: ignore[valid-type]
    active_collider_group_index: IntProperty(min=0)  # type: ignore[valid-type]

    animation_state: PointerProperty(  # type: ignore[valid-type]
        type=SpringBone1SpringAnimationStatePropertyGroup
    )

    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]
        joints: CollectionPropertyProtocol[  # type: ignore[no-redef]
            SpringBone1JointPropertyGroup
        ]
        collider_groups: CollectionPropertyProtocol[  # type: ignore[no-redef]
            SpringBone1ColliderGroupReferencePropertyGroup
        ]
        center: BonePropertyGroup  # type: ignore[no-redef]
        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_joint_index: int  # type: ignore[no-redef]
        active_collider_group_index: int  # type: ignore[no-redef]
        animation_state: (  # type: ignore[no-redef]
            SpringBone1SpringAnimationStatePropertyGroup
        )


# https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_springBone-1.0-beta/schema/VRMC_springBone.schema.json
class SpringBone1SpringBonePropertyGroup(PropertyGroup):
    colliders: CollectionProperty(  # type: ignore[valid-type]
        type=SpringBone1ColliderPropertyGroup,
    )
    collider_groups: CollectionProperty(  # type: ignore[valid-type]
        type=SpringBone1ColliderGroupPropertyGroup,
    )
    springs: CollectionProperty(  # type: ignore[valid-type]
        type=SpringBone1SpringPropertyGroup,
    )

    def update_enable_animation(self, _context: Context) -> None:
        for spring in self.springs:
            for joint in spring.joints:
                joint.animation_state.initialized_as_tail = False

    enable_animation: BoolProperty(  # type: ignore[valid-type]
        name="Enable Animation",
        update=update_enable_animation,
    )

    # for UI
    show_expanded_colliders: BoolProperty(  # type: ignore[valid-type]
        name="Spring Bone Colliders"
    )
    show_expanded_collider_groups: BoolProperty(  # type: ignore[valid-type]
        name="Spring Bone Collider Groups"
    )
    show_expanded_springs: BoolProperty(  # type: ignore[valid-type]
        name="Spring Bone Springs"
    )

    active_collider_index: IntProperty(min=0)  # type: ignore[valid-type]
    active_collider_group_index: IntProperty(min=0)  # type: ignore[valid-type]
    active_spring_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.
        colliders: CollectionPropertyProtocol[  # type: ignore[no-redef]
            SpringBone1ColliderPropertyGroup
        ]
        collider_groups: CollectionPropertyProtocol[  # type: ignore[no-redef]
            SpringBone1ColliderGroupPropertyGroup
        ]
        springs: CollectionPropertyProtocol[  # type: ignore[no-redef]
            SpringBone1SpringPropertyGroup
        ]
        enable_animation: bool  # type: ignore[no-redef]
        show_expanded_colliders: bool  # type: ignore[no-redef]
        show_expanded_collider_groups: bool  # type: ignore[no-redef]
        show_expanded_springs: bool  # type: ignore[no-redef]
        active_collider_index: int  # type: ignore[no-redef]
        active_collider_group_index: int  # type: ignore[no-redef]
        active_spring_index: int  # type: ignore[no-redef]


def get_armature_spring_bone1_extension(
    armature: Armature,
) -> SpringBone1SpringBonePropertyGroup:
    from ..extension import get_armature_extension

    ext = get_armature_extension(armature)
    spring_bone1: SpringBone1SpringBonePropertyGroup = ext.spring_bone1
    return spring_bone1
