# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
import json
from typing import Optional, Union

import bpy
from bpy.types import (
    Bone,
    CopyRotationConstraint,
    DampedTrackConstraint,
    EditBone,
    Image,
    Mesh,
    Object,
    PoseBone,
)
from mathutils import Matrix, Vector

from ..common import convert, ops, shader
from ..common.convert import Json
from ..common.logger import get_logger
from ..common.preferences import get_preferences
from ..common.progress import PartialProgress
from ..common.version import get_addon_version
from ..common.vrm1 import human_bone as vrm1_human_bone
from ..common.vrm1.human_bone import HumanBoneName, HumanBoneSpecifications
from ..editor import make_armature, migration
from ..editor.extension import (
    VrmAddonBoneExtensionPropertyGroup as BoneExtension,
)
from ..editor.extension import (
    get_armature_extension,
    get_bone_extension,
    get_material_extension,
    get_object_extension,
)
from ..editor.make_armature import (
    connect_parent_tail_and_child_head_if_very_close_position,
)
from ..editor.mtoon1.property_group import (
    Mtoon1KhrTextureTransformPropertyGroup,
    Mtoon1SamplerPropertyGroup,
    Mtoon1TextureInfoPropertyGroup,
    Mtoon1TexturePropertyGroup,
)
from ..editor.spring_bone1.property_group import (
    SpringBone1ColliderPropertyGroup,
    SpringBone1SpringBonePropertyGroup,
)
from ..editor.vrm1.property_group import (
    Vrm1ExpressionPropertyGroup,
    Vrm1ExpressionsPropertyGroup,
    Vrm1FirstPersonPropertyGroup,
    Vrm1HumanBonesPropertyGroup,
    Vrm1HumanoidPropertyGroup,
    Vrm1LookAtPropertyGroup,
    Vrm1MaterialColorBindPropertyGroup,
    Vrm1MetaPropertyGroup,
)
from .abstract_base_vrm_importer import AbstractBaseVrmImporter

logger = get_logger(__name__)


class Vrm1Importer(AbstractBaseVrmImporter):
    @staticmethod
    def assign_texture_colorspace(image: Image, preferred_colorspace: str) -> None:
        colorspaces = [preferred_colorspace]
        if preferred_colorspace == "Non-Color":
            # https://github.com/saturday06/VRM-Addon-for-Blender/issues/336#issuecomment-1760729404
            colorspaces.extend(["Linear", "Generic Data"])

        colorspace_settings = image.colorspace_settings
        exceptions: list[Exception] = []
        for colorspace in colorspaces:
            # The values that can be set in colorspace_settings.name vary
            # depending on the Blender setup. For example, bpy 3.6.0 from pypi
            # can only choose Linear or sRGB.
            # To detect this, we would ideally want to reference
            # colorspace_settings.bl_rna.properties.get("name").enum_items
            # etc., but it cannot be used because it crashes in Blender 2.93 etc.
            # So we catch exceptions and try until assignment succeeds.
            try:
                colorspace_settings.name = colorspace
            except TypeError as e:
                exceptions.append(e)
            else:
                return

        logger.error(
            "image.colorspace_settings.name doesn't support %s:\n%s",
            colorspaces,
            "\n".join(map(str, exceptions)),
        )

    def assign_texture(
        self,
        texture: Mtoon1TexturePropertyGroup,
        texture_dict: dict[str, Json],
    ) -> None:
        source = texture_dict.get("source")
        if isinstance(source, int):
            image = self.images.get(source)
            if image:
                texture.source = image
                self.assign_texture_colorspace(image, texture.colorspace)

        sampler = texture_dict.get("sampler")
        samplers = self.parse_result.json_dict.get("samplers")
        if not isinstance(sampler, int) or not isinstance(samplers, list):
            return
        if not 0 <= sampler < len(samplers):
            return

        sampler_dict = samplers[sampler]
        if not isinstance(sampler_dict, dict):
            return

        mag_filter = sampler_dict.get("magFilter")
        if isinstance(mag_filter, int):
            texture.sampler.mag_filter = (
                Mtoon1SamplerPropertyGroup.mag_filter_enum.value_to_identifier(
                    mag_filter, Mtoon1SamplerPropertyGroup.MAG_FILTER_DEFAULT.identifier
                )
            )

        min_filter = sampler_dict.get("minFilter")
        if isinstance(min_filter, int):
            texture.sampler.min_filter = (
                Mtoon1SamplerPropertyGroup.min_filter_enum.value_to_identifier(
                    min_filter, Mtoon1SamplerPropertyGroup.MIN_FILTER_DEFAULT.identifier
                )
            )

        wrap_s = sampler_dict.get("wrapS")
        if isinstance(wrap_s, int):
            texture.sampler.wrap_s = (
                Mtoon1SamplerPropertyGroup.wrap_enum.value_to_identifier(
                    wrap_s, Mtoon1SamplerPropertyGroup.WRAP_DEFAULT.identifier
                )
            )

        wrap_t = sampler_dict.get("wrapT")
        if isinstance(wrap_t, int):
            texture.sampler.wrap_t = (
                Mtoon1SamplerPropertyGroup.wrap_enum.value_to_identifier(
                    wrap_t, Mtoon1SamplerPropertyGroup.WRAP_DEFAULT.identifier
                )
            )

    def assign_khr_texture_transform(
        self,
        khr_texture_transform: Mtoon1KhrTextureTransformPropertyGroup,
        khr_texture_transform_dict: dict[str, Json],
    ) -> None:
        offset = convert.float2_or_none(khr_texture_transform_dict.get("offset"))
        if offset:
            khr_texture_transform.offset = offset

        scale = convert.float2_or_none(khr_texture_transform_dict.get("scale"))
        if scale:
            khr_texture_transform.scale = scale

    def assign_texture_info(
        self,
        texture_info: Mtoon1TextureInfoPropertyGroup,
        texture_info_dict: dict[str, Json],
    ) -> None:
        index = texture_info_dict.get("index")
        if isinstance(index, int):
            texture_dicts = self.parse_result.json_dict.get("textures")
            if isinstance(texture_dicts, list) and 0 <= index < len(texture_dicts):
                texture_dict = texture_dicts[index]
                if isinstance(texture_dict, dict):
                    self.assign_texture(texture_info.index, texture_dict)

        extensions_dict = texture_info_dict.get("extensions")
        if not isinstance(extensions_dict, dict):
            return

        khr_texture_transform_dict = extensions_dict.get("KHR_texture_transform")
        if not isinstance(khr_texture_transform_dict, dict):
            return

        self.assign_khr_texture_transform(
            texture_info.extensions.khr_texture_transform,
            khr_texture_transform_dict,
        )

    def make_mtoon1_material(
        self, material_index: int, gltf_dict: dict[str, Json]
    ) -> None:
        extensions_dict = gltf_dict.get("extensions")
        if not isinstance(extensions_dict, dict):
            return

        mtoon_dict = extensions_dict.get("VRMC_materials_mtoon")
        if not isinstance(mtoon_dict, dict):
            return

        material = self.materials.get(material_index)
        if not material:
            name = gltf_dict.get("name")
            if not isinstance(name, str):
                name = "Material"
            material = self.context.blend_data.materials.new(name)
        self.reset_material(material)
        material.use_backface_culling = True

        gltf = get_material_extension(material).mtoon1
        gltf.addon_version = get_addon_version()
        gltf.enabled = True
        mtoon = gltf.extensions.vrmc_materials_mtoon

        pbr_metallic_roughness_dict = gltf_dict.get("pbrMetallicRoughness")
        if isinstance(pbr_metallic_roughness_dict, dict):
            base_color_factor = shader.rgba_or_none(
                pbr_metallic_roughness_dict.get("baseColorFactor")
            )
            if base_color_factor:
                gltf.pbr_metallic_roughness.base_color_factor = base_color_factor

            base_color_texture_dict = pbr_metallic_roughness_dict.get(
                "baseColorTexture"
            )
            if isinstance(base_color_texture_dict, dict):
                self.assign_texture_info(
                    gltf.pbr_metallic_roughness.base_color_texture,
                    base_color_texture_dict,
                )

        alpha_mode = gltf_dict.get("alphaMode")
        if (
            isinstance(alpha_mode, str)
            and alpha_mode in gltf.alpha_mode_enum.identifiers()
        ):
            gltf.alpha_mode = alpha_mode

        alpha_cutoff = gltf_dict.get("alphaCutoff")
        if isinstance(alpha_cutoff, (float, int)):
            gltf.alpha_cutoff = float(alpha_cutoff)

        double_sided = gltf_dict.get("doubleSided")
        if isinstance(double_sided, bool):
            gltf.double_sided = double_sided

        normal_texture_dict = gltf_dict.get("normalTexture")
        if isinstance(normal_texture_dict, dict):
            self.assign_texture_info(
                gltf.normal_texture,
                normal_texture_dict,
            )
            normal_texture_scale = normal_texture_dict.get("scale")
            if isinstance(normal_texture_scale, (float, int)):
                gltf.normal_texture.scale = float(normal_texture_scale)

        emissive_factor = shader.rgb_or_none(gltf_dict.get("emissiveFactor"))
        if emissive_factor:
            gltf.emissive_factor = emissive_factor

        emissive_texture_dict = gltf_dict.get("emissiveTexture")
        if isinstance(emissive_texture_dict, dict):
            self.assign_texture_info(
                gltf.emissive_texture,
                emissive_texture_dict,
            )

        emissive_strength_dict = extensions_dict.get("KHR_materials_emissive_strength")
        if isinstance(emissive_strength_dict, dict):
            emissive_strength = convert.float_or_none(
                emissive_strength_dict.get("emissiveStrength")
            )
            if emissive_strength is not None:
                gltf.extensions.khr_materials_emissive_strength.emissive_strength = (
                    emissive_strength
                )

        shade_color_factor = shader.rgb_or_none(mtoon_dict.get("shadeColorFactor"))
        if shade_color_factor:
            mtoon.shade_color_factor = shade_color_factor

        shade_multiply_texture_dict = mtoon_dict.get("shadeMultiplyTexture")
        if isinstance(shade_multiply_texture_dict, dict):
            self.assign_texture_info(
                mtoon.shade_multiply_texture,
                shade_multiply_texture_dict,
            )

        transparent_with_z_write = mtoon_dict.get("transparentWithZWrite")
        if isinstance(transparent_with_z_write, bool):
            mtoon.transparent_with_z_write = transparent_with_z_write

        render_queue_offset_number = mtoon_dict.get("renderQueueOffsetNumber")
        if isinstance(render_queue_offset_number, (float, int)):
            mtoon.render_queue_offset_number = max(
                -9, min(9, int(render_queue_offset_number))
            )

        shading_shift_factor = mtoon_dict.get("shadingShiftFactor")
        if isinstance(shading_shift_factor, (float, int)):
            mtoon.shading_shift_factor = float(shading_shift_factor)

        shading_shift_texture_dict = mtoon_dict.get("shadingShiftTexture")
        if isinstance(shading_shift_texture_dict, dict):
            self.assign_texture_info(
                mtoon.shading_shift_texture,
                shading_shift_texture_dict,
            )
            shading_shift_texture_scale = shading_shift_texture_dict.get("scale")
            if isinstance(shading_shift_texture_scale, (float, int)):
                mtoon.shading_shift_texture.scale = float(shading_shift_texture_scale)

        shading_toony_factor = mtoon_dict.get("shadingToonyFactor")
        if isinstance(shading_toony_factor, (float, int)):
            mtoon.shading_toony_factor = float(shading_toony_factor)

        gi_equalization_factor = mtoon_dict.get("giEqualizationFactor")
        if isinstance(gi_equalization_factor, (float, int)):
            mtoon.gi_equalization_factor = float(gi_equalization_factor)

        matcap_factor = shader.rgb_or_none(mtoon_dict.get("matcapFactor"))
        if matcap_factor:
            mtoon.matcap_factor = matcap_factor

        matcap_texture_dict = mtoon_dict.get("matcapTexture")
        if isinstance(matcap_texture_dict, dict):
            self.assign_texture_info(
                mtoon.matcap_texture,
                matcap_texture_dict,
            )

        parametric_rim_color_factor = shader.rgb_or_none(
            mtoon_dict.get("parametricRimColorFactor")
        )
        if parametric_rim_color_factor:
            mtoon.parametric_rim_color_factor = parametric_rim_color_factor

        parametric_rim_fresnel_power_factor = mtoon_dict.get(
            "parametricRimFresnelPowerFactor"
        )
        if isinstance(parametric_rim_fresnel_power_factor, (float, int)):
            mtoon.parametric_rim_fresnel_power_factor = float(
                parametric_rim_fresnel_power_factor
            )

        parametric_rim_lift_factor = mtoon_dict.get("parametricRimLiftFactor")
        if isinstance(parametric_rim_lift_factor, (float, int)):
            mtoon.parametric_rim_lift_factor = float(parametric_rim_lift_factor)

        rim_multiply_texture_dict = mtoon_dict.get("rimMultiplyTexture")
        if isinstance(rim_multiply_texture_dict, dict):
            self.assign_texture_info(
                mtoon.rim_multiply_texture,
                rim_multiply_texture_dict,
            )

        rim_lighting_mix_factor = mtoon_dict.get("rimLightingMixFactor")
        if isinstance(rim_lighting_mix_factor, (float, int)):
            mtoon.rim_lighting_mix_factor = float(rim_lighting_mix_factor)

        mtoon.enable_outline_preview = get_preferences(
            self.context
        ).enable_mtoon_outline_preview

        outline_width_mode = mtoon_dict.get("outlineWidthMode")
        if outline_width_mode in mtoon.outline_width_mode_enum.identifiers():
            mtoon.outline_width_mode = outline_width_mode

        outline_width_factor = mtoon_dict.get("outlineWidthFactor")
        if isinstance(outline_width_factor, (float, int)):
            mtoon.outline_width_factor = float(outline_width_factor)

        outline_width_multiply_texture_dict = mtoon_dict.get(
            "outlineWidthMultiplyTexture"
        )
        if isinstance(outline_width_multiply_texture_dict, dict):
            self.assign_texture_info(
                mtoon.outline_width_multiply_texture,
                outline_width_multiply_texture_dict,
            )

        outline_color_factor = shader.rgb_or_none(mtoon_dict.get("outlineColorFactor"))
        if outline_color_factor:
            mtoon.outline_color_factor = outline_color_factor

        outline_lighting_mix_factor = mtoon_dict.get("outlineLightingMixFactor")
        if isinstance(outline_lighting_mix_factor, (float, int)):
            mtoon.outline_lighting_mix_factor = float(outline_lighting_mix_factor)

        uv_animation_mask_texture_dict = mtoon_dict.get("uvAnimationMaskTexture")
        if isinstance(uv_animation_mask_texture_dict, dict):
            self.assign_texture_info(
                mtoon.uv_animation_mask_texture,
                uv_animation_mask_texture_dict,
            )

        uv_animation_rotation_speed_factor = mtoon_dict.get(
            "uvAnimationRotationSpeedFactor"
        )
        if isinstance(uv_animation_rotation_speed_factor, (float, int)):
            mtoon.uv_animation_rotation_speed_factor = float(
                uv_animation_rotation_speed_factor
            )

        uv_animation_scroll_x_speed_factor = mtoon_dict.get(
            "uvAnimationScrollXSpeedFactor"
        )
        if isinstance(uv_animation_scroll_x_speed_factor, (float, int)):
            mtoon.uv_animation_scroll_x_speed_factor = float(
                uv_animation_scroll_x_speed_factor
            )

        uv_animation_scroll_y_speed_factor = mtoon_dict.get(
            "uvAnimationScrollYSpeedFactor"
        )
        if isinstance(uv_animation_scroll_y_speed_factor, (float, int)):
            mtoon.uv_animation_scroll_y_speed_factor = float(
                uv_animation_scroll_y_speed_factor
            )

    def load_materials(self, progress: PartialProgress) -> None:
        material_dicts = self.parse_result.json_dict.get("materials")
        if not isinstance(material_dicts, list):
            progress.update(1)
            return
        for index, material_dict in enumerate(material_dicts):
            if isinstance(material_dict, dict):
                self.make_mtoon1_material(index, material_dict)
            progress.update(float(index) / len(material_dicts))
        progress.update(1)

    def find_vrm1_bone_node_indices(self) -> list[int]:
        result: list[int] = []

        vrm1_dict = self.parse_result.vrm1_extension_dict
        if not isinstance(vrm1_dict, dict):
            return result

        humanoid_dict = vrm1_dict.get("humanoid")
        if not isinstance(humanoid_dict, dict):
            return result

        human_bones_dict = humanoid_dict.get("humanBones")
        if not isinstance(human_bones_dict, dict):
            return result

        for human_bone_dict in human_bones_dict.values():
            if not isinstance(human_bone_dict, dict):
                continue
            node_index = human_bone_dict.get("node")
            if isinstance(node_index, int):
                result.append(node_index)

        return list(dict.fromkeys(result))  # Remove duplicates

    def find_spring_bone1_bone_node_indices(self) -> list[int]:
        result: list[int] = []

        extensions_dict = self.parse_result.json_dict.get("extensions")
        if not isinstance(extensions_dict, dict):
            return result

        spring_bone1_dict = extensions_dict.get("VRMC_springBone")
        if not isinstance(spring_bone1_dict, dict):
            return result

        collider_dicts = spring_bone1_dict.get("colliders")
        if isinstance(collider_dicts, list):
            for collider_dict in collider_dicts:
                if not isinstance(collider_dict, dict):
                    continue

                node_index = collider_dict.get("node")
                if isinstance(node_index, int):
                    result.append(node_index)

        spring_dicts = spring_bone1_dict.get("springs")
        if isinstance(spring_dicts, list):
            for spring_dict in spring_dicts:
                if not isinstance(spring_dict, dict):
                    continue

                center_index = spring_dict.get("center")
                if isinstance(center_index, int):
                    result.append(center_index)

                joint_dicts = spring_dict.get("joints")
                if not isinstance(joint_dicts, list):
                    continue

                for joint_dict in joint_dicts:
                    if not isinstance(joint_dict, dict):
                        continue
                    node_index = joint_dict.get("node")
                    if isinstance(node_index, int):
                        result.append(node_index)

        return list(dict.fromkeys(result))  # Remove duplicates

    def find_vrm_bone_node_indices(self) -> list[int]:
        # TODO: SkinnedMeshRenderer root <=> skin.skeleton???
        return list(
            dict.fromkeys(  # Remove duplicates
                self.find_vrm1_bone_node_indices()
                + self.find_spring_bone1_bone_node_indices()
            )
        )

    def setup_vrm1_humanoid_bones(self) -> None:
        armature = self.armature
        if not armature:
            return
        addon_extension = get_armature_extension(self.armature_data)

        Vrm1HumanBonesPropertyGroup.update_all_node_candidates(
            self.context, self.armature_data.name, force=True
        )

        human_bones = addon_extension.vrm1.humanoid.human_bones
        for name, human_bone in human_bones.human_bone_name_to_human_bone().items():
            if (
                human_bone.node.bone_name
                and human_bone.node.bone_name not in human_bone.node_candidates
            ):
                # Invalid bone structure
                return

            spec = HumanBoneSpecifications.get(name)
            if spec.requirement and not human_bone.node.bone_name:
                # Missing required bones
                return

        with self.save_bone_child_object_transforms(
            self.context, armature
        ) as armature_data:
            bone_name_to_human_bone_name: dict[str, HumanBoneName] = {}
            for (
                human_bone_name,
                human_bone,
            ) in human_bones.human_bone_name_to_human_bone().items():
                if not human_bone.node.bone_name:
                    continue
                bone_name_to_human_bone_name[human_bone.node.bone_name] = (
                    human_bone_name
                )

            # When a bone has multiple children
            # Create a dict to pick the child bone name to point the tail to
            # from that bone name
            bone_name_to_main_child_bone_name: dict[str, str] = {}
            for (
                bone_name,
                human_bone_name,
            ) in bone_name_to_human_bone_name.items():
                # The current algorithm cannot handle
                #
                #   head ---- node ---- leftEye
                #                   \
                #                    -- rightEye
                #
                # well, so we don't process leftEye and rightEye
                if human_bone_name in [HumanBoneName.RIGHT_EYE, HumanBoneName.LEFT_EYE]:
                    continue

                bone = armature_data.edit_bones.get(bone_name)
                if not bone:
                    continue
                last_human_bone_name = human_bone_name
                while True:
                    parent = bone.parent
                    if not parent:
                        break
                    parent_human_bone_name = bone_name_to_human_bone_name.get(
                        parent.name
                    )

                    if parent_human_bone_name in [
                        HumanBoneName.RIGHT_HAND,
                        HumanBoneName.LEFT_HAND,
                    ]:
                        break

                    if (
                        parent_human_bone_name == HumanBoneName.UPPER_CHEST
                        and last_human_bone_name
                        not in [HumanBoneName.HEAD, HumanBoneName.NECK]
                    ):
                        break

                    if (
                        parent_human_bone_name == HumanBoneName.CHEST
                        and last_human_bone_name
                        not in [
                            HumanBoneName.HEAD,
                            HumanBoneName.NECK,
                            HumanBoneName.UPPER_CHEST,
                        ]
                    ):
                        break

                    if (
                        parent_human_bone_name == HumanBoneName.SPINE
                        and last_human_bone_name
                        not in [
                            HumanBoneName.HEAD,
                            HumanBoneName.NECK,
                            HumanBoneName.UPPER_CHEST,
                            HumanBoneName.CHEST,
                        ]
                    ):
                        break

                    if (
                        parent_human_bone_name == HumanBoneName.HIPS
                        and last_human_bone_name != HumanBoneName.SPINE
                    ):
                        break

                    if parent_human_bone_name:
                        last_human_bone_name = parent_human_bone_name

                    bone_name_to_main_child_bone_name[parent.name] = bone.name
                    bone = parent

            # Get human bones and their ancestor bones
            human_bone_tree_bone_names: set[str] = set()
            for bone_name in bone_name_to_human_bone_name:
                bone = armature_data.edit_bones.get(bone_name)
                while bone:
                    human_bone_tree_bone_names.add(bone.name)
                    bone = bone.parent

            bone_name_to_axis_translation: dict[str, str] = {}

            human_bone_tree_bones: list[EditBone] = []
            non_human_bone_tree_bones: list[EditBone] = []
            constraint_node_index_to_source_index: dict[int, int] = {}
            constraint_node_index_groups: list[set[int]] = []
            nodes = self.parse_result.json_dict.get("nodes")
            if not isinstance(nodes, list):
                nodes = []
            for node_index, node_dict in enumerate(nodes):
                if not isinstance(node_dict, dict):
                    continue

                node_extensions_dict = node_dict.get("extensions")
                if not isinstance(node_extensions_dict, dict):
                    continue

                vrmc_node_constraint_dict = node_extensions_dict.get(
                    "VRMC_node_constraint"
                )
                if not isinstance(vrmc_node_constraint_dict, dict):
                    continue

                constraint_dict = vrmc_node_constraint_dict.get("constraint")
                if not isinstance(constraint_dict, dict):
                    continue

                object_or_bone = self.get_object_or_bone_by_node_index(node_index)
                if not object_or_bone:
                    continue

                roll_dict = constraint_dict.get("roll")
                if isinstance(roll_dict, dict):
                    source = roll_dict.get("source")
                    if isinstance(source, int):
                        constraint_node_index_to_source_index[node_index] = source
                        continue

                aim_dict = constraint_dict.get("aim")
                if isinstance(aim_dict, dict):
                    source = aim_dict.get("source")
                    if isinstance(source, int):
                        constraint_node_index_to_source_index[node_index] = source
                        continue

                rotation_dict = constraint_dict.get("rotation")
                if isinstance(rotation_dict, dict):
                    source = rotation_dict.get("source")
                    if isinstance(source, int):
                        constraint_node_index_to_source_index[node_index] = source
                        continue

            node_indices = list(constraint_node_index_to_source_index.keys())
            while node_indices:
                node_index = node_indices.pop()
                source_index = constraint_node_index_to_source_index.get(node_index)
                if not isinstance(source_index, int):
                    continue
                node_indices.append(source_index)
                found = False
                for constraint_node_index_group in constraint_node_index_groups:
                    if node_index in constraint_node_index_group:
                        constraint_node_index_group.add(source_index)
                        found = True
                        break
                    if source_index in constraint_node_index_group:
                        constraint_node_index_group.add(node_index)
                        found = True
                        break

                if not found:
                    constraint_node_index_groups.append({node_index, source_index})

            # When constraints are attached during axis conversion, we want to
            # prioritize human bones and their ancestor bones, so we process
            # them first in depth-first order, then process other bones in
            # depth-first order
            unsorted_bones = [
                bone for bone in armature_data.edit_bones if not bone.parent
            ]
            while unsorted_bones:
                bone = unsorted_bones.pop()
                unsorted_bones.extend(bone.children)
                if bone.name in human_bone_tree_bone_names:
                    human_bone_tree_bones.append(bone)
                else:
                    non_human_bone_tree_bones.append(bone)

            for bone in human_bone_tree_bones + non_human_bone_tree_bones:
                bone_index = next(
                    (i for i, n in self.bone_names.items() if n == bone.name), None
                )
                if isinstance(bone_index, int):
                    group_axis_translation: Optional[str] = None
                    for node_index in next(
                        (g for g in constraint_node_index_groups if bone_index in g),
                        set[int](),
                    ):
                        object_or_bone = self.get_object_or_bone_by_node_index(
                            node_index
                        )
                        if isinstance(object_or_bone, PoseBone):
                            group_axis_translation = bone_name_to_axis_translation.get(
                                object_or_bone.name
                            )
                            if group_axis_translation is not None:
                                break
                    if group_axis_translation is not None:
                        bone.matrix = BoneExtension.translate_axis(
                            bone.matrix,
                            group_axis_translation,
                        )
                        if bone.parent and not bone.children:
                            bone.length = max(
                                bone.parent.length / 2, make_armature.MIN_BONE_LENGTH
                            )
                        bone_name_to_axis_translation[bone.name] = (
                            group_axis_translation
                        )
                        continue

                main_child_bone_name = bone_name_to_main_child_bone_name.get(bone.name)
                target_vector: Optional[Vector] = None
                if main_child_bone_name:
                    main_child = next(
                        bone
                        for bone in bone.children
                        if bone.name == main_child_bone_name
                    )
                    target_translation = (
                        armature.matrix_world @ Matrix.Translation(main_child.head)
                    ).to_translation()
                    base_translation = (
                        armature.matrix_world @ Matrix.Translation(bone.head)
                    ).to_translation()
                    target_vector = target_translation - base_translation
                elif bone.children:
                    target_translation = Vector((0.0, 0.0, 0.0))
                    for child in bone.children:
                        child_translation = (
                            armature.matrix_world @ Matrix.Translation(child.head)
                        ).to_translation()
                        target_translation += child_translation
                    target_translation /= len(bone.children)
                    base_translation = (
                        armature.matrix_world @ Matrix.Translation(bone.head)
                    ).to_translation()
                    target_vector = target_translation - base_translation
                elif not bone.parent:
                    bone_name_to_axis_translation[bone.name] = (
                        BoneExtension.AXIS_TRANSLATION_NONE.identifier
                    )
                    continue
                elif bone_name_to_human_bone_name.get(bone.name) in [
                    HumanBoneName.RIGHT_EYE,
                    HumanBoneName.LEFT_EYE,
                    HumanBoneName.RIGHT_FOOT,
                    HumanBoneName.LEFT_FOOT,
                ]:
                    target_vector = Vector((0, -1, 0))
                else:
                    target_translation = (
                        armature.matrix_world @ Matrix.Translation(bone.head)
                    ).to_translation()
                    base_translation = (
                        armature.matrix_world @ Matrix.Translation(bone.parent.head)
                    ).to_translation()
                    target_vector = target_translation - base_translation

                base_rotation = (armature.matrix_world @ bone.matrix).to_quaternion()

                x_vector = Vector((1, 0, 0))
                x_vector.rotate(base_rotation)
                x_negated_vector = x_vector.copy()
                x_negated_vector.negate()
                y_vector = Vector((0, 1, 0))
                y_vector.rotate(base_rotation)
                y_negated_vector = y_vector.copy()
                y_negated_vector.negate()
                z_vector = Vector((0, 0, 1))
                z_vector.rotate(base_rotation)
                z_negated_vector = z_vector.copy()
                z_negated_vector.negate()

                bone_length_and_axis_translations: list[tuple[float, str]] = [
                    (
                        target_vector.dot(x_vector),
                        BoneExtension.AXIS_TRANSLATION_X_TO_Y.identifier,
                    ),
                    (
                        target_vector.dot(y_vector),
                        BoneExtension.AXIS_TRANSLATION_NONE.identifier,
                    ),
                    (
                        target_vector.dot(z_vector),
                        BoneExtension.AXIS_TRANSLATION_Z_TO_Y.identifier,
                    ),
                    (
                        target_vector.dot(x_negated_vector),
                        BoneExtension.AXIS_TRANSLATION_MINUS_X_TO_Y.identifier,
                    ),
                    (
                        target_vector.dot(y_negated_vector),
                        BoneExtension.AXIS_TRANSLATION_MINUS_Y_TO_Y_AROUND_Z.identifier,
                    ),
                    (
                        target_vector.dot(z_negated_vector),
                        BoneExtension.AXIS_TRANSLATION_MINUS_Z_TO_Y.identifier,
                    ),
                ]
                bone_length, axis_translation = sorted(
                    bone_length_and_axis_translations, reverse=True, key=lambda x: x[0]
                )[0]
                bone.matrix = BoneExtension.translate_axis(
                    bone.matrix,
                    axis_translation,
                )
                if bone.children:
                    bone.length = max(bone_length, make_armature.MIN_BONE_LENGTH)
                elif bone.parent:
                    bone.length = max(
                        bone.parent.length / 2, make_armature.MIN_BONE_LENGTH
                    )
                bone_name_to_axis_translation[bone.name] = axis_translation

            connect_parent_tail_and_child_head_if_very_close_position(armature_data)

            bpy.ops.object.mode_set(mode="OBJECT")
            for bone_name, axis_translation in bone_name_to_axis_translation.items():
                data_bone = armature_data.bones.get(bone_name)
                if not data_bone:
                    continue
                get_bone_extension(
                    data_bone
                ).axis_translation = BoneExtension.reverse_axis_translation(
                    axis_translation
                )

            for node_index in set(constraint_node_index_to_source_index.keys()) | set(
                constraint_node_index_to_source_index.values()
            ):
                object_or_bone = self.get_object_or_bone_by_node_index(node_index)
                if not isinstance(object_or_bone, Object):
                    continue
                for group_node_index in next(
                    (g for g in constraint_node_index_groups if node_index in g),
                    set[int](),
                ):
                    group_object_or_bone = self.get_object_or_bone_by_node_index(
                        group_node_index
                    )
                    if isinstance(group_object_or_bone, PoseBone):
                        self.translate_object_axis(
                            object_or_bone,
                            BoneExtension.reverse_axis_translation(
                                get_bone_extension(
                                    group_object_or_bone.bone
                                ).axis_translation
                            ),
                        )
                        break

    def translate_object_axis(self, obj: Object, axis_translation: str) -> None:
        if axis_translation != BoneExtension.AXIS_TRANSLATION_AUTO.identifier:
            return
        matrix = BoneExtension.translate_axis(obj.matrix_world, axis_translation)
        location, rotation, _ = matrix.decompose()
        matrix = Matrix.Translation(location) @ rotation.to_matrix().to_4x4()
        bpy.ops.object.select_all(action="DESELECT")
        self.context.scene.cursor.matrix = matrix
        obj.select_set(True)
        bpy.ops.object.origin_set(type="ORIGIN_CURSOR", center="MEDIAN")

        get_object_extension(
            obj
        ).axis_translation = BoneExtension.reverse_axis_translation(axis_translation)

    def load_gltf_extensions(self) -> None:
        armature = self.armature
        if not armature:
            return
        addon_extension = get_armature_extension(self.armature_data)
        vrm1 = addon_extension.vrm1

        addon_extension.spec_version = addon_extension.SPEC_VERSION_VRM1
        vrm1_extension_dict = self.parse_result.vrm1_extension_dict

        addon_extension.addon_version = get_addon_version()

        textblock = self.context.blend_data.texts.new(name="vrm.json")
        textblock.write(json.dumps(self.parse_result.json_dict, indent=4))

        self.load_vrm1_meta(vrm1.meta, vrm1_extension_dict.get("meta"))
        self.load_vrm1_humanoid(vrm1.humanoid, vrm1_extension_dict.get("humanoid"))
        self.setup_vrm1_humanoid_bones()
        self.load_vrm1_first_person(
            vrm1.first_person, vrm1_extension_dict.get("firstPerson")
        )
        self.load_vrm1_look_at(vrm1.look_at, vrm1_extension_dict.get("lookAt"))
        self.load_vrm1_expressions(
            vrm1.expressions, vrm1_extension_dict.get("expressions")
        )

        extensions_dict = self.parse_result.json_dict.get("extensions")
        if isinstance(extensions_dict, dict):
            self.load_spring_bone1(
                addon_extension.spring_bone1, extensions_dict.get("VRMC_springBone")
            )

        self.load_node_constraint1()
        migration.migrate(self.context, armature.name)

    def load_vrm1_meta(self, meta: Vrm1MetaPropertyGroup, meta_dict: Json) -> None:
        if not isinstance(meta_dict, dict):
            return

        name = meta_dict.get("name")
        if name is not None:
            meta.vrm_name = str(name)

        version = meta_dict.get("version")
        if version is not None:
            meta.version = str(version)

        authors = meta_dict.get("authors")
        if isinstance(authors, list):
            for author in authors:
                if author is not None:
                    meta.authors.add().value = str(author)

        copyright_information = meta_dict.get("copyrightInformation")
        if copyright_information is not None:
            meta.copyright_information = str(copyright_information)

        contact_information = meta_dict.get("contactInformation")
        if contact_information is not None:
            meta.contact_information = str(contact_information)

        references = meta_dict.get("references")
        if isinstance(references, list):
            for reference in references:
                if reference is not None:
                    meta.references.add().value = str(reference)

        third_party_licenses = meta_dict.get("thirdPartyLicenses")
        if third_party_licenses is not None:
            meta.third_party_licenses = str(third_party_licenses)

        thumbnail_image_index = meta_dict.get("thumbnailImage")
        if isinstance(thumbnail_image_index, int):
            thumbnail_image = self.images.get(thumbnail_image_index)
            if thumbnail_image:
                meta.thumbnail_image = thumbnail_image

        avatar_permission = meta_dict.get("avatarPermission")
        if (
            isinstance(avatar_permission, str)
            and avatar_permission
            in Vrm1MetaPropertyGroup.avatar_permission_enum.identifiers()
        ):
            meta.avatar_permission = avatar_permission

        meta.allow_excessively_violent_usage = bool(
            meta_dict.get("allowExcessivelyViolentUsage")
        )

        meta.allow_excessively_sexual_usage = bool(
            meta_dict.get("allowExcessivelySexualUsage")
        )

        commercial_usage = meta_dict.get("commercialUsage")
        if (
            isinstance(commercial_usage, str)
            and commercial_usage
            in Vrm1MetaPropertyGroup.commercial_usage_enum.identifiers()
        ):
            meta.commercial_usage = commercial_usage

        meta.allow_political_or_religious_usage = bool(
            meta_dict.get("allowPoliticalOrReligiousUsage")
        )

        meta.allow_antisocial_or_hate_usage = bool(
            meta_dict.get("allowAntisocialOrHateUsage")
        )

        credit_notation = meta_dict.get("creditNotation")
        if (
            isinstance(credit_notation, str)
            and credit_notation
            in Vrm1MetaPropertyGroup.credit_notation_enum.identifiers()
        ):
            meta.credit_notation = credit_notation

        meta.allow_redistribution = bool(meta_dict.get("allowRedistribution"))

        modification = meta_dict.get("modification")
        if (
            isinstance(modification, str)
            and modification in Vrm1MetaPropertyGroup.modification_enum.identifiers()
        ):
            meta.modification = modification

        other_license_url = meta_dict.get("otherLicenseUrl")
        if other_license_url is not None:
            meta.other_license_url = str(other_license_url)

    def load_vrm1_humanoid(
        self, humanoid: Vrm1HumanoidPropertyGroup, humanoid_dict: Json
    ) -> None:
        if not isinstance(humanoid_dict, dict):
            return

        human_bones_dict = humanoid_dict.get("humanBones")
        if not isinstance(human_bones_dict, dict):
            return

        human_bone_name_to_human_bone = (
            humanoid.human_bones.human_bone_name_to_human_bone()
        )

        assigned_bone_names: list[str] = []
        for human_bone_name in [
            human_bone.name
            for human_bone in vrm1_human_bone.HumanBoneSpecifications.all_human_bones
        ]:
            human_bone_dict = human_bones_dict.get(human_bone_name.value)
            if not isinstance(human_bone_dict, dict):
                continue
            node_index = human_bone_dict.get("node")
            if not isinstance(node_index, int):
                continue
            bone_name = self.bone_names.get(node_index)
            if not isinstance(bone_name, str) or bone_name in assigned_bone_names:
                continue
            human_bone_name_to_human_bone[human_bone_name].node.bone_name = bone_name
            assigned_bone_names.append(bone_name)

    def load_vrm1_first_person(
        self,
        first_person: Vrm1FirstPersonPropertyGroup,
        first_person_dict: Json,
    ) -> None:
        if not isinstance(first_person_dict, dict):
            return

        mesh_annotation_dicts = first_person_dict.get("meshAnnotations")
        if not isinstance(mesh_annotation_dicts, list):
            mesh_annotation_dicts = []

        for mesh_annotation_dict in mesh_annotation_dicts:
            mesh_annotation = first_person.mesh_annotations.add()
            if not isinstance(mesh_annotation_dict, dict):
                continue

            node = mesh_annotation_dict.get("node")
            if isinstance(node, int):
                mesh_object_name = self.mesh_object_names.get(node)
                if isinstance(mesh_object_name, str):
                    mesh_annotation.node.mesh_object_name = mesh_object_name

            type_ = mesh_annotation_dict.get("type")
            if type_ in mesh_annotation.type_enum.identifiers():
                mesh_annotation.type = type_

    def load_vrm1_look_at(
        self,
        look_at: Vrm1LookAtPropertyGroup,
        look_at_dict: Json,
    ) -> None:
        if not isinstance(look_at_dict, dict):
            return
        armature = self.armature
        if armature is None:
            return
        offset_from_head_bone = Vector(
            convert.vrm_json_array_to_float_vector(
                look_at_dict.get("offsetFromHeadBone"), [0, 0, 0]
            )
        )

        humanoid = get_armature_extension(self.armature_data).vrm1.humanoid
        head_bone_name = humanoid.human_bones.head.node.bone_name
        head_bone = self.armature_data.bones.get(head_bone_name)
        if head_bone:
            offset_from_head_bone = (
                offset_from_head_bone
                @ BoneExtension.translate_axis(
                    Matrix(),
                    BoneExtension.reverse_axis_translation(
                        get_bone_extension(head_bone).axis_translation
                    ),
                )
            )

        look_at.offset_from_head_bone = offset_from_head_bone
        type_ = look_at_dict.get("type")
        if type_ in look_at.type_enum.identifiers():
            look_at.type = type_

        for range_map, range_map_dict in [
            (
                look_at.range_map_horizontal_inner,
                look_at_dict.get("rangeMapHorizontalInner"),
            ),
            (
                look_at.range_map_horizontal_outer,
                look_at_dict.get("rangeMapHorizontalOuter"),
            ),
            (
                look_at.range_map_vertical_down,
                look_at_dict.get("rangeMapVerticalDown"),
            ),
            (
                look_at.range_map_vertical_up,
                look_at_dict.get("rangeMapVerticalUp"),
            ),
        ]:
            if not isinstance(range_map_dict, dict):
                continue

            input_max_value = range_map_dict.get("inputMaxValue")
            if isinstance(input_max_value, (float, int)):
                range_map.input_max_value = float(input_max_value)

            output_scale = range_map_dict.get("outputScale")
            if isinstance(output_scale, (float, int)):
                range_map.output_scale = float(output_scale)

    def load_vrm1_expression(
        self,
        expression: Vrm1ExpressionPropertyGroup,
        expression_dict: Json,
    ) -> None:
        if not isinstance(expression_dict, dict):
            return

        morph_target_bind_dicts = expression_dict.get("morphTargetBinds")
        if not isinstance(morph_target_bind_dicts, list):
            morph_target_bind_dicts = []

        for morph_target_bind_dict in morph_target_bind_dicts:
            if not isinstance(morph_target_bind_dict, dict):
                continue

            morph_target_bind = expression.morph_target_binds.add()

            weight = convert.float_or(morph_target_bind_dict.get("weight"), 0.0)

            morph_target_bind.weight = weight

            node_index = morph_target_bind_dict.get("node")
            if not isinstance(node_index, int):
                continue
            node_dicts = self.parse_result.json_dict.get("nodes")
            if not isinstance(node_dicts, list) or not (
                0 <= node_index < len(node_dicts)
            ):
                continue
            node_dict = node_dicts[node_index]
            if not isinstance(node_dict, dict):
                continue
            mesh_index = node_dict.get("mesh")
            if not isinstance(mesh_index, int):
                continue
            mesh_obj = self.meshes.get(mesh_index)
            if not mesh_obj:
                continue

            morph_target_bind.node.mesh_object_name = mesh_obj.name
            index = morph_target_bind_dict.get("index")
            if not isinstance(index, int):
                continue
            mesh_data = mesh_obj.data
            if not isinstance(mesh_data, Mesh):
                continue
            shape_keys = mesh_data.shape_keys
            if not shape_keys:
                continue
            key_blocks = shape_keys.key_blocks
            if 1 <= (index + 1) < len(key_blocks):
                morph_target_bind.index = key_blocks[index + 1].name

        material_color_bind_dicts = expression_dict.get("materialColorBinds")
        if not isinstance(material_color_bind_dicts, list):
            material_color_bind_dicts = []

        for material_color_bind_dict in material_color_bind_dicts:
            material_color_bind = expression.material_color_binds.add()

            if not isinstance(material_color_bind_dict, dict):
                continue

            material_index = material_color_bind_dict.get("material")
            if isinstance(material_index, int):
                material = self.materials.get(material_index)
                if material:
                    material_color_bind.material = material

            type_ = material_color_bind_dict.get("type")
            if type_ in Vrm1MaterialColorBindPropertyGroup.type_enum.identifiers():
                material_color_bind.type = type_

            target_value = material_color_bind_dict.get("targetValue")
            if not isinstance(target_value, list):
                target_value = []
            target_value = [
                float(v) if isinstance(v, (float, int)) else 0.0 for v in target_value
            ]
            while len(target_value) < 4:
                target_value.append(0.0)
            material_color_bind.target_value = target_value[:4]

        texture_transform_bind_dicts = expression_dict.get("textureTransformBinds")
        if not isinstance(texture_transform_bind_dicts, list):
            texture_transform_bind_dicts = []
        for texture_transform_bind_dict in texture_transform_bind_dicts:
            texture_transform_bind = expression.texture_transform_binds.add()

            if not isinstance(texture_transform_bind_dict, dict):
                continue

            material_index = texture_transform_bind_dict.get("material")
            if isinstance(material_index, int):
                material = self.materials.get(material_index)
                if material:
                    texture_transform_bind.material = material

            texture_transform_bind.scale = convert.vrm_json_array_to_float_vector(
                texture_transform_bind_dict.get("scale"), [1, 1]
            )

            texture_transform_bind.offset = convert.vrm_json_array_to_float_vector(
                texture_transform_bind_dict.get("offset"), [0, 0]
            )

        is_binary = expression_dict.get("isBinary")
        if isinstance(is_binary, bool):
            expression.is_binary = is_binary

        override_blink = expression_dict.get("overrideBlink")
        if (
            isinstance(override_blink, str)
            and override_blink
            in Vrm1ExpressionPropertyGroup.expression_override_type_enum.identifiers()
        ):
            expression.override_blink = override_blink

        override_look_at = expression_dict.get("overrideLookAt")
        if (
            isinstance(override_look_at, str)
            and override_look_at
            in Vrm1ExpressionPropertyGroup.expression_override_type_enum.identifiers()
        ):
            expression.override_look_at = override_look_at

        override_mouth = expression_dict.get("overrideMouth")
        if (
            isinstance(override_mouth, str)
            and override_mouth
            in Vrm1ExpressionPropertyGroup.expression_override_type_enum.identifiers()
        ):
            expression.override_mouth = override_mouth

    def load_vrm1_expressions(
        self,
        expressions: Vrm1ExpressionsPropertyGroup,
        expressions_dict: Json,
    ) -> None:
        if not isinstance(expressions_dict, dict):
            return

        preset_dict = expressions_dict.get("preset")
        if isinstance(preset_dict, dict):
            for (
                preset_name,
                expression,
            ) in expressions.preset.name_to_expression_dict().items():
                self.load_vrm1_expression(expression, preset_dict.get(preset_name))

        custom_dict = expressions_dict.get("custom")
        if isinstance(custom_dict, dict):
            for custom_name, expression_dict in custom_dict.items():
                expression = expressions.custom.add()
                expression.custom_name = custom_name
                self.load_vrm1_expression(expression, expression_dict)

    def load_spring_bone1_collider(
        self,
        collider: SpringBone1ColliderPropertyGroup,
        collider_dict: dict[str, Json],
    ) -> None:
        bone: Optional[Bone] = None
        node_index = collider_dict.get("node")
        if isinstance(node_index, int):
            bone_name = self.bone_names.get(node_index)
            if isinstance(bone_name, str):
                collider.node.bone_name = bone_name
                collider_bpy_object = collider.bpy_object
                if collider_bpy_object:
                    collider_bpy_object.name = f"{bone_name} Collider"
                bone = self.armature_data.bones.get(collider.node.bone_name)

        shape_dict = collider_dict.get("shape")
        if not isinstance(shape_dict, dict):
            collider.shape.sphere.radius = 0.0001
            collider.shape.sphere.offset = (0, 0, -10000)
            return

        shape = collider.shape

        sphere_dict = shape_dict.get("sphere")
        capsule_dict = shape_dict.get("capsule")
        if isinstance(sphere_dict, dict):
            collider.shape_type = collider.SHAPE_TYPE_SPHERE.identifier
            offset = Vector(
                convert.vrm_json_array_to_float_vector(
                    sphere_dict.get("offset"), [0, 0, 0]
                )
            )
            if bone:
                axis_translation = get_bone_extension(bone).axis_translation
                offset = Vector(offset) @ BoneExtension.translate_axis(
                    Matrix(),
                    BoneExtension.reverse_axis_translation(axis_translation),
                )
            shape.sphere.offset = offset
            radius = sphere_dict.get("radius")
            if isinstance(radius, (float, int)):
                shape.sphere.radius = float(radius)
        elif isinstance(capsule_dict, dict):
            collider.shape_type = collider.SHAPE_TYPE_CAPSULE.identifier

            offset = Vector(
                convert.vrm_json_array_to_float_vector(
                    capsule_dict.get("offset"), [0, 0, 0]
                )
            )
            if bone:
                axis_translation = get_bone_extension(bone).axis_translation
                offset = Vector(offset) @ BoneExtension.translate_axis(
                    Matrix(),
                    BoneExtension.reverse_axis_translation(axis_translation),
                )
            shape.capsule.offset = offset

            radius = capsule_dict.get("radius")
            if isinstance(radius, (float, int)):
                shape.capsule.radius = float(radius)

            tail = Vector(
                convert.vrm_json_array_to_float_vector(
                    capsule_dict.get("tail"), [0, 0, 0]
                )
            )
            if bone:
                axis_translation = get_bone_extension(bone).axis_translation
                tail = Vector(tail) @ BoneExtension.translate_axis(
                    Matrix(),
                    BoneExtension.reverse_axis_translation(axis_translation),
                )
            shape.capsule.tail = tail

        extensions_dict = collider_dict.get("extensions")
        if not isinstance(extensions_dict, dict):
            return
        extended_collider_dict = extensions_dict.get(
            "VRMC_springBone_extended_collider"
        )
        if not isinstance(extended_collider_dict, dict):
            return
        extended_shape_dict = extended_collider_dict.get("shape")
        if not isinstance(extended_shape_dict, dict):
            return

        extended_collider = collider.extensions.vrmc_spring_bone_extended_collider
        extended_shape = extended_collider.shape
        extended_sphere_dict = extended_shape_dict.get("sphere")
        extended_capsule_dict = extended_shape_dict.get("capsule")
        extended_plane_dict = extended_shape_dict.get("plane")
        if isinstance(extended_sphere_dict, dict):
            extended_collider.shape_type = (
                extended_collider.SHAPE_TYPE_EXTENDED_SPHERE.identifier
            )
            offset = Vector(
                convert.vrm_json_array_to_float_vector(
                    extended_sphere_dict.get("offset"), [0, 0, 0]
                )
            )
            if bone:
                axis_translation = get_bone_extension(bone).axis_translation
                offset = Vector(offset) @ BoneExtension.translate_axis(
                    Matrix(),
                    BoneExtension.reverse_axis_translation(axis_translation),
                )
            extended_shape.sphere.offset = offset
            radius = extended_sphere_dict.get("radius")
            if isinstance(radius, (float, int)):
                extended_shape.sphere.radius = float(radius)

            inside = extended_sphere_dict.get("inside")
            if isinstance(inside, bool):
                extended_shape.sphere.inside = inside

        elif isinstance(extended_capsule_dict, dict):
            extended_collider.shape_type = (
                extended_collider.SHAPE_TYPE_EXTENDED_CAPSULE.identifier
            )
            offset = Vector(
                convert.vrm_json_array_to_float_vector(
                    extended_capsule_dict.get("offset"), [0, 0, 0]
                )
            )
            if bone:
                axis_translation = get_bone_extension(bone).axis_translation
                offset = Vector(offset) @ BoneExtension.translate_axis(
                    Matrix(),
                    BoneExtension.reverse_axis_translation(axis_translation),
                )
            extended_shape.capsule.offset = offset

            radius = extended_capsule_dict.get("radius")
            if isinstance(radius, (float, int)):
                extended_shape.capsule.radius = float(radius)

            tail = Vector(
                convert.vrm_json_array_to_float_vector(
                    extended_capsule_dict.get("tail"), [0, 0, 0]
                )
            )
            if bone:
                axis_translation = get_bone_extension(bone).axis_translation
                tail = Vector(tail) @ BoneExtension.translate_axis(
                    Matrix(),
                    BoneExtension.reverse_axis_translation(axis_translation),
                )
            extended_shape.capsule.tail = tail

            inside = extended_capsule_dict.get("inside")
            if isinstance(inside, bool):
                extended_shape.capsule.inside = inside

        elif isinstance(extended_plane_dict, dict):
            extended_collider.shape_type = (
                extended_collider.SHAPE_TYPE_EXTENDED_PLANE.identifier
            )

            offset = Vector(
                convert.vrm_json_array_to_float_vector(
                    extended_plane_dict.get("offset"), [0, 0, 0]
                )
            )
            if bone:
                axis_translation = get_bone_extension(bone).axis_translation
                offset = Vector(offset) @ BoneExtension.translate_axis(
                    Matrix(),
                    BoneExtension.reverse_axis_translation(axis_translation),
                )
            extended_shape.plane.offset = offset

            normal = Vector(
                convert.vrm_json_array_to_float_vector(
                    extended_plane_dict.get("normal"), [0, 0, 0]
                )
            )
            if bone:
                axis_translation = get_bone_extension(bone).axis_translation
                normal = Vector(normal) @ BoneExtension.translate_axis(
                    Matrix(),
                    BoneExtension.reverse_axis_translation(axis_translation),
                )
            extended_shape.plane.normal = normal

            collider_bpy_object = collider.bpy_object
            if collider_bpy_object:
                # Override the fallback collider values with fixed values when loading
                collider_bpy_object.empty_display_size = 0.125

        else:
            return

        extended_collider.enabled = True
        extended_collider.automatic_fallback_generation = False

    def load_spring_bone1_colliders(
        self,
        spring_bone: SpringBone1SpringBonePropertyGroup,
        spring_bone_dict: dict[str, Json],
        armature: Object,
    ) -> None:
        collider_dicts = spring_bone_dict.get("colliders")
        if not isinstance(collider_dicts, list):
            collider_dicts = []

        for collider_dict in collider_dicts:
            # Since the reference from ColliderGroup to Collider is by index,
            # create empty data even if the contents of collider_dict are invalid
            if ops.vrm.add_spring_bone1_collider(
                armature_object_name=armature.name
            ) != {"FINISHED"}:
                message = f'Failed to add spring bone 1.0 collider to "{armature.name}"'
                raise ValueError(message)

            collider = spring_bone.colliders[-1]

            if not isinstance(collider_dict, dict):
                collider.shape.sphere.radius = 0.0001
                collider.shape.sphere.offset = (0, 0, -10000)
                continue

            self.load_spring_bone1_collider(collider, collider_dict)

        for collider in spring_bone.colliders:
            collider.reset_bpy_object(self.context, armature)

        collider_object_names: list[str] = []
        if spring_bone.colliders:
            colliders_collection = self.context.blend_data.collections.new("Colliders")
            self.context.scene.collection.children.link(colliders_collection)
            for collider in spring_bone.colliders:
                collider_bpy_object = collider.bpy_object
                if not collider_bpy_object:
                    continue
                colliders_collection.objects.link(collider_bpy_object)
                collider_object_names.append(collider_bpy_object.name)
                if collider_bpy_object.name in self.context.scene.collection.objects:
                    self.context.scene.collection.objects.unlink(collider_bpy_object)
                for child in collider_bpy_object.children:
                    collider_object_names.append(child.name)
                    colliders_collection.objects.link(child)
                    if child.name in self.context.scene.collection.objects:
                        self.context.scene.collection.objects.unlink(child)

        spring_bone.active_collider_index = 0

        if collider_object_names:
            imported_object_names = self.imported_object_names
            if imported_object_names is None:
                imported_object_names = []
                self.imported_object_names = imported_object_names
            imported_object_names.extend(collider_object_names)

    def load_spring_bone1_collider_groups(
        self,
        spring_bone: SpringBone1SpringBonePropertyGroup,
        spring_bone_dict: dict[str, Json],
        armature_object_name: str,
    ) -> None:
        collider_group_dicts = spring_bone_dict.get("colliderGroups")
        if not isinstance(collider_group_dicts, list):
            collider_group_dicts = []

        for collider_group_index, collider_group_dict in enumerate(
            collider_group_dicts
        ):
            # Since the reference from Spring to ColliderGroup is by index,
            # create empty data even if the contents of collider_group_dict are invalid
            if ops.vrm.add_spring_bone1_collider_group(
                armature_object_name=armature_object_name
            ) != {"FINISHED"}:
                message = (
                    "Failed to add spring bone 1.0 collider group"
                    + f" to {armature_object_name}"
                )
                raise ValueError(message)

            if not isinstance(collider_group_dict, dict):
                continue

            collider_group = spring_bone.collider_groups[-1]

            name = collider_group_dict.get("name")
            if isinstance(name, str):
                collider_group.vrm_name = name

            collider_indices = collider_group_dict.get("colliders")
            if not isinstance(collider_indices, list):
                continue

            for collider_index in collider_indices:
                if ops.vrm.add_spring_bone1_collider_group_collider(
                    armature_object_name=armature_object_name,
                    collider_group_index=collider_group_index,
                ) != {"FINISHED"}:
                    raise ValueError(
                        "Failed to assign spring bone 1.0 collider to collider group "
                        + f"{collider_group_index} in {armature_object_name}"
                    )
                if not isinstance(collider_index, int):
                    continue
                if not 0 <= collider_index < len(spring_bone.colliders):
                    continue
                collider = spring_bone.colliders[collider_index]
                if not collider:
                    continue
                collider_reference = collider_group.colliders[-1]
                collider_reference.collider_name = collider.name
            collider_group.active_collider_index = 0
        spring_bone.active_collider_group_index = 0

    def load_spring_bone1_springs(
        self,
        spring_bone: SpringBone1SpringBonePropertyGroup,
        spring_bone_dict: dict[str, Json],
        armature_object_name: str,
    ) -> None:
        spring_dicts = spring_bone_dict.get("springs")
        if not isinstance(spring_dicts, list):
            spring_dicts = []

        for spring_dict in spring_dicts:
            if ops.vrm.add_spring_bone1_spring(
                armature_object_name=armature_object_name
            ) != {"FINISHED"} or not isinstance(spring_dict, dict):
                continue

            spring = spring_bone.springs[-1]

            name = spring_dict.get("name")
            if isinstance(name, str):
                spring.vrm_name = name

            center_index = spring_dict.get("center")
            if isinstance(center_index, int):
                bone_name = self.bone_names.get(center_index)
                if bone_name:
                    spring.center.bone_name = bone_name

            joint_dicts = spring_dict.get("joints")
            if not isinstance(joint_dicts, list):
                joint_dicts = []
            for joint_dict in joint_dicts:
                if ops.vrm.add_spring_bone1_spring_joint(
                    armature_object_name=armature_object_name,
                    spring_index=len(spring_bone.springs) - 1,
                ) != {"FINISHED"}:
                    continue
                if not isinstance(joint_dict, dict):
                    continue

                joint = spring.joints[-1]

                node_index = joint_dict.get("node")
                if isinstance(node_index, int):
                    bone_name = self.bone_names.get(node_index)
                    if bone_name:
                        joint.node.bone_name = bone_name

                hit_radius = convert.float_or_none(joint_dict.get("hitRadius"))
                if hit_radius is not None:
                    joint.hit_radius = hit_radius

                stiffness = convert.float_or_none(joint_dict.get("stiffness"))
                if stiffness is not None:
                    joint.stiffness = stiffness

                gravity_power = convert.float_or_none(joint_dict.get("gravityPower"))
                if gravity_power is not None:
                    joint.gravity_power = gravity_power

                gltf_axis_gravity_dir = convert.vrm_json_array_to_float_vector(
                    joint_dict.get("gravityDir"),
                    [0.0, -1.0, 0.0],
                )
                joint.gravity_dir = [
                    gltf_axis_gravity_dir[0],
                    -gltf_axis_gravity_dir[2],
                    gltf_axis_gravity_dir[1],
                ]

                drag_force = convert.float_or_none(joint_dict.get("dragForce"))
                if drag_force is not None:
                    joint.drag_force = drag_force
            spring.active_joint_index = 0

            collider_group_indices = spring_dict.get("colliderGroups")
            if not isinstance(collider_group_indices, list):
                collider_group_indices = []
            for collider_group_index in collider_group_indices:
                if ops.vrm.add_spring_bone1_spring_collider_group(
                    armature_object_name=armature_object_name,
                    spring_index=len(spring_bone.springs) - 1,
                ) != {"FINISHED"}:
                    continue
                if not isinstance(collider_group_index, int):
                    continue
                if not 0 <= collider_group_index < len(spring_bone.collider_groups):
                    continue
                collider_group = spring_bone.collider_groups[collider_group_index]
                if not collider_group:
                    continue
                collider_group_reference = spring.collider_groups[-1]
                collider_group_reference.collider_group_name = collider_group.name
            spring.active_collider_group_index = 0
        spring_bone.active_spring_index = 0

    def load_spring_bone1(
        self,
        spring_bone: SpringBone1SpringBonePropertyGroup,
        spring_bone_dict: Json,
    ) -> None:
        if not isinstance(spring_bone_dict, dict):
            return
        armature = self.armature
        if armature is None:
            message = "armature is None"
            raise ValueError(message)

        self.load_spring_bone1_colliders(spring_bone, spring_bone_dict, armature)

        self.load_spring_bone1_collider_groups(
            spring_bone,
            spring_bone_dict,
            armature.name,
        )

        self.load_spring_bone1_springs(
            spring_bone,
            spring_bone_dict,
            armature.name,
        )

    def get_object_or_bone_by_node_index(
        self, node_index: int
    ) -> Union[Object, PoseBone, None]:
        object_name = self.object_names.get(node_index)
        bone_name = self.bone_names.get(node_index)
        if object_name is not None:
            return self.context.blend_data.objects.get(object_name)
        if self.armature and bone_name is not None:
            return self.armature.pose.bones.get(bone_name)
        return None

    def load_node_constraint1(
        self,
    ) -> None:
        armature = self.armature
        if not armature:
            return

        nodes = self.parse_result.json_dict.get("nodes")
        if not isinstance(nodes, list):
            nodes = []
        for node_index, node_dict in enumerate(nodes):
            if not isinstance(node_dict, dict):
                continue

            node_extensions_dict = node_dict.get("extensions")
            if not isinstance(node_extensions_dict, dict):
                continue

            vrmc_node_constraint = node_extensions_dict.get("VRMC_node_constraint")
            if not isinstance(vrmc_node_constraint, dict):
                continue

            constraint_dict = vrmc_node_constraint.get("constraint")
            if not isinstance(constraint_dict, dict):
                continue

            roll_dict = constraint_dict.get("roll")
            aim_dict = constraint_dict.get("aim")
            rotation_dict = constraint_dict.get("rotation")

            object_or_bone = self.get_object_or_bone_by_node_index(node_index)
            if not object_or_bone:
                continue

            axis_translation = BoneExtension.reverse_axis_translation(
                get_bone_extension(object_or_bone.bone).axis_translation
                if isinstance(object_or_bone, PoseBone)
                else get_object_extension(object_or_bone).axis_translation
            )

            if isinstance(roll_dict, dict):
                constraint = object_or_bone.constraints.new(type="COPY_ROTATION")
                if not isinstance(constraint, CopyRotationConstraint):
                    logger.error("%s is not a CopyRotationConstraint", type(constraint))
                    continue
                constraint.mix_mode = "ADD"
                constraint.owner_space = "LOCAL"
                constraint.target_space = "LOCAL"
                roll_axis = roll_dict.get("rollAxis")
                if isinstance(object_or_bone, PoseBone) and isinstance(roll_axis, str):
                    roll_axis = BoneExtension.node_constraint_roll_axis_translation(
                        axis_translation,
                        roll_axis,
                    )
                constraint.use_x = False
                constraint.use_y = False
                constraint.use_z = False
                if roll_axis == "X":
                    constraint.use_x = True
                elif roll_axis == "Y":
                    constraint.use_y = True
                elif roll_axis == "Z":
                    constraint.use_z = True
                weight = convert.float_or_none(roll_dict.get("weight"))
                if weight is not None:
                    constraint.influence = weight
                source_index = roll_dict.get("source")
            elif isinstance(aim_dict, dict):
                constraint = object_or_bone.constraints.new(type="DAMPED_TRACK")
                if not isinstance(constraint, DampedTrackConstraint):
                    logger.error("%s is not a CopyRotationConstraint", type(constraint))
                    continue
                aim_axis = aim_dict.get("aimAxis")
                if isinstance(aim_axis, str) and isinstance(object_or_bone, PoseBone):
                    aim_axis = BoneExtension.node_constraint_aim_axis_translation(
                        axis_translation,
                        aim_axis,
                    )
                if isinstance(aim_axis, str):
                    track_axis = convert.VRM_AIM_AXIS_TO_BPY_TRACK_AXIS.get(aim_axis)
                    if track_axis:
                        constraint.track_axis = track_axis
                weight = convert.float_or_none(aim_dict.get("weight"))
                if weight is not None:
                    constraint.influence = weight
                source_index = aim_dict.get("source")
            elif isinstance(rotation_dict, dict):
                constraint = object_or_bone.constraints.new(type="COPY_ROTATION")
                if not isinstance(constraint, CopyRotationConstraint):
                    logger.error("%s is not a CopyRotationConstraint", type(constraint))
                    continue
                constraint.mix_mode = "ADD"
                constraint.owner_space = "LOCAL"
                constraint.target_space = "LOCAL"
                constraint.use_x = True
                constraint.use_y = True
                constraint.use_z = True
                weight = convert.float_or_none(rotation_dict.get("weight"))
                if weight is not None:
                    constraint.influence = weight
                source_index = rotation_dict.get("source")
            else:
                continue

            if isinstance(source_index, int):
                source = self.get_object_or_bone_by_node_index(source_index)
                if isinstance(source, Object):
                    constraint.target = source
                elif isinstance(source, PoseBone):
                    constraint.target = armature
                    constraint.subtarget = source.name
