# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
import math
import re
from sys import float_info

from mathutils import Quaternion

from ..common import deep, gltf
from ..common.convert import Json
from ..common.deep import make_json
from ..common.gltf import read_accessors


def human_bone_sort_key(human_bone_dict: Json) -> int:
    if not isinstance(human_bone_dict, dict):
        return -1
    node = human_bone_dict.get("node")
    if not isinstance(node, int):
        return -1
    return node


asset_generator_pattern = (
    r"\AVRM Add-on for Blender v999\.999\.999"
    + r" with Khronos glTF Blender I/O v[0-9]+\.[0-9]+\.[0-9]+\Z"
)

fixed_asset_generator_value = (
    "VRM Add-on for Blender v999.999.999"
    + " with Khronos glTF Blender I/O v999.999.999"
)


def create_vrm_json_dict(data: bytes) -> dict[str, Json]:
    json_dict, buffer0_bytes = gltf.parse_glb(data)
    json_dict["__decoded_accessors"] = make_json(
        read_accessors(json_dict, buffer0_bytes)
    )

    extensions_dict = json_dict.get("extensions")
    if isinstance(extensions_dict, dict):
        is_vrm0 = bool(extensions_dict.get("VRM"))
    else:
        is_vrm0 = False

    asset_dict = json_dict.get("asset")
    if isinstance(asset_dict, dict):
        asset_generator = asset_dict.get("generator")
        if isinstance(asset_generator, str):
            asset_dict["generator"] = re.sub(
                asset_generator_pattern,
                fixed_asset_generator_value,
                asset_generator,
            )

    node_dicts = json_dict.get("nodes")
    if isinstance(node_dicts, list):
        for node_dict in node_dicts:
            if not isinstance(node_dict, dict):
                continue

            if is_vrm0:
                skin_index = node_dict.get("skin")
                if isinstance(skin_index, int):
                    skin_dicts = json_dict.get("skins")
                    if (
                        isinstance(skin_dicts, list)
                        and skin_dicts
                        and 0 <= skin_index < len(skin_dicts)
                        and not deep.diff(skin_dicts[0], skin_dicts[skin_index])
                    ):
                        node_dict["skin"] = 0

            if "matrix" in node_dict:
                continue

            if "scale" not in node_dict:
                node_dict["scale"] = [1.0, 1.0, 1.0]

            rotation = node_dict.get("rotation")
            if not isinstance(rotation, list) or len(rotation) != 4:
                continue

            rotation_x, rotation_y, rotation_z, rotation_w = rotation
            if (
                not isinstance(rotation_x, (float, int))
                or not isinstance(rotation_y, (float, int))
                or not isinstance(rotation_z, (float, int))
                or not isinstance(rotation_w, (float, int))
            ):
                node_dict["rotation"] = [0.0, 0.0, 0.0, 1.0]
                continue

            axis, angle = Quaternion(
                (rotation_w, rotation_x, rotation_y, rotation_z)
            ).to_axis_angle()
            if abs(angle - math.pi) > 0.000001:
                continue

            if abs(axis.x) > abs(axis.y) and abs(axis.x) > abs(axis.z):
                if axis.x < 0:
                    axis = -axis
            elif abs(axis.y) > abs(axis.z):
                if axis.y < 0:
                    axis = -axis
            elif axis.z < 0:
                axis = -axis

            q = Quaternion(axis, angle).normalized()
            node_dict["rotation"] = [q.x, q.y, q.z, q.w]

        if is_vrm0:
            skin_dicts = json_dict.get("skins")
            if isinstance(skin_dicts, list) and skin_dicts:
                first_skin_dict = skin_dicts[0]
                if all(
                    not deep.diff(skin_dict, first_skin_dict)
                    for skin_dict in skin_dicts
                    if isinstance(skin_dict, dict)
                ):
                    while len(skin_dicts) > 1:
                        skin_dicts.pop()

    scene_dicts = json_dict.get("scenes")
    if isinstance(scene_dicts, list):
        for scene_dict in scene_dicts:
            if not isinstance(scene_dict, dict):
                continue
            scene_extras_dict = scene_dict.get("extras")
            if not isinstance(scene_extras_dict, dict):
                continue
            for k in ["show_mmd_tabs", "embed_textures", "ui_lang"]:
                scene_extras_dict.pop(k, None)
            if not scene_extras_dict:
                scene_dict.pop("extras", None)

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

    vrm0_extension_dict = extensions_dict.get("VRM")
    if not isinstance(vrm0_extension_dict, dict):
        return json_dict

    vrm0_first_person_dict = vrm0_extension_dict.get("firstPerson")
    if not isinstance(vrm0_first_person_dict, dict):
        vrm0_first_person_dict = {}
        vrm0_extension_dict["firstPerson"] = vrm0_first_person_dict

    vrm0_humanoid_dict = vrm0_extension_dict.get("humanoid")
    if isinstance(vrm0_humanoid_dict, dict):
        vrm0_human_bone_dicts = vrm0_humanoid_dict.get("humanBones")
        if isinstance(vrm0_human_bone_dicts, list):
            vrm0_humanoid_dict["humanBones"] = sorted(
                vrm0_human_bone_dicts, key=human_bone_sort_key
            )

            vrm0_first_person_bone = vrm0_first_person_dict.get("firstPersonBone")
            if vrm0_first_person_bone in [None, -1]:
                for vrm0_human_bone_dict in vrm0_human_bone_dicts:
                    if not isinstance(vrm0_human_bone_dict, dict):
                        continue
                    node = vrm0_human_bone_dict.get("node")
                    if not isinstance(node, int) or node < 0:
                        continue
                    if vrm0_human_bone_dict.get("bone") == "head":
                        vrm0_first_person_dict["firstPersonBone"] = node
                        break

    for look_at_key in [
        "lookAtHorizontalInner",
        "lookAtHorizontalOuter",
        "lookAtVerticalDown",
        "lookAtVerticalUp",
    ]:
        if look_at_key not in vrm0_first_person_dict:
            vrm0_first_person_dict[look_at_key] = {}
        look_at_dict = vrm0_first_person_dict[look_at_key]
        if not isinstance(look_at_dict, dict):
            continue
        if "curve" not in look_at_dict:
            look_at_dict["curve"] = [0, 0, 0, 1, 1, 1, 1, 0]

    vrm0_blend_shape_master_dict = vrm0_extension_dict.get("blendShapeMaster")
    if isinstance(vrm0_blend_shape_master_dict, dict):
        vrm0_blend_shape_group_dicts = vrm0_blend_shape_master_dict.get(
            "blendShapeGroups"
        )
        if isinstance(vrm0_blend_shape_group_dicts, list):
            for vrm0_blend_shape_group_dict in vrm0_blend_shape_group_dicts:
                if not isinstance(vrm0_blend_shape_group_dict, dict):
                    continue
                if "isBinary" not in vrm0_blend_shape_group_dict:
                    vrm0_blend_shape_group_dict["isBinary"] = False
                if "binds" not in vrm0_blend_shape_group_dict:
                    vrm0_blend_shape_group_dict["binds"] = []
                if "materialValues" not in vrm0_blend_shape_group_dict:
                    vrm0_blend_shape_group_dict["materialValues"] = []

    vrm0_secondary_animation = vrm0_extension_dict.get("secondaryAnimation")
    if isinstance(vrm0_secondary_animation, dict):
        vrm0_collider_group_dicts = vrm0_secondary_animation.get("colliderGroups")
        if isinstance(vrm0_collider_group_dicts, list):

            def sort_collider_groups_with_index_key(
                collider_group_with_index: tuple[int, Json],
            ) -> int:
                (_, collider_group) = collider_group_with_index
                if not isinstance(collider_group, dict):
                    return -999
                node = collider_group.get("node")
                if not isinstance(node, int):
                    return -999
                return node

            sorted_collider_groups_with_original_index = sorted(
                enumerate(vrm0_collider_group_dicts),
                key=sort_collider_groups_with_index_key,
            )
            vrm0_collider_group_dicts = [
                collider_group
                for _, collider_group in sorted_collider_groups_with_original_index
            ]
            original_index_to_sorted_index = {
                original_index: sorted_index
                for (sorted_index, (original_index, _)) in enumerate(
                    sorted_collider_groups_with_original_index
                )
            }

            vrm0_bone_groups = vrm0_secondary_animation.get("boneGroups")
            if isinstance(vrm0_bone_groups, list):
                for vrm0_bone_group in vrm0_bone_groups:
                    if not isinstance(vrm0_bone_group, dict):
                        continue
                    if "comment" not in vrm0_bone_group:
                        vrm0_bone_group["comment"] = ""
                    collider_groups = vrm0_bone_group.get("colliderGroups")
                    if not isinstance(collider_groups, list):
                        continue
                    for i, collider_group in enumerate(list(collider_groups)):
                        if not isinstance(collider_group, int):
                            continue
                        if collider_group not in original_index_to_sorted_index:
                            collider_groups[i] = -999
                            continue
                        collider_groups[i] = original_index_to_sorted_index[
                            collider_group
                        ]

    vrm0_material_properties = vrm0_extension_dict.get("materialProperties")
    if isinstance(vrm0_material_properties, list):
        for vrm0_material_property in vrm0_material_properties:
            if not isinstance(vrm0_material_property, dict):
                continue
            if vrm0_material_property.get("shader") != "VRM/MToon":
                continue

            keyword_map = vrm0_material_property.get("keywordMap")
            if not isinstance(keyword_map, dict):
                keyword_map = {}

            vrm0_float_properties = vrm0_material_property.get("floatProperties")
            if isinstance(vrm0_float_properties, dict):
                if vrm0_float_properties.get("_OutlineWidthMode", 0) == 0:
                    vrm0_float_properties["_OutlineWidth"] = 0
                    vrm0_float_properties["_OutlineColorMode"] = 0
                    vrm0_float_properties["_OutlineLightingMix"] = 0
                    keyword_map.pop("MTOON_OUTLINE_COLOR_FIXED", None)
                    keyword_map.pop("MTOON_OUTLINE_COLOR_MIXED", None)

                if vrm0_float_properties.get("_OutlineWidthMode") != 2:
                    vrm0_float_properties["_OutlineScaledMaxDistance"] = 1

                if vrm0_float_properties.get("_OutlineColorMode") == 0:
                    vrm0_float_properties["_OutlineLightingMix"] = 0

                outline_lighting_mix = vrm0_float_properties.get("_OutlineLightingMix")
                if (
                    isinstance(outline_lighting_mix, (float, int))
                    and abs(outline_lighting_mix) < float_info.epsilon
                ):
                    vrm0_float_properties["_OutlineColorMode"] = 0
                    keyword_map.pop("MTOON_OUTLINE_COLOR_MIXED", None)
                    if "MTOON_OUTLINE_COLOR_FIXED" not in keyword_map:
                        keyword_map["MTOON_OUTLINE_COLOR_FIXED"] = True

            vrm0_vector_properties = vrm0_material_property.get("vectorProperties")
            if isinstance(vrm0_vector_properties, dict):
                vrm0_emission_color = vrm0_vector_properties.get("_EmissionColor")
                if (
                    isinstance(vrm0_emission_color, list)
                    and len(vrm0_emission_color) == 4
                ):
                    vrm0_emission_color[3] = 1

                vrm0_outline_color = vrm0_vector_properties.get("_OutlineColor")
                if (
                    isinstance(vrm0_outline_color, list)
                    and len(vrm0_outline_color) == 4
                ):
                    vrm0_outline_color[3] = 1

    return json_dict


def vrm_diff(before: bytes, after: bytes, float_tolerance: float) -> list[str]:
    return deep.diff(
        create_vrm_json_dict(before), create_vrm_json_dict(after), float_tolerance
    )
