# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
# SPDX-FileCopyrightText: 2018 iCyP

import json
import math
import uuid
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from typing import Optional, Union

from bpy.types import (
    Armature,
    Context,
    Material,
    Mesh,
    Object,
)
from mathutils import Matrix, Vector

from ..common import convert, 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.vrm0.human_bone import HumanBoneName, HumanBoneSpecifications
from ..editor import make_armature, migration
from ..editor.extension import get_armature_extension, get_material_extension
from ..editor.make_armature import (
    connect_parent_tail_and_child_head_if_very_close_position,
)
from ..editor.mtoon1.property_group import (
    Mtoon0TexturePropertyGroup,
    Mtoon1MaterialPropertyGroup,
    Mtoon1SamplerPropertyGroup,
    Mtoon1TextureInfoPropertyGroup,
    Mtoon1TexturePropertyGroup,
)
from ..editor.vrm0.property_group import (
    Vrm0BlendShapeMasterPropertyGroup,
    Vrm0FirstPersonPropertyGroup,
    Vrm0HumanoidBonePropertyGroup,
    Vrm0HumanoidPropertyGroup,
    Vrm0MetaPropertyGroup,
    Vrm0SecondaryAnimationPropertyGroup,
)
from .abstract_base_vrm_importer import AbstractBaseVrmImporter

logger = get_logger(__name__)


@dataclass(frozen=True)
class MaterialProperty:
    name: str
    shader: str
    render_queue: Optional[int]
    keyword_map: Mapping[str, bool]
    tag_map: Mapping[str, str]
    float_properties: Mapping[str, float]
    vector_properties: Mapping[str, Sequence[float]]
    texture_properties: Mapping[str, int]

    @staticmethod
    def create(json_dict: Mapping[str, Json], i: int) -> "MaterialProperty":
        fallback = MaterialProperty(
            name="Undefined",
            shader="VRM_USE_GLTFSHADER",
            render_queue=None,
            keyword_map={},
            tag_map={},
            float_properties={},
            vector_properties={},
            texture_properties={},
        )

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

        vrm_dict = extensions_dict.get("VRM")
        if not isinstance(vrm_dict, dict):
            return fallback

        material_property_dicts = vrm_dict.get("materialProperties")
        if not isinstance(material_property_dicts, list):
            return fallback

        if not 0 <= i < len(material_property_dicts):
            return fallback

        material_property_dict = material_property_dicts[i]
        if not isinstance(material_property_dict, dict):
            return fallback

        name = material_property_dict.get("name")
        if not isinstance(name, str):
            name = fallback.name

        shader = material_property_dict.get("shader")
        if not isinstance(shader, str):
            shader = fallback.shader

        render_queue = material_property_dict.get("renderQueue")
        if not isinstance(render_queue, int):
            render_queue = fallback.render_queue

        raw_keyword_map = material_property_dict.get("keywordMap")
        if isinstance(raw_keyword_map, dict):
            keyword_map: Mapping[str, bool] = {
                k: v for k, v in raw_keyword_map.items() if isinstance(v, bool)
            }
        else:
            keyword_map = fallback.keyword_map

        raw_tag_map = material_property_dict.get("tagMap")
        if isinstance(raw_tag_map, dict):
            tag_map: Mapping[str, str] = {
                k: v for k, v in raw_tag_map.items() if isinstance(v, str)
            }
        else:
            tag_map = fallback.tag_map

        raw_float_properties = material_property_dict.get("floatProperties")
        if isinstance(raw_float_properties, dict):
            float_properties: Mapping[str, float] = {
                k: float(v)
                for k, v in raw_float_properties.items()
                if isinstance(v, (float, int))
            }
        else:
            float_properties = fallback.float_properties

        raw_vector_properties = material_property_dict.get("vectorProperties")
        if isinstance(raw_vector_properties, dict):
            vector_properties: Mapping[str, Sequence[float]] = {
                k: [
                    vector_element
                    for vector_element in vector
                    if isinstance(vector_element, (float, int))
                ]
                for k, vector in raw_vector_properties.items()
                if isinstance(vector, list)
            }
        else:
            vector_properties = fallback.vector_properties

        raw_texture_properties = material_property_dict.get("textureProperties")
        if isinstance(raw_texture_properties, dict):
            texture_properties: Mapping[str, int] = {
                k: v for k, v in raw_texture_properties.items() if isinstance(v, int)
            }
        else:
            texture_properties = fallback.texture_properties

        return MaterialProperty(
            name=name,
            shader=shader,
            render_queue=render_queue,
            keyword_map=keyword_map,
            tag_map=tag_map,
            float_properties=float_properties,
            vector_properties=vector_properties,
            texture_properties=texture_properties,
        )


class Vrm0Importer(AbstractBaseVrmImporter):
    def load_materials(self, progress: PartialProgress) -> None:
        shader_to_assignment_method = {
            "VRM/MToon": self.assign_mtoon0_property,
            "VRM/UnlitTexture": self.assign_unlit_texture_property,
            "VRM/UnlitCutout": self.assign_unlit_cutout_property,
            "VRM/UnlitTransparent": self.assign_unlit_transparent_property,
            "VRM/UnlitTransparentZWrite": (
                self.assign_unlit_transparent_z_write_property
            ),
        }

        material_dicts = self.parse_result.json_dict.get("materials")
        if not isinstance(material_dicts, list):
            return

        material_properties = [
            MaterialProperty.create(self.parse_result.json_dict, index)
            for index in range(len(material_dicts))
        ]

        for index, material_property in enumerate(material_properties):
            assignment_method = shader_to_assignment_method.get(
                material_property.shader
            )
            if not assignment_method:
                continue

            material = self.materials.get(index)
            if not material:
                material = self.context.blend_data.materials.new(material_property.name)
                self.materials[index] = material

            self.reset_material(material)
            assignment_method(material, material_property)
            progress.update(float(index) / len(material_properties))

        progress.update(1)

    def assign_mtoon0_texture(
        self,
        texture: Union[Mtoon0TexturePropertyGroup, Mtoon1TexturePropertyGroup],
        texture_index: Optional[int],
    ) -> bool:
        if texture_index is None:
            return False
        texture_dicts = self.parse_result.json_dict.get("textures")
        if not isinstance(texture_dicts, list):
            return False
        if not 0 <= texture_index < len(texture_dicts):
            return False
        texture_dict = texture_dicts[texture_index]
        if not isinstance(texture_dict, dict):
            return False

        source = texture_dict.get("source")
        if isinstance(source, int):
            image = self.images.get(source)
            if image:
                image.colorspace_settings.name = texture.colorspace
                texture.source = image

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

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

        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
                )
            )

        return True

    def assign_mtoon0_texture_info(
        self,
        texture_info: Mtoon1TextureInfoPropertyGroup,
        texture_index: Optional[int],
        uv_transform: tuple[float, float, float, float],
    ) -> None:
        if not self.assign_mtoon0_texture(texture_info.index, texture_index):
            return

        texture_info.extensions.khr_texture_transform.offset = (
            uv_transform[0],
            uv_transform[1],
        )
        texture_info.extensions.khr_texture_transform.scale = (
            uv_transform[2],
            uv_transform[3],
        )

    def assign_mtoon0_property(
        self, material: Material, material_property: MaterialProperty
    ) -> None:
        # https://github.com/saturday06/VRM-Addon-for-Blender/blob/2_15_26/io_scene_vrm/editor/mtoon1/ops.py#L98
        material.use_backface_culling = True

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

        gltf.alpha_mode = gltf.ALPHA_MODE_OPAQUE.identifier
        blend_mode = material_property.float_properties.get("_BlendMode")
        if blend_mode is not None:
            if math.fabs(blend_mode - 1) < 0.001:
                gltf.alpha_mode = gltf.ALPHA_MODE_MASK.identifier
            elif math.fabs(blend_mode - 2) < 0.001:
                gltf.alpha_mode = gltf.ALPHA_MODE_BLEND.identifier
            elif math.fabs(blend_mode - 3) < 0.001:
                gltf.alpha_mode = gltf.ALPHA_MODE_BLEND.identifier
                mtoon.transparent_with_z_write = True

        cutoff = material_property.float_properties.get("_Cutoff")
        if cutoff is not None:
            gltf.alpha_cutoff = cutoff

        gltf.double_sided = True
        cull_mode = material_property.float_properties.get("_CullMode")
        if cull_mode is not None:
            if math.fabs(cull_mode - 1) < 0.001:
                gltf.mtoon0_front_cull_mode = True
            elif math.fabs(cull_mode - 2) < 0.001:
                gltf.double_sided = False

        base_color_factor = shader.rgba_or_none(
            material_property.vector_properties.get("_Color")
        )
        if base_color_factor:
            gltf.pbr_metallic_roughness.base_color_factor = convert.srgb_to_linear(
                base_color_factor
            )

        raw_uv_transform = material_property.vector_properties.get("_MainTex")
        if raw_uv_transform is None or len(raw_uv_transform) != 4:
            uv_transform = (0.0, 0.0, 1.0, 1.0)
        else:
            uv_transform = (
                raw_uv_transform[0],
                raw_uv_transform[1],
                raw_uv_transform[2],
                raw_uv_transform[3],
            )

        self.assign_mtoon0_texture_info(
            gltf.pbr_metallic_roughness.base_color_texture,
            material_property.texture_properties.get("_MainTex"),
            uv_transform,
        )

        shade_color_factor = shader.rgb_or_none(
            material_property.vector_properties.get("_ShadeColor")
        )
        if shade_color_factor:
            mtoon.shade_color_factor = convert.srgb_to_linear(shade_color_factor)

        self.assign_mtoon0_texture_info(
            mtoon.shade_multiply_texture,
            material_property.texture_properties.get("_ShadeTexture"),
            uv_transform,
        )

        self.assign_mtoon0_texture_info(
            gltf.normal_texture,
            material_property.texture_properties.get("_BumpMap"),
            uv_transform,
        )
        normal_texture_scale = material_property.float_properties.get("_BumpScale")
        if isinstance(normal_texture_scale, (float, int)):
            gltf.normal_texture.scale = float(normal_texture_scale)

        shading_shift_0x = material_property.float_properties.get("_ShadeShift")
        if shading_shift_0x is None:
            shading_shift_0x = 0

        shading_toony_0x = material_property.float_properties.get("_ShadeToony")
        if shading_toony_0x is None:
            shading_toony_0x = 0

        mtoon.shading_shift_factor = convert.mtoon_shading_shift_0_to_1(
            shading_toony_0x, shading_shift_0x
        )

        mtoon.shading_toony_factor = convert.mtoon_shading_toony_0_to_1(
            shading_toony_0x, shading_shift_0x
        )

        indirect_light_intensity = material_property.float_properties.get(
            "_IndirectLightIntensity"
        )
        if indirect_light_intensity is not None:
            mtoon.gi_equalization_factor = convert.mtoon_intensity_to_gi_equalization(
                indirect_light_intensity
            )
        else:
            mtoon.gi_equalization_factor = 0.5

        raw_emissive_factor = shader.rgb_or_none(
            material_property.vector_properties.get("_EmissionColor")
        ) or (0, 0, 0)
        max_color_component_value = max(raw_emissive_factor)
        if max_color_component_value > 1:
            gltf.emissive_factor = convert.srgb_to_linear(
                Vector(raw_emissive_factor) / max_color_component_value
            )
            gltf.extensions.khr_materials_emissive_strength.emissive_strength = (
                max_color_component_value
            )
        else:
            gltf.emissive_factor = convert.srgb_to_linear(raw_emissive_factor)

        self.assign_mtoon0_texture_info(
            gltf.emissive_texture,
            material_property.texture_properties.get("_EmissionMap"),
            uv_transform,
        )

        self.assign_mtoon0_texture_info(
            mtoon.matcap_texture,
            material_property.texture_properties.get("_SphereAdd"),
            (0, 0, 1, 1),
        )

        parametric_rim_color_factor = shader.rgb_or_none(
            material_property.vector_properties.get("_RimColor")
        )
        if parametric_rim_color_factor:
            mtoon.parametric_rim_color_factor = convert.srgb_to_linear(
                parametric_rim_color_factor
            )

        parametric_rim_fresnel_power_factor = material_property.float_properties.get(
            "_RimFresnelPower"
        )
        if isinstance(parametric_rim_fresnel_power_factor, (float, int)):
            mtoon.parametric_rim_fresnel_power_factor = (
                parametric_rim_fresnel_power_factor
            )

        mtoon.parametric_rim_lift_factor = material_property.float_properties.get(
            "_RimLift", 0
        )

        self.assign_mtoon0_texture_info(
            mtoon.rim_multiply_texture,
            material_property.texture_properties.get("_RimTexture"),
            uv_transform,
        )

        # https://github.com/vrm-c/UniVRM/blob/7c9919ef47a25c04100a2dcbe6a75dff49ef4857/Assets/VRM10/Runtime/Migration/Materials/MigrationMToonMaterial.cs#L287-L290
        mtoon.rim_lighting_mix_factor = 1.0
        rim_lighting_mix = material_property.float_properties.get("_RimLightingMix")
        if rim_lighting_mix is not None:
            gltf.mtoon0_rim_lighting_mix = rim_lighting_mix

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

        centimeter_to_meter = 0.01
        one_hundredth = 0.01

        outline_width_mode = round(
            material_property.float_properties.get("_OutlineWidthMode", 0)
        )

        outline_width = material_property.float_properties.get("_OutlineWidth")
        if outline_width is None:
            outline_width = 0.0

        if outline_width_mode == 0:
            mtoon.outline_width_mode = mtoon.OUTLINE_WIDTH_MODE_NONE.identifier
        elif outline_width_mode == 1:
            mtoon.outline_width_mode = (
                mtoon.OUTLINE_WIDTH_MODE_WORLD_COORDINATES.identifier
            )
            mtoon.outline_width_factor = max(0.0, outline_width * centimeter_to_meter)
        else:
            mtoon.outline_width_mode = (
                mtoon.OUTLINE_WIDTH_MODE_SCREEN_COORDINATES.identifier
            )
            mtoon.outline_width_factor = max(0.0, outline_width * one_hundredth * 0.5)
            outline_scaled_max_distance = material_property.float_properties.get(
                "_OutlineScaledMaxDistance"
            )
            if outline_scaled_max_distance is not None:
                gltf.mtoon0_outline_scaled_max_distance = outline_scaled_max_distance

        self.assign_mtoon0_texture_info(
            mtoon.outline_width_multiply_texture,
            material_property.texture_properties.get("_OutlineWidthTexture"),
            uv_transform,
        )

        outline_color_factor = shader.rgb_or_none(
            material_property.vector_properties.get("_OutlineColor")
        )
        if outline_color_factor:
            mtoon.outline_color_factor = convert.srgb_to_linear(outline_color_factor)

        outline_color_mode = material_property.float_properties.get("_OutlineColorMode")
        if (
            outline_color_mode is not None
            and math.fabs(outline_color_mode - 1) < 0.00001
        ):
            mtoon.outline_lighting_mix_factor = (
                material_property.float_properties.get("_OutlineLightingMix") or 1
            )
        else:
            mtoon.outline_lighting_mix_factor = 0  # Fixed Color

        self.assign_mtoon0_texture_info(
            mtoon.uv_animation_mask_texture,
            material_property.texture_properties.get("_UvAnimMaskTexture"),
            uv_transform,
        )

        uv_animation_rotation_speed_factor = material_property.float_properties.get(
            "_UvAnimRotation"
        )
        if isinstance(uv_animation_rotation_speed_factor, (float, int)):
            mtoon.uv_animation_rotation_speed_factor = (
                uv_animation_rotation_speed_factor
            )

        uv_animation_scroll_x_speed_factor = material_property.float_properties.get(
            "_UvAnimScrollX"
        )
        if isinstance(uv_animation_scroll_x_speed_factor, (float, int)):
            mtoon.uv_animation_scroll_x_speed_factor = (
                uv_animation_scroll_x_speed_factor
            )

        uv_animation_scroll_y_speed_factor = material_property.float_properties.get(
            "_UvAnimScrollY"
        )
        if isinstance(uv_animation_scroll_y_speed_factor, (float, int)):
            mtoon.uv_animation_scroll_y_speed_factor = (
                -uv_animation_scroll_y_speed_factor
            )

        light_color_attenuation = material_property.float_properties.get(
            "_LightColorAttenuation"
        )
        if light_color_attenuation is not None:
            gltf.mtoon0_light_color_attenuation = light_color_attenuation

        self.assign_mtoon0_texture(
            gltf.mtoon0_receive_shadow_texture,
            material_property.texture_properties.get("_ReceiveShadowTexture"),
        )

        gltf.mtoon0_receive_shadow_rate = material_property.float_properties.get(
            "_ReceiveShadowRate", 0.5
        )

        self.assign_mtoon0_texture(
            gltf.mtoon0_shading_grade_texture,
            material_property.texture_properties.get("_ShadingGradeTexture"),
        )

        gltf.mtoon0_shading_grade_rate = material_property.float_properties.get(
            "_ShadingGradeRate", 0.5
        )

        render_queue = material_property.render_queue
        if render_queue is not None:
            gltf.mtoon0_render_queue = render_queue

    def assign_unlit_common_property(
        self,
        material: Material,
        material_property: MaterialProperty,
    ) -> None:
        gltf = get_material_extension(material).mtoon1
        mtoon = gltf.extensions.vrmc_materials_mtoon

        main_texture_index = material_property.texture_properties.get("_MainTex")
        main_texture_image = None
        if isinstance(main_texture_index, int):
            texture_dicts = self.parse_result.json_dict.get("textures")
            if isinstance(texture_dicts, list) and 0 <= main_texture_index < len(
                texture_dicts
            ):
                texture_dict = texture_dicts[main_texture_index]
                if isinstance(texture_dict, dict):
                    image_index = texture_dict.get("source")
                    if isinstance(image_index, int):
                        main_texture_image = self.images.get(image_index)

        (texture_offset_u, texture_offset_v, texture_scale_u, texture_scale_v) = (
            convert.float4_or(
                material_property.vector_properties.get("_MainTex"),
                (0.0, 0.0, 1.0, 1.0),
            )
        )
        if main_texture_image:
            for texture in [
                gltf.pbr_metallic_roughness.base_color_texture,
                gltf.emissive_texture,
                mtoon.shade_multiply_texture,
            ]:
                texture.index.source = main_texture_image
                khr_texture_transform = texture.extensions.khr_texture_transform
                khr_texture_transform.offset = (texture_offset_u, texture_offset_v)
                khr_texture_transform.scale = (texture_scale_u, texture_scale_v)

        gltf.pbr_metallic_roughness.base_color_factor = (0, 0, 0, 1)
        gltf.emissive_factor = (1, 1, 1)

        gltf.double_sided = False
        mtoon.shade_color_factor = (0, 0, 0)
        mtoon.shading_toony_factor = 0.95
        mtoon.shading_shift_factor = -0.05
        mtoon.rim_lighting_mix_factor = 1
        mtoon.parametric_rim_fresnel_power_factor = 5
        mtoon.parametric_rim_lift_factor = 0

        render_queue = material_property.render_queue
        if render_queue is not None:
            gltf.mtoon0_render_queue = render_queue

    def assign_unlit_texture_property(
        self,
        material: Material,
        material_property: MaterialProperty,
    ) -> None:
        gltf = get_material_extension(material).mtoon1
        gltf.enabled = True
        gltf.alpha_mode = Mtoon1MaterialPropertyGroup.ALPHA_MODE_OPAQUE.identifier

        self.assign_unlit_common_property(material, material_property)

    def assign_unlit_cutout_property(
        self,
        material: Material,
        material_property: MaterialProperty,
    ) -> None:
        gltf = get_material_extension(material).mtoon1
        gltf.enabled = True
        gltf.alpha_mode = Mtoon1MaterialPropertyGroup.ALPHA_MODE_MASK.identifier

        cutoff = material_property.float_properties.get("_Cutoff")
        if isinstance(cutoff, (int, float)):
            gltf.alpha_cutoff = cutoff

        self.assign_unlit_common_property(material, material_property)

    def assign_unlit_transparent_property(
        self,
        material: Material,
        material_property: MaterialProperty,
    ) -> None:
        gltf = get_material_extension(material).mtoon1
        gltf.enabled = True
        gltf.alpha_mode = Mtoon1MaterialPropertyGroup.ALPHA_MODE_BLEND.identifier

        self.assign_unlit_common_property(material, material_property)

    def assign_unlit_transparent_z_write_property(
        self,
        material: Material,
        material_property: MaterialProperty,
    ) -> None:
        gltf = get_material_extension(material).mtoon1
        gltf.enabled = True
        gltf.alpha_mode = Mtoon1MaterialPropertyGroup.ALPHA_MODE_BLEND.identifier

        mtoon = gltf.extensions.vrmc_materials_mtoon
        mtoon.transparent_with_z_write = True

        self.assign_unlit_common_property(material, material_property)

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

        if self.parse_result.spec_version_number >= (1, 0):
            return

        vrm0_extension = self.parse_result.vrm0_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_vrm0_meta(vrm0.meta, vrm0_extension.get("meta"))
        self.load_vrm0_humanoid(vrm0.humanoid, vrm0_extension.get("humanoid"))
        setup_bones(self.context, armature)
        self.load_vrm0_first_person(
            vrm0.first_person, vrm0_extension.get("firstPerson")
        )
        self.load_vrm0_blend_shape_master(
            vrm0.blend_shape_master, vrm0_extension.get("blendShapeMaster")
        )
        self.load_vrm0_secondary_animation(
            vrm0.secondary_animation, vrm0_extension.get("secondaryAnimation")
        )
        migration.migrate(self.context, armature.name)

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

        title = meta_dict.get("title")
        if isinstance(title, str):
            meta.title = title

        version = meta_dict.get("version")
        if isinstance(version, str):
            meta.version = version

        author = meta_dict.get("author")
        if isinstance(author, str):
            meta.author = author

        contact_information = meta_dict.get("contactInformation")
        if isinstance(contact_information, str):
            meta.contact_information = contact_information

        reference = meta_dict.get("reference")
        if isinstance(reference, str):
            meta.reference = reference

        allowed_user_name = meta_dict.get("allowedUserName")
        if (
            isinstance(allowed_user_name, str)
            and allowed_user_name in meta.allowed_user_name_enum.identifiers()
        ):
            meta.allowed_user_name = allowed_user_name

        violent_ussage_name = meta_dict.get("violentUssageName")
        if (
            isinstance(violent_ussage_name, str)
            and violent_ussage_name in meta.violent_ussage_name_enum.identifiers()
        ):
            meta.violent_ussage_name = violent_ussage_name

        sexual_ussage_name = meta_dict.get("sexualUssageName")
        if (
            isinstance(sexual_ussage_name, str)
            and sexual_ussage_name in meta.sexual_ussage_name_enum.identifiers()
        ):
            meta.sexual_ussage_name = sexual_ussage_name

        commercial_ussage_name = meta_dict.get("commercialUssageName")
        if (
            isinstance(commercial_ussage_name, str)
            and commercial_ussage_name in meta.commercial_ussage_name_enum.identifiers()
        ):
            meta.commercial_ussage_name = commercial_ussage_name

        other_permission_url = meta_dict.get("otherPermissionUrl")
        if isinstance(other_permission_url, str):
            meta.other_permission_url = other_permission_url

        license_name = meta_dict.get("licenseName")
        if (
            isinstance(license_name, str)
            and license_name in meta.license_name_enum.identifiers()
        ):
            meta.license_name = license_name

        other_license_url = meta_dict.get("otherLicenseUrl")
        if isinstance(other_license_url, str):
            meta.other_license_url = other_license_url

        texture = meta_dict.get("texture")
        texture_dicts = self.parse_result.json_dict.get("textures")
        if (
            isinstance(texture, int)
            and isinstance(texture_dicts, list)
            # extensions.VRM.meta.texture could be -1
            # https://github.com/vrm-c/UniVRM/issues/91#issuecomment-454284964
            and 0 <= texture < len(texture_dicts)
        ):
            texture_dict = texture_dicts[texture]
            if isinstance(texture_dict, dict):
                image_index = texture_dict.get("source")
                if isinstance(image_index, int) and image_index in self.images:
                    meta.texture = self.images[image_index]

    def load_vrm0_humanoid(
        self, humanoid: Vrm0HumanoidPropertyGroup, humanoid_dict: Json
    ) -> None:
        if not isinstance(humanoid_dict, dict):
            return
        human_bone_dicts = humanoid_dict.get("humanBones")
        if isinstance(human_bone_dicts, list):
            for human_bone_dict in human_bone_dicts:
                if not isinstance(human_bone_dict, dict):
                    continue

                bone = human_bone_dict.get("bone")
                if bone not in HumanBoneSpecifications.all_names:
                    continue

                node = human_bone_dict.get("node")
                if not isinstance(node, int):
                    continue

                node_bone_name = self.bone_names.get(node)
                if node_bone_name is None:
                    continue

                human_bone = next(
                    (
                        human_bone
                        for human_bone in humanoid.human_bones
                        if human_bone.bone == bone
                    ),
                    None,
                )
                if human_bone:
                    logger.warning('Duplicated bone: "%s"', bone)
                else:
                    human_bone = humanoid.human_bones.add()
                human_bone.bone = bone
                human_bone.node.bone_name = node_bone_name

                use_default_values = human_bone_dict.get("useDefaultValues")
                if isinstance(use_default_values, bool):
                    human_bone.use_default_values = use_default_values

                min_ = convert.vrm_json_vector3_to_tuple(human_bone_dict.get("min"))
                if min_ is not None:
                    human_bone.min = min_

                max_ = convert.vrm_json_vector3_to_tuple(human_bone_dict.get("max"))
                if max_ is not None:
                    human_bone.max = max_

                center = convert.vrm_json_vector3_to_tuple(
                    human_bone_dict.get("center")
                )
                if center is not None:
                    human_bone.center = center

                axis_length = convert.float_or_none(human_bone_dict.get("axisLength"))
                if axis_length is not None:
                    human_bone.axis_length = axis_length

        arm_stretch = convert.float_or_none(humanoid_dict.get("armStretch"))
        if arm_stretch is not None:
            humanoid.arm_stretch = arm_stretch

        leg_stretch = convert.float_or_none(humanoid_dict.get("legStretch"))
        if leg_stretch is not None:
            humanoid.leg_stretch = leg_stretch

        upper_arm_twist = convert.float_or_none(humanoid_dict.get("upperArmTwist"))
        if upper_arm_twist is not None:
            humanoid.upper_arm_twist = upper_arm_twist

        lower_arm_twist = convert.float_or_none(humanoid_dict.get("lowerArmTwist"))
        if lower_arm_twist is not None:
            humanoid.lower_arm_twist = lower_arm_twist

        upper_leg_twist = convert.float_or_none(humanoid_dict.get("upperLegTwist"))
        if upper_leg_twist is not None:
            humanoid.upper_leg_twist = upper_leg_twist

        lower_leg_twist = convert.float_or_none(humanoid_dict.get("lowerLegTwist"))
        if lower_leg_twist is not None:
            humanoid.lower_leg_twist = lower_leg_twist

        feet_spacing = convert.float_or_none(humanoid_dict.get("feetSpacing"))
        if feet_spacing is not None:
            humanoid.feet_spacing = feet_spacing

        has_translation_dof = humanoid_dict.get("hasTranslationDoF")
        if isinstance(has_translation_dof, bool):
            humanoid.has_translation_dof = has_translation_dof

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

        first_person_bone = first_person_dict.get("firstPersonBone")
        if isinstance(first_person_bone, int) and (
            first_person_bone_name := self.bone_names.get(first_person_bone)
        ):
            first_person.first_person_bone.bone_name = first_person_bone_name

        first_person_bone_offset = convert.vrm_json_vector3_to_tuple(
            first_person_dict.get("firstPersonBoneOffset")
        )
        if first_person_bone_offset is not None:
            # Axis confusing
            (x, y, z) = first_person_bone_offset
            first_person.first_person_bone_offset = (x, z, y)

        mesh_annotation_dicts = first_person_dict.get("meshAnnotations")
        if isinstance(mesh_annotation_dicts, list):
            for mesh_annotation_dict in mesh_annotation_dicts:
                mesh_annotation = first_person.mesh_annotations.add()

                if not isinstance(mesh_annotation_dict, dict):
                    continue

                mesh = mesh_annotation_dict.get("mesh")
                if isinstance(mesh, int) and mesh in self.meshes:
                    mesh_annotation.mesh.mesh_object_name = self.meshes[mesh].name

                first_person_flag = mesh_annotation_dict.get("firstPersonFlag")
                if (
                    isinstance(first_person_flag, str)
                    and first_person_flag
                    in mesh_annotation.first_person_flag_enum.identifiers()
                ):
                    mesh_annotation.first_person_flag = first_person_flag

        look_at_type_name = first_person_dict.get("lookAtTypeName")
        if (
            isinstance(look_at_type_name, str)
            and look_at_type_name in first_person.look_at_type_name_enum.identifiers()
        ):
            first_person.look_at_type_name = look_at_type_name

        for look_at, look_at_dict in [
            (
                first_person.look_at_horizontal_inner,
                first_person_dict.get("lookAtHorizontalInner"),
            ),
            (
                first_person.look_at_horizontal_outer,
                first_person_dict.get("lookAtHorizontalOuter"),
            ),
            (
                first_person.look_at_vertical_down,
                first_person_dict.get("lookAtVerticalDown"),
            ),
            (
                first_person.look_at_vertical_up,
                first_person_dict.get("lookAtVerticalUp"),
            ),
        ]:
            if not isinstance(look_at_dict, dict):
                continue

            curve = convert.vrm_json_curve_to_list(look_at_dict.get("curve"))
            if curve is not None:
                look_at.curve = curve

            x_range = look_at_dict.get("xRange")
            if isinstance(x_range, (float, int)):
                look_at.x_range = x_range

            y_range = look_at_dict.get("yRange")
            if isinstance(y_range, (float, int)):
                look_at.y_range = y_range

    def load_vrm0_blend_shape_master(
        self,
        blend_shape_master: Vrm0BlendShapeMasterPropertyGroup,
        blend_shape_master_dict: Json,
    ) -> None:
        if not isinstance(blend_shape_master_dict, dict):
            return
        blend_shape_group_dicts = blend_shape_master_dict.get("blendShapeGroups")
        if not isinstance(blend_shape_group_dicts, list):
            return

        for blend_shape_group_dict in blend_shape_group_dicts:
            blend_shape_group = blend_shape_master.blend_shape_groups.add()

            if not isinstance(blend_shape_group_dict, dict):
                continue

            name = blend_shape_group_dict.get("name")
            if isinstance(name, str):
                blend_shape_group.name = name

            preset_name = blend_shape_group_dict.get("presetName")
            if (
                isinstance(preset_name, str)
                and preset_name in blend_shape_group.preset_name_enum.identifiers()
            ):
                blend_shape_group.preset_name = preset_name

            bind_dicts = blend_shape_group_dict.get("binds")
            if isinstance(bind_dicts, list):
                for bind_dict in bind_dicts:
                    if not isinstance(bind_dict, dict):
                        continue

                    mesh_index = bind_dict.get("mesh")
                    if not isinstance(mesh_index, int):
                        continue

                    mesh_object = self.meshes.get(mesh_index)
                    if not mesh_object:
                        continue

                    bind = blend_shape_group.binds.add()
                    bind.mesh.mesh_object_name = mesh_object.name
                    mesh_data = mesh_object.data
                    if isinstance(mesh_data, Mesh):
                        shape_keys = mesh_data.shape_keys
                        if shape_keys:
                            index = bind_dict.get("index")
                            if isinstance(index, int) and (
                                1 <= (index + 1) < len(shape_keys.key_blocks)
                            ):
                                bind.index = list(shape_keys.key_blocks.keys())[
                                    index + 1
                                ]
                    weight = convert.float_or(bind_dict.get("weight"), 0.0)
                    bind.weight = min(max(weight / 100.0, 0), 1)

            material_value_dicts = blend_shape_group_dict.get("materialValues")
            if isinstance(material_value_dicts, list):
                for material_value_dict in material_value_dicts:
                    material_value = blend_shape_group.material_values.add()

                    if not isinstance(material_value_dict, dict):
                        continue

                    material_name = material_value_dict.get("materialName")
                    if (
                        isinstance(material_name, str)
                        and material_name in self.context.blend_data.materials
                    ):
                        material_value.material = self.context.blend_data.materials[
                            material_name
                        ]

                    property_name = material_value_dict.get("propertyName")
                    if isinstance(property_name, str):
                        material_value.property_name = property_name

                    target_value_vector = material_value_dict.get("targetValue")
                    if isinstance(target_value_vector, list):
                        for v in target_value_vector:
                            material_value.target_value.add().value = convert.float_or(
                                v, 0.0
                            )

            is_binary = blend_shape_group_dict.get("isBinary")
            if isinstance(is_binary, bool):
                blend_shape_group.is_binary = is_binary

    def load_vrm0_secondary_animation(
        self,
        secondary_animation: Vrm0SecondaryAnimationPropertyGroup,
        secondary_animation_dict: Json,
    ) -> None:
        if not isinstance(secondary_animation_dict, dict):
            return
        armature = self.armature
        if armature is None:
            message = "armature is None"
            raise ValueError(message)

        collider_group_dicts = secondary_animation_dict.get("colliderGroups")
        if not isinstance(collider_group_dicts, list):
            collider_group_dicts = []

        self.context.view_layer.depsgraph.update()
        self.context.scene.view_layers.update()
        collider_objs: list[Object] = []
        for collider_group_dict in collider_group_dicts:
            collider_group = secondary_animation.collider_groups.add()
            collider_group.uuid = uuid.uuid4().hex
            collider_group.refresh(armature)

            if not isinstance(collider_group_dict, dict):
                continue

            node = collider_group_dict.get("node")
            if not isinstance(node, int):
                continue
            bone_name = self.bone_names.get(node)
            if bone_name is None:
                continue
            collider_group.node.bone_name = bone_name
            collider_dicts = collider_group_dict.get("colliders")
            if not isinstance(collider_dicts, list):
                continue

            for collider_index, collider_dict in enumerate(collider_dicts):
                collider = collider_group.colliders.add()

                if not isinstance(collider_dict, dict):
                    continue

                offset = convert.vrm_json_vector3_to_tuple(collider_dict.get("offset"))
                if offset is None:
                    offset = (0, 0, 0)

                radius = convert.float_or(collider_dict.get("radius"), 0.0)

                collider_name = f"{bone_name}_collider_{collider_index}"
                obj = self.context.blend_data.objects.new(
                    name=collider_name, object_data=None
                )
                collider.bpy_object = obj
                obj.parent = self.armature
                obj.parent_type = "BONE"
                obj.parent_bone = bone_name
                fixed_offset = [
                    offset[axis] * inv for axis, inv in zip([0, 2, 1], [-1, -1, 1])
                ]  # TODO: Y-axis inversion is matched to UniVRM serialization

                # Since it's parented to the tail side of the bone, move it to the
                # position from the root
                obj.matrix_world = Matrix.Translation(
                    [
                        armature.matrix_world.to_translation()[i]
                        + self.armature_data.bones[
                            bone_name
                        ].matrix_local.to_translation()[i]
                        + fixed_offset[i]
                        for i in range(3)
                    ]
                )

                obj.empty_display_size = radius
                obj.empty_display_type = "SPHERE"
                collider_objs.append(obj)
        if collider_objs:
            colliders_collection = self.context.blend_data.collections.new("Colliders")
            self.context.scene.collection.children.link(colliders_collection)
            for collider_obj in collider_objs:
                colliders_collection.objects.link(collider_obj)

        for collider_group in secondary_animation.collider_groups:
            collider_group.refresh(armature)

        bone_group_dicts = secondary_animation_dict.get("boneGroups")
        if not isinstance(bone_group_dicts, list):
            bone_group_dicts = []

        for bone_group_dict in bone_group_dicts:
            bone_group = secondary_animation.bone_groups.add()

            if not isinstance(bone_group_dict, dict):
                bone_group.refresh(armature)
                continue

            comment = bone_group_dict.get("comment")
            if isinstance(comment, str):
                bone_group.comment = comment

            stiffiness = convert.float_or_none(bone_group_dict.get("stiffiness"))
            if stiffiness is not None:
                bone_group.stiffiness = stiffiness

            gravity_power = convert.float_or_none(bone_group_dict.get("gravityPower"))
            if gravity_power is not None:
                bone_group.gravity_power = gravity_power

            gravity_dir = convert.vrm_json_vector3_to_tuple(
                bone_group_dict.get("gravityDir")
            )
            if gravity_dir is not None:
                # Axis confusing
                (x, y, z) = gravity_dir
                bone_group.gravity_dir = (x, z, y)

            drag_force = convert.float_or_none(bone_group_dict.get("dragForce"))
            if drag_force is not None:
                bone_group.drag_force = drag_force

            center = bone_group_dict.get("center")
            if isinstance(center, int) and (
                center_bone_name := self.bone_names.get(center)
            ):
                bone_group.center.bone_name = center_bone_name

            hit_radius = convert.float_or_none(bone_group_dict.get("hitRadius"))
            if hit_radius is not None:
                bone_group.hit_radius = hit_radius

            bones = bone_group_dict.get("bones")
            if isinstance(bones, list):
                for bone in bones:
                    bone_prop = bone_group.bones.add()
                    if not isinstance(bone, int):
                        continue
                    bone_name = self.bone_names.get(bone)
                    if bone_name is None:
                        continue
                    bone_prop.bone_name = bone_name

            collider_group_indices = bone_group_dict.get("colliderGroups")
            if isinstance(collider_group_indices, list):
                for collider_group_index in collider_group_indices:
                    if not isinstance(collider_group_index, int) or not (
                        0
                        <= collider_group_index
                        < len(secondary_animation.collider_groups)
                    ):
                        continue
                    collider_group_uuid = bone_group.collider_groups.add()
                    collider_group = secondary_animation.collider_groups[
                        collider_group_index
                    ]
                    collider_group_uuid.value = collider_group.uuid

        for bone_group in secondary_animation.bone_groups:
            bone_group.refresh(armature)

        collider_object_names = [
            collider_bpy_object.name
            for collider_group in secondary_animation.collider_groups
            for collider in collider_group.colliders
            if (collider_bpy_object := collider.bpy_object)
        ]
        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 find_vrm0_bone_node_indices(self) -> list[int]:
        result: list[int] = []
        vrm0_dict = self.parse_result.vrm0_extension_dict

        first_person_dict = vrm0_dict.get("firstPerson")
        if isinstance(first_person_dict, dict):
            first_person_bone_index = first_person_dict.get("firstPersonBone")
            if isinstance(first_person_bone_index, int):
                result.append(first_person_bone_index)

        humanoid_dict = vrm0_dict.get("humanoid")
        if isinstance(humanoid_dict, dict):
            human_bone_dicts = humanoid_dict.get("humanBones")
            if isinstance(human_bone_dicts, list):
                for human_bone_dict in human_bone_dicts:
                    if not isinstance(human_bone_dict, dict):
                        continue
                    node_index = human_bone_dict.get("node")
                    if isinstance(node_index, int):
                        result.append(node_index)

        secondary_animation_dict = vrm0_dict.get("secondaryAnimation")
        if isinstance(secondary_animation_dict, dict):
            collider_group_dicts = secondary_animation_dict.get("colliderGroups")
            if isinstance(collider_group_dicts, list):
                for collider_group_dict in collider_group_dicts:
                    if not isinstance(collider_group_dict, dict):
                        continue
                    node_index = collider_group_dict.get("node")
                    if isinstance(node_index, int):
                        result.append(node_index)

            bone_group_dicts = secondary_animation_dict.get("boneGroups")
            if isinstance(bone_group_dicts, list):
                for bone_group_dict in bone_group_dicts:
                    if not isinstance(bone_group_dict, dict):
                        continue
                    center_index = bone_group_dict.get("center")
                    if isinstance(center_index, int):
                        result.append(center_index)
                    bone_indices = bone_group_dict.get("bones")
                    if isinstance(bone_indices, list):
                        result.extend(
                            bone_index
                            for bone_index in bone_indices
                            if isinstance(bone_index, int)
                        )
        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_vrm0_bone_node_indices()
            )
        )


def setup_bones(context: Context, armature: Object) -> None:
    """Set the direction and length of Human Bones.

    VRM0 does not have bone direction and length, so currently FORTUNE's
    direction and length determination is performed at import time. However,
    since FORTUNE does not consider human body structure, it can result in
    unnatural bones. Therefore, we determine direction and length using a
    heuristic method that considers human body structure.

    Things to do someday:
        Limit the Head angle to within 30 degrees from the parent angle.
        Add elevation angle when Toes or Foot are not touching the ground.
    """
    armature_data = armature.data
    if not isinstance(armature_data, Armature):
        return
    addon_extension = get_armature_extension(armature_data)

    Vrm0HumanoidPropertyGroup.fixup_human_bones(armature)
    Vrm0HumanoidPropertyGroup.update_all_node_candidates(
        context,
        armature_data.name,
        force=True,
    )

    human_bones = addon_extension.vrm0.humanoid.human_bones
    for human_bone in human_bones:
        if (
            human_bone.node.bone_name
            and human_bone.node.bone_name not in human_bone.node_candidates
        ):
            # has error
            return

    for humanoid_name in HumanBoneSpecifications.required_names:
        if not any(
            human_bone.bone == humanoid_name and human_bone.node.bone_name
            for human_bone in human_bones
        ):
            # has error
            return

    with AbstractBaseVrmImporter.save_bone_child_object_transforms(
        context, armature
    ) as armature_data:
        bone_name_to_human_bone_name: dict[str, HumanBoneName] = {}
        for human_bone in human_bones:
            if not human_bone.node.bone_name:
                continue
            name = HumanBoneName.from_str(human_bone.bone)
            if not name:
                continue
            bone_name_to_human_bone_name[human_bone.node.bone_name] = name

        for bone_name in bone_name_to_human_bone_name:
            bone = armature_data.edit_bones.get(bone_name)
            while bone:
                bone.roll = 0.0
                bone = bone.parent

        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

                if (parent.head - bone.head).length >= make_armature.MIN_BONE_LENGTH:
                    parent.tail = bone.head

                bone = parent

        human_bone_name_to_human_bone: dict[
            HumanBoneName, Vrm0HumanoidBonePropertyGroup
        ] = {}
        for human_bone in human_bones:
            n = HumanBoneName.from_str(human_bone.bone)
            if not n:
                continue
            human_bone_name_to_human_bone[n] = human_bone

        # When VRM Humanoid terminal bones other than eye and head bones have
        # multiple children, point them in the same direction as the parent.
        # This results in a more natural direction than the default.
        # Since it might be a direction specification by the VRM creator,
        # do nothing when there is only one child.
        for tip_bone_name in [
            HumanBoneName.JAW,
            HumanBoneName.LEFT_THUMB_DISTAL,
            HumanBoneName.LEFT_INDEX_DISTAL,
            HumanBoneName.LEFT_MIDDLE_DISTAL,
            HumanBoneName.LEFT_RING_DISTAL,
            HumanBoneName.LEFT_LITTLE_DISTAL,
            HumanBoneName.RIGHT_THUMB_DISTAL,
            HumanBoneName.RIGHT_INDEX_DISTAL,
            HumanBoneName.RIGHT_MIDDLE_DISTAL,
            HumanBoneName.RIGHT_RING_DISTAL,
            HumanBoneName.RIGHT_LITTLE_DISTAL,
            HumanBoneName.LEFT_TOES,
            HumanBoneName.RIGHT_TOES,
        ]:
            bone = None
            searching_tip_bone_name: Optional[HumanBoneName] = tip_bone_name
            while searching_tip_bone_name:
                # Break if there is a corresponding bone
                human_bone = human_bone_name_to_human_bone.get(searching_tip_bone_name)
                if human_bone:
                    bone = armature_data.edit_bones.get(human_bone.node.bone_name)
                    if bone:
                        break

                # If there is no corresponding bone and it was a required bone,
                # abort due to data error
                specification = HumanBoneSpecifications.get(searching_tip_bone_name)
                if specification.requirement:
                    break
                parent_specification = specification.parent()
                if not parent_specification:
                    logger.error(
                        'logic error: "%s" has no parent', searching_tip_bone_name
                    )
                    break

                # Do nothing if there are already assigned bones in the parent's
                # descendants
                assigned_parent_descendant_found = False
                for parent_descendant in parent_specification.descendants():
                    parent_descendant_human_bone = human_bone_name_to_human_bone.get(
                        parent_descendant.name
                    )
                    if not parent_descendant_human_bone:
                        continue
                    if (
                        parent_descendant_human_bone.node.bone_name
                        in armature_data.edit_bones
                    ):
                        assigned_parent_descendant_found = True
                        break
                if assigned_parent_descendant_found:
                    break

                searching_tip_bone_name = specification.parent_name
            if not bone:
                continue
            if len(bone.children) <= 1:
                continue

            parent_bone = bone.parent
            if not parent_bone:
                continue

            bone.roll = parent_bone.roll
            bone.tail = bone.head + parent_bone.vector / 2

        # When eye bones have no children or multiple children, point them forward.
        # This results in a more natural direction than the default.
        # Since it might be a direction specification by the VRM creator,
        # do nothing when there is only one child.
        for eye_human_bone in [
            human_bone_name_to_human_bone.get(HumanBoneName.LEFT_EYE),
            human_bone_name_to_human_bone.get(HumanBoneName.RIGHT_EYE),
        ]:
            if not eye_human_bone or not eye_human_bone.node.bone_name:
                continue

            bone = armature_data.edit_bones.get(eye_human_bone.node.bone_name)
            if not bone:
                continue
            if len(bone.children) == 1:
                continue

            world_head = (
                armature.matrix_world @ Matrix.Translation(bone.head)
            ).to_translation()

            world_tail = list(world_head)
            world_tail[1] -= 0.03125

            world_inv = armature.matrix_world.inverted()
            if not world_inv:
                continue
            bone.tail = (Matrix.Translation(world_tail) @ world_inv).to_translation()

        # When head bones have no children or multiple children, point them upward.
        # This results in a more natural direction than the default.
        # Since it might be a direction specification by the VRM creator,
        # do nothing when there is only one child.
        for head_human_bone in [human_bone_name_to_human_bone.get(HumanBoneName.HEAD)]:
            if not head_human_bone or not head_human_bone.node.bone_name:
                continue

            bone = armature_data.edit_bones.get(head_human_bone.node.bone_name)
            if not bone:
                continue
            if len(bone.children) == 1:
                continue

            world_head = (
                armature.matrix_world @ Matrix.Translation(bone.head)
            ).to_translation()

            world_tail = list(world_head)

            parent_bone = bone.parent
            world_tail[2] += parent_bone.length if parent_bone else 0.2

            world_inv = armature.matrix_world.inverted()
            if not world_inv:
                continue

            parent_bone = bone.parent
            if parent_bone:
                bone.roll = parent_bone.roll
            bone.tail = (Matrix.Translation(world_tail) @ world_inv).to_translation()

        connect_parent_tail_and_child_head_if_very_close_position(armature_data)
