# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
from bpy.types import Armature, Context, Material, Object
from idprop.types import IDPropertyGroup
from mathutils import Vector

from ...common import convert, ops, shader
from ..extension import get_armature_extension
from .property_group import (
    Vrm1ExpressionPropertyGroup,
    Vrm1ExpressionsPropertyGroup,
    Vrm1HumanBonesPropertyGroup,
    Vrm1MaterialColorBindPropertyGroup,
    Vrm1PropertyGroup,
)


def migrate_old_expression_layout(
    old_expression: object, expression: Vrm1ExpressionPropertyGroup
) -> None:
    if not isinstance(old_expression, IDPropertyGroup):
        return

    old_morph_target_binds = convert.iterator_or_none(
        old_expression.get("morph_target_binds")
    )
    if old_morph_target_binds is not None:
        for old_morph_target_bind in old_morph_target_binds:
            if not isinstance(old_morph_target_bind, IDPropertyGroup):
                continue

            morph_target_bind = expression.morph_target_binds.add()
            old_node = old_morph_target_bind.get("node")
            if isinstance(old_node, IDPropertyGroup):
                old_bpy_object = old_node.get("bpy_object")
                if isinstance(old_bpy_object, Object) and old_bpy_object.type == "MESH":
                    morph_target_bind.node.mesh_object_name = old_bpy_object.name
                old_node.clear()

            old_index = old_morph_target_bind.get("index")
            if isinstance(old_index, str):
                morph_target_bind.index = old_index

            old_weight = old_morph_target_bind.get("weight")
            if isinstance(old_weight, (float, int)):
                morph_target_bind.weight = old_weight

            old_morph_target_bind.clear()

    old_material_color_binds = convert.iterator_or_none(
        old_expression.get("material_color_binds")
    )
    if old_material_color_binds is not None:
        for old_material_color_bind in old_material_color_binds:
            if not isinstance(old_material_color_bind, IDPropertyGroup):
                continue

            material_color_bind = expression.material_color_binds.add()
            old_material = old_material_color_bind.get("material")
            if isinstance(old_material, Material):
                material_color_bind.material = old_material

            old_type = next(
                (
                    enum.identifier
                    for enum in Vrm1MaterialColorBindPropertyGroup.type_enum
                    if old_material_color_bind.get("type") == enum.value
                ),
                None,
            )
            if old_type is not None:
                material_color_bind.type = old_type

            old_target_value = shader.rgba_or_none(
                old_material_color_bind.get("target_value"), 0.0, 1.0
            )
            if old_target_value:
                material_color_bind.target_value = old_target_value

            old_material_color_bind.clear()

    old_texture_transform_binds = convert.iterator_or_none(
        old_expression.get("texture_transform_binds")
    )
    if old_texture_transform_binds is not None:
        for old_texture_transform_bind in old_texture_transform_binds:
            if not isinstance(old_texture_transform_bind, IDPropertyGroup):
                continue

            texture_transform_bind = expression.texture_transform_binds.add()
            old_material = old_texture_transform_bind.get("material")
            if isinstance(old_material, Material):
                texture_transform_bind.material = old_material

            old_scale = convert.float2_or_none(old_texture_transform_bind.get("scale"))
            if old_scale:
                texture_transform_bind.scale = old_scale

            old_offset = convert.float2_or_none(
                old_texture_transform_bind.get("offset")
            )
            if old_offset:
                texture_transform_bind.offset = old_offset

            old_texture_transform_bind.clear()

    old_is_binary = old_expression.get("is_binary")
    if isinstance(old_is_binary, int):
        expression.is_binary = bool(old_is_binary)

    old_override_blink = next(
        (
            enum.identifier
            for enum in Vrm1ExpressionPropertyGroup.expression_override_type_enum
            if old_expression.get("override_blink") == enum.value
        ),
        None,
    )
    if old_override_blink is not None:
        expression.override_blink = old_override_blink

    old_override_look_at = next(
        (
            enum.identifier
            for enum in Vrm1ExpressionPropertyGroup.expression_override_type_enum
            if old_expression.get("override_look_at") == enum.value
        ),
        None,
    )
    if old_override_look_at is not None:
        expression.override_look_at = old_override_look_at

    old_override_mouth = next(
        (
            enum.identifier
            for enum in Vrm1ExpressionPropertyGroup.expression_override_type_enum
            if old_expression.get("override_mouth") == enum.value
        ),
        None,
    )
    if old_override_mouth is not None:
        expression.override_mouth = old_override_mouth


def migrate_old_expressions_layout(expressions: Vrm1ExpressionsPropertyGroup) -> None:
    for name, expression in expressions.preset.name_to_expression_dict().items():
        property_name = {
            "blinkLeft": "blink_left",
            "blinkRight": "blink_right",
            "lookUp": "look_up",
            "lookDown": "look_down",
            "lookLeft": "look_left",
            "lookRight": "look_right",
        }.get(name)
        if property_name is None:
            property_name = name
        old_expression = expressions.get(property_name)
        migrate_old_expression_layout(old_expression, expression)
        expression.name = name
    for expression in expressions.custom:
        old_expression = expression.get("expression")
        migrate_old_expression_layout(old_expression, expression)


def migrate_pose(context: Context, armature: Object, armature_data: Armature) -> None:
    ext = get_armature_extension(armature_data)
    if tuple(ext.addon_version) >= (2, 20, 34):
        return

    humanoid = ext.vrm1.humanoid
    if isinstance(humanoid.get("pose"), int):
        return

    if tuple(ext.addon_version) == ext.INITIAL_ADDON_VERSION:
        if ext.has_vrm_model_metadata(armature):
            humanoid.pose = humanoid.POSE_CURRENT_POSE.identifier
        return

    action = humanoid.pose_library
    if action and action.name in context.blend_data.actions:
        humanoid.pose = humanoid.POSE_CUSTOM_POSE.identifier
    elif armature_data.pose_position == "REST":
        humanoid.pose = humanoid.POSE_REST_POSITION_POSE.identifier
    else:
        humanoid.pose = humanoid.POSE_CURRENT_POSE.identifier


def migrate_auto_pose(_context: Context, armature_data: Armature) -> None:
    ext = get_armature_extension(armature_data)
    if tuple(ext.addon_version) == ext.INITIAL_ADDON_VERSION or tuple(
        ext.addon_version
    ) >= (2, 20, 81):
        return

    humanoid = ext.vrm1.humanoid
    if not isinstance(humanoid.get("pose"), int):
        humanoid.pose = humanoid.POSE_CURRENT_POSE.identifier


def migrate(context: Context, vrm1: Vrm1PropertyGroup, armature: Object) -> None:
    armature_data = armature.data
    if not isinstance(armature_data, Armature):
        return

    human_bones = vrm1.humanoid.human_bones
    human_bones.last_bone_names_str = ""
    Vrm1HumanBonesPropertyGroup.fixup_human_bones(armature)
    Vrm1HumanBonesPropertyGroup.update_all_node_candidates(context, armature_data.name)

    if human_bones.initial_automatic_bone_assignment:
        human_bones.initial_automatic_bone_assignment = False
        human_bone_name_to_human_bone = human_bones.human_bone_name_to_human_bone()
        if all(not b.node.bone_name for b in human_bone_name_to_human_bone.values()):
            ops.vrm.assign_vrm1_humanoid_human_bones_automatically(
                armature_object_name=armature.name
            )

    if tuple(get_armature_extension(armature_data).addon_version) <= (2, 14, 10):
        ext = get_armature_extension(armature_data)
        head_bone_name = ext.vrm1.humanoid.human_bones.head.node.bone_name
        head_bone = armature_data.bones.get(head_bone_name)
        if head_bone:
            look_at = get_armature_extension(armature_data).vrm1.look_at
            world_translation = (
                armature.matrix_world @ head_bone.matrix_local
            ).to_quaternion() @ Vector(look_at.offset_from_head_bone)
            look_at.offset_from_head_bone = list(world_translation)

    if tuple(get_armature_extension(armature_data).addon_version) <= (2, 15, 5):
        # Apply lower limit value
        look_at = get_armature_extension(armature_data).vrm1.look_at
        look_at.range_map_horizontal_inner.input_max_value = (
            look_at.range_map_horizontal_inner.input_max_value
        )
        look_at.range_map_horizontal_outer.input_max_value = (
            look_at.range_map_horizontal_outer.input_max_value
        )
        look_at.range_map_vertical_down.input_max_value = (
            look_at.range_map_vertical_down.input_max_value
        )
        look_at.range_map_vertical_up.input_max_value = (
            look_at.range_map_vertical_up.input_max_value
        )

    if tuple(get_armature_extension(armature_data).addon_version) < (2, 18, 0):
        migrate_old_expressions_layout(
            get_armature_extension(armature_data).vrm1.expressions
        )

    if tuple(get_armature_extension(armature_data).addon_version) < (2, 20, 0):
        look_at = get_armature_extension(armature_data).vrm1.look_at
        look_at.offset_from_head_bone = (
            look_at.offset_from_head_bone[0],
            look_at.offset_from_head_bone[2],
            -look_at.offset_from_head_bone[1],
        )

    migrate_pose(context, armature, armature_data)
    migrate_auto_pose(context, armature_data)

    # Set a name for the expression preset.
    # It's not necessary for management, but I want to set it because it's
    # displayed in the animation keyframes.
    expressions = get_armature_extension(armature_data).vrm1.expressions
    preset_name_to_expression_dict = expressions.preset.name_to_expression_dict()
    for preset_name, preset_expression in preset_name_to_expression_dict.items():
        if preset_expression.name != preset_name:
            preset_expression.name = preset_name

    if tuple(get_armature_extension(armature_data).addon_version) < (3, 9, 0):
        for expression in expressions.preset.name_to_expression_dict().values():
            for morph_target_bind in expression.morph_target_binds:
                morph_target_bind.node.saved_mesh_object_name_to_restore = (
                    morph_target_bind.node.mesh_object_name
                )

    Vrm1HumanBonesPropertyGroup.update_all_node_candidates(
        context,
        armature_data.name,
        force=True,
    )
    ops.vrm.update_vrm1_expression_ui_list_elements()
