# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
import itertools
import struct
from collections.abc import Sequence
from os import environ
from pathlib import Path
from typing import Optional

import bpy
from bpy.types import (
    Action,
    Armature,
    Context,
    FCurve,
    Object,
    PoseBone,
)
from mathutils import Euler, Matrix, Quaternion, Vector

from ..common import version
from ..common.convert import Json
from ..common.deep import make_json
from ..common.gl import GL_FLOAT
from ..common.gltf import pack_glb
from ..common.logger import get_logger
from ..common.rotation import (
    ROTATION_MODE_AXIS_ANGLE,
    ROTATION_MODE_EULER,
    ROTATION_MODE_QUATERNION,
    get_rotation_as_quaternion,
)
from ..common.vrm1.human_bone import HumanBoneName, HumanBoneSpecification
from ..common.workspace import save_workspace
from ..editor.extension import get_armature_extension
from ..editor.t_pose import setup_humanoid_t_pose
from ..editor.vrm1.property_group import Vrm1PropertyGroup

logger = get_logger(__name__)


class VrmAnimationExporter:
    @staticmethod
    def execute(context: Context, path: Path, armature: Object) -> set[str]:
        armature_data = armature.data
        if not isinstance(armature_data, Armature):
            return {"CANCELLED"}

        with (
            setup_humanoid_t_pose(context, armature),
            save_workspace(context, armature, mode="POSE"),
        ):
            output_bytes = export_vrm_animation(context, armature)

        path.write_bytes(output_bytes)
        return {"FINISHED"}


def connect_humanoid_node_dicts(
    human_bone_specification: HumanBoneSpecification,
    human_bone_name_to_node_dict: dict[HumanBoneName, dict[str, Json]],
    parent_node_dict: Optional[dict[str, Json]],
    node_dicts: list[dict[str, Json]],
    human_bone_name_to_node_index: dict[HumanBoneName, int],
) -> None:
    current_node_dict = human_bone_name_to_node_dict.get(human_bone_specification.name)
    if isinstance(current_node_dict, dict):
        node_index = len(node_dicts)
        human_bone_name_to_node_index[human_bone_specification.name] = node_index
        node_dicts.append(current_node_dict)
        if parent_node_dict is not None:
            children = parent_node_dict.get("children")
            if not isinstance(children, list):
                children = []
                parent_node_dict["children"] = children
            children.append(node_index)
        parent_node_dict = current_node_dict
    for child in human_bone_specification.children():
        connect_humanoid_node_dicts(
            child,
            human_bone_name_to_node_dict,
            parent_node_dict,
            node_dicts,
            human_bone_name_to_node_index,
        )


def create_node_dicts(
    bone: PoseBone,
    parent_bone: Optional[PoseBone],
    node_dicts: list[dict[str, Json]],
    bone_name_to_node_index: dict[str, int],
) -> int:
    node_index = len(node_dicts)
    node_dict: dict[str, Json] = {"name": bone.name}
    node_dicts.append(node_dict)
    bone_name_to_node_index[bone.name] = node_index

    matrix = parent_bone.matrix.inverted() @ bone.matrix if parent_bone else bone.matrix
    translation = matrix.to_translation()
    rotation = matrix.to_quaternion()
    node_dict["translation"] = [
        translation.x,
        translation.z,
        -translation.y,
    ]
    node_dict["rotation"] = [
        rotation.x,
        rotation.z,
        -rotation.y,
        rotation.w,
    ]
    children = [
        create_node_dicts(child_bone, bone, node_dicts, bone_name_to_node_index)
        for child_bone in bone.children
    ]
    if children:
        node_dict["children"] = make_json(children)

    return node_index


def export_vrm_animation(context: Context, armature: Object) -> bytes:
    armature_data = armature.data
    if not isinstance(armature_data, Armature):
        message = "Armature data is not an Armature"
        raise TypeError(message)
    vrm1 = get_armature_extension(armature_data).vrm1
    human_bones = vrm1.humanoid.human_bones

    node_dicts: list[dict[str, Json]] = []
    bone_name_to_node_index: dict[str, int] = {}
    bone_name_to_base_quaternion: dict[str, Quaternion] = {}
    scene_node_indices: list[int] = [0]
    data_path_to_bone_and_property_name: dict[str, tuple[PoseBone, str]] = {}
    root_node_translation = armature.matrix_world.to_translation()
    root_node_rotation = armature.matrix_world.to_quaternion()
    root_node_scale = armature.matrix_world.to_scale()
    root_node_dict: dict[str, Json] = {
        "name": armature.name,
        "translation": [
            root_node_translation.x,
            root_node_translation.z,
            -root_node_translation.y,
        ],
        "rotation": [
            root_node_rotation.x,
            root_node_rotation.z,
            -root_node_rotation.y,
            root_node_rotation.w,
        ],
        "scale": [
            root_node_scale.x,
            root_node_scale.z,
            root_node_scale.y,
        ],
    }
    root_node_child_indices: list[int] = []
    node_dicts.append(root_node_dict)
    for bone in armature.pose.bones:
        if not bone.parent:
            root_node_child_indices.append(
                create_node_dicts(
                    bone,
                    None,
                    node_dicts,
                    bone_name_to_node_index,
                )
            )

        base_quaternion: Optional[Quaternion] = None
        if bone_parent := bone.parent:
            base_quaternion = (
                bone_parent.matrix.inverted_safe() @ bone.matrix
            ).to_quaternion()
        else:
            base_quaternion = bone.matrix.to_quaternion()
        bone_name_to_base_quaternion[bone.name] = (
            base_quaternion @ get_rotation_as_quaternion(bone).inverted()
        )

        if bone.rotation_mode == ROTATION_MODE_QUATERNION:
            data_path_to_bone_and_property_name[
                bone.path_from_id("rotation_quaternion")
            ] = (bone, "rotation_quaternion")
        elif bone.rotation_mode == ROTATION_MODE_AXIS_ANGLE:
            data_path_to_bone_and_property_name[
                bone.path_from_id("rotation_axis_angle")
            ] = (bone, "rotation_axis_angle")
        elif bone.rotation_mode in ROTATION_MODE_EULER:
            data_path_to_bone_and_property_name[bone.path_from_id("rotation_euler")] = (
                bone,
                "rotation_euler",
            )
        else:
            logger.error(
                "Unexpected rotation mode for bone %s: %s",
                bone.name,
                bone.rotation_mode,
            )

        if human_bones.hips.node.bone_name == bone.name:
            data_path_to_bone_and_property_name[bone.path_from_id("location")] = (
                bone,
                "location",
            )

    if root_node_child_indices:
        root_node_dict["children"] = make_json(root_node_child_indices)

    frame_to_timestamp_factor = context.scene.render.fps_base / float(
        context.scene.render.fps
    )

    buffer0_bytearray = bytearray()
    accessor_dicts: list[dict[str, Json]] = []
    buffer_view_dicts: list[dict[str, Json]] = []
    animation_sampler_dicts: list[dict[str, Json]] = []
    animation_channel_dicts: list[dict[str, Json]] = []
    preset_expression_dict: dict[str, dict[str, Json]] = {}
    custom_expression_dict: dict[str, dict[str, Json]] = {}

    frame_start = context.scene.frame_start
    frame_end = context.scene.frame_end

    create_expression_animation(
        vrm1,
        frame_start=frame_start,
        frame_end=frame_end,
        frame_to_timestamp_factor=frame_to_timestamp_factor,
        armature_data=armature_data,
        node_dicts=node_dicts,
        accessor_dicts=accessor_dicts,
        buffer_view_dicts=buffer_view_dicts,
        animation_channel_dicts=animation_channel_dicts,
        animation_sampler_dicts=animation_sampler_dicts,
        scene_node_indices=scene_node_indices,
        buffer0_bytearray=buffer0_bytearray,
        preset_expression_dict=preset_expression_dict,
        custom_expression_dict=custom_expression_dict,
    )

    create_node_animation(
        vrm1,
        frame_start=frame_start,
        frame_end=frame_end,
        frame_to_timestamp_factor=frame_to_timestamp_factor,
        armature=armature,
        data_path_to_bone_and_property_name=data_path_to_bone_and_property_name,
        bone_name_to_node_index=bone_name_to_node_index,
        bone_name_to_base_quaternion=bone_name_to_base_quaternion,
        buffer0_bytearray=buffer0_bytearray,
        buffer_view_dicts=buffer_view_dicts,
        accessor_dicts=accessor_dicts,
        animation_channel_dicts=animation_channel_dicts,
        animation_sampler_dicts=animation_sampler_dicts,
    )

    look_at_target_node_index = create_look_at_animation(
        vrm1,
        frame_start=frame_start,
        frame_end=frame_end,
        frame_to_timestamp_factor=frame_to_timestamp_factor,
        node_dicts=node_dicts,
        accessor_dicts=accessor_dicts,
        buffer_view_dicts=buffer_view_dicts,
        animation_channel_dicts=animation_channel_dicts,
        animation_sampler_dicts=animation_sampler_dicts,
        buffer0_bytearray=buffer0_bytearray,
    )

    buffer_dicts: list[dict[str, Json]] = [{"byteLength": len(buffer0_bytearray)}]

    human_bones_dict: dict[str, Json] = {}
    human_bone_name_to_human_bone = human_bones.human_bone_name_to_human_bone()
    for human_bone_name, human_bone in human_bone_name_to_human_bone.items():
        bone_name = human_bone.node.bone_name
        node_index = bone_name_to_node_index.get(bone_name)
        if not isinstance(node_index, int):
            continue
        human_bones_dict[human_bone_name.value] = {"node": node_index}

    addon_version = version.get_addon_version()
    if environ.get("BLENDER_VRM_USE_TEST_EXPORTER_VERSION") == "true":
        addon_version = (999, 999, 999)

    vrmc_vrm_animation_dict: dict[str, Json] = {
        "specVersion": "1.0",
        "humanoid": {
            "humanBones": human_bones_dict,
        },
        "expressions": make_json(
            {
                "preset": preset_expression_dict,
                "custom": custom_expression_dict,
            }
        ),
    }
    if look_at_target_node_index is not None:
        vrmc_vrm_animation_dict["lookAt"] = {
            "node": look_at_target_node_index,
            "offsetFromHeadBone": list(vrm1.look_at.offset_from_head_bone),
        }

    vrma_dict = make_json(
        {
            "asset": {
                "version": "2.0",
                "generator": "VRM Add-on for Blender v"
                + ".".join(map(str, addon_version)),
            },
            "nodes": node_dicts,
            "scenes": [{"nodes": scene_node_indices}],
            "buffers": buffer_dicts,
            "bufferViews": buffer_view_dicts,
            "accessors": accessor_dicts,
            "animations": [
                {
                    "channels": animation_channel_dicts,
                    "samplers": animation_sampler_dicts,
                }
            ],
            "extensionsUsed": ["VRMC_vrm_animation"],
            "extensions": {
                "VRMC_vrm_animation": vrmc_vrm_animation_dict,
            },
        }
    )

    if not isinstance(vrma_dict, dict):
        message = "vrma_dict is not a dict"
        raise TypeError(message)

    return pack_glb(vrma_dict, buffer0_bytearray)


def create_look_at_animation(
    vrm1: Vrm1PropertyGroup,
    *,
    frame_start: int,
    frame_end: int,
    frame_to_timestamp_factor: float,
    node_dicts: list[dict[str, Json]],
    accessor_dicts: list[dict[str, Json]],
    buffer_view_dicts: list[dict[str, Json]],
    animation_channel_dicts: list[dict[str, Json]],
    animation_sampler_dicts: list[dict[str, Json]],
    buffer0_bytearray: bytearray,
) -> Optional[int]:
    look_at_target_object = vrm1.look_at.preview_target_bpy_object
    if not look_at_target_object:
        return None

    animation_data = look_at_target_object.animation_data
    if not animation_data:
        return None

    action = animation_data.action
    if not action:
        return None

    look_at_translation_offsets: list[Vector] = []
    data_path = look_at_target_object.path_from_id("location")
    for fcurve in get_action_fcurves(action):
        if fcurve.mute:
            continue
        if not fcurve.is_valid:
            continue
        if fcurve.data_path != data_path:
            continue
        for frame in range(frame_start, frame_end + 1):
            offset = frame - frame_start
            value = float(fcurve.evaluate(frame))
            if offset < len(look_at_translation_offsets):
                translation_offset = look_at_translation_offsets[offset]
            else:
                translation_offset = Vector((0.0, 0.0, 0.0))
                look_at_translation_offsets.append(translation_offset)
            translation_offset[fcurve.array_index] = value

    parent = look_at_target_object.parent
    parent_world_matrix = parent.matrix_world if parent else Matrix()

    look_at_translations = [
        parent_world_matrix @ look_at_translation_offset
        for look_at_translation_offset in look_at_translation_offsets
    ]
    if not look_at_translations:
        return None

    look_at_target_node_index = len(node_dicts)
    look_at_default_node_translation = (
        look_at_target_object.matrix_world.to_translation()
    )
    node_dicts.append(
        {
            "name": look_at_target_object.name,
            "translation": [
                look_at_default_node_translation.x,
                look_at_default_node_translation.z,
                -look_at_default_node_translation.y,
            ],
        }
    )

    input_byte_offset = len(buffer0_bytearray)
    input_floats = [
        frame * frame_to_timestamp_factor
        for frame, _ in enumerate(look_at_translations)
    ]
    input_bytes = struct.pack("<" + "f" * len(input_floats), *input_floats)
    buffer0_bytearray.extend(input_bytes)
    while len(buffer0_bytearray) % 32 != 0:  # TODO: Find the correct alignment
        buffer0_bytearray.append(0)
    input_buffer_view_index = len(buffer_view_dicts)
    input_buffer_view_dict: dict[str, Json] = {
        "buffer": 0,
        "byteLength": len(input_bytes),
    }
    if input_byte_offset > 0:
        input_buffer_view_dict["byteOffset"] = input_byte_offset
    buffer_view_dicts.append(input_buffer_view_dict)

    output_byte_offset = len(buffer0_bytearray)
    gltf_translations = [
        (
            translation.x,
            translation.z,
            -translation.y,
        )
        for translation in look_at_translations
    ]
    translation_floats = list(itertools.chain(*gltf_translations))
    translation_bytes = struct.pack(
        "<" + "f" * len(translation_floats), *translation_floats
    )
    buffer0_bytearray.extend(translation_bytes)
    while len(buffer0_bytearray) % 32 != 0:  # TODO: Find the correct alignment
        buffer0_bytearray.append(0)
    output_buffer_view_index = len(buffer_view_dicts)
    output_buffer_view_dict: dict[str, Json] = {
        "buffer": 0,
        "byteLength": len(translation_bytes),
    }
    if output_byte_offset > 0:
        output_buffer_view_dict["byteOffset"] = output_byte_offset
    buffer_view_dicts.append(output_buffer_view_dict)

    input_accessor_index = len(accessor_dicts)
    accessor_dicts.append(
        {
            "bufferView": input_buffer_view_index,
            "componentType": GL_FLOAT,
            "count": len(input_floats),
            "type": "SCALAR",
            "min": [min(input_floats)],
            "max": [max(input_floats)],
        }
    )

    output_accessor_index = len(accessor_dicts)
    gltf_translation_x_values = [t[0] for t in gltf_translations]
    gltf_translation_y_values = [t[1] for t in gltf_translations]
    gltf_translation_z_values = [t[2] for t in gltf_translations]
    accessor_dicts.append(
        {
            "bufferView": output_buffer_view_index,
            "componentType": GL_FLOAT,
            "count": len(look_at_translations),
            "type": "VEC3",
            "min": [
                min(gltf_translation_x_values),
                min(gltf_translation_y_values),
                min(gltf_translation_z_values),
            ],
            "max": [
                max(gltf_translation_x_values),
                max(gltf_translation_y_values),
                max(gltf_translation_z_values),
            ],
        }
    )

    animation_sampler_index = len(animation_sampler_dicts)
    animation_sampler_dicts.append(
        {
            "input": input_accessor_index,
            "output": output_accessor_index,
        }
    )
    animation_channel_dicts.append(
        {
            "sampler": animation_sampler_index,
            "target": {
                "node": look_at_target_node_index,
                "path": "translation",
            },
        }
    )

    return look_at_target_node_index


def create_expression_animation(
    vrm1: Vrm1PropertyGroup,
    *,
    frame_start: int,
    frame_end: int,
    frame_to_timestamp_factor: float,
    armature_data: Armature,
    node_dicts: list[dict[str, Json]],
    accessor_dicts: list[dict[str, Json]],
    buffer_view_dicts: list[dict[str, Json]],
    animation_channel_dicts: list[dict[str, Json]],
    animation_sampler_dicts: list[dict[str, Json]],
    scene_node_indices: list[int],
    buffer0_bytearray: bytearray,
    preset_expression_dict: dict[str, dict[str, Json]],
    custom_expression_dict: dict[str, dict[str, Json]],
) -> None:
    expression_animation_data = armature_data.animation_data
    if not expression_animation_data:
        return

    action = expression_animation_data.action
    if not action:
        return

    data_path_to_expression_name: dict[str, str] = {}
    for (
        expression_name,
        expression,
    ) in vrm1.expressions.all_name_to_expression_dict().items():
        if expression_name in [
            "lookUp",
            "lookDown",
            "lookLeft",
            "lookRight",
        ]:
            continue
        data_path_to_expression_name[expression.path_from_id("preview")] = (
            expression.name
        )

    expression_name_to_expression_values: dict[
        str, list[tuple[float, float, float]]
    ] = {}

    expression_export_index = 0
    for fcurve in get_action_fcurves(action):
        if fcurve.mute:
            continue
        if not fcurve.is_valid:
            continue
        expression_name = data_path_to_expression_name.get(fcurve.data_path)
        if not expression_name:
            continue
        for frame in range(frame_start, frame_end + 1):
            expression_values = expression_name_to_expression_values.get(
                expression_name
            )
            if not expression_values:
                expression_values = []
                expression_name_to_expression_values[expression_name] = (
                    expression_values
                )
            expression_values.append(
                (
                    max(0, min(fcurve.evaluate(frame), 1)),
                    0,
                    expression_export_index / 8.0,
                )
            )
        expression_export_index += 1

    node_index: Optional[int] = None
    for (
        expression_name,
        expression_translations,
    ) in expression_name_to_expression_values.items():
        node_index = len(node_dicts)
        node_dicts.append(
            {
                "name": expression_name,
            }
        )

        if expression_name in vrm1.expressions.preset.name_to_expression_dict():
            preset_expression_dict[expression_name] = {
                "node": node_index,
            }
        else:
            custom_expression_dict[expression_name] = {
                "node": node_index,
            }
        scene_node_indices.append(node_index)

        input_byte_offset = len(buffer0_bytearray)
        input_floats = [
            frame * frame_to_timestamp_factor
            for frame, _ in enumerate(expression_translations)
        ]
        input_bytes = struct.pack("<" + "f" * len(input_floats), *input_floats)
        buffer0_bytearray.extend(input_bytes)
        while len(buffer0_bytearray) % 32 != 0:  # TODO: Find the correct alignment
            buffer0_bytearray.append(0)
        input_buffer_view_index = len(buffer_view_dicts)
        input_buffer_view_dict: dict[str, Json] = {
            "buffer": 0,
            "byteLength": len(input_bytes),
        }
        if input_byte_offset > 0:
            input_buffer_view_dict["byteOffset"] = input_byte_offset
        buffer_view_dicts.append(input_buffer_view_dict)

        output_byte_offset = len(buffer0_bytearray)
        expression_translation_floats: list[float] = list(
            itertools.chain(*expression_translations)
        )
        translation_bytes = struct.pack(
            "<" + "f" * len(expression_translation_floats),
            *expression_translation_floats,
        )
        buffer0_bytearray.extend(translation_bytes)
        while len(buffer0_bytearray) % 32 != 0:  # TODO: Find the correct alignment
            buffer0_bytearray.append(0)
        output_buffer_view_index = len(buffer_view_dicts)
        output_buffer_view_dict: dict[str, Json] = {
            "buffer": 0,
            "byteLength": len(translation_bytes),
        }
        if output_byte_offset > 0:
            output_buffer_view_dict["byteOffset"] = output_byte_offset
        buffer_view_dicts.append(output_buffer_view_dict)

        input_accessor_index = len(accessor_dicts)
        accessor_dicts.append(
            {
                "bufferView": input_buffer_view_index,
                "componentType": GL_FLOAT,
                "count": len(input_floats),
                "type": "SCALAR",
                "min": [min(input_floats)],
                "max": [max(input_floats)],
            }
        )

        output_accessor_index = len(accessor_dicts)
        accessor_dicts.append(
            {
                "bufferView": output_buffer_view_index,
                "componentType": GL_FLOAT,
                "count": len(expression_translations),
                "type": "VEC3",
                "min": [
                    min(values)
                    for values in [
                        [
                            gltf_translation[i]
                            for gltf_translation in expression_translations
                        ]
                        for i in range(3)
                    ]
                ],
                "max": [
                    max(values)
                    for values in [
                        [
                            gltf_translation[i]
                            for gltf_translation in expression_translations
                        ]
                        for i in range(3)
                    ]
                ],
            }
        )

        animation_sampler_index = len(animation_sampler_dicts)
        animation_sampler_dicts.append(
            {
                "input": input_accessor_index,
                "output": output_accessor_index,
            }
        )
        animation_channel_dicts.append(
            {
                "sampler": animation_sampler_index,
                "target": {"node": node_index, "path": "translation"},
            }
        )


def create_node_animation(
    vrm1: Vrm1PropertyGroup,
    *,
    frame_start: int,
    frame_end: int,
    frame_to_timestamp_factor: float,
    armature: Object,
    data_path_to_bone_and_property_name: dict[str, tuple[PoseBone, str]],
    bone_name_to_node_index: dict[str, int],
    bone_name_to_base_quaternion: dict[str, Quaternion],
    buffer0_bytearray: bytearray,
    buffer_view_dicts: list[dict[str, Json]],
    accessor_dicts: list[dict[str, Json]],
    animation_channel_dicts: list[dict[str, Json]],
    animation_sampler_dicts: list[dict[str, Json]],
) -> None:
    human_bones = vrm1.humanoid.human_bones
    human_bone_name_to_human_bone = human_bones.human_bone_name_to_human_bone()

    animation_data = armature.animation_data
    if not animation_data:
        return

    action = animation_data.action
    if not action:
        return

    bone_name_to_quaternion_offsets: dict[str, list[Quaternion]] = {}
    bone_name_to_euler_offsets: dict[str, list[Euler]] = {}
    bone_name_to_axis_angle_offsets: dict[str, list[list[float]]] = {}
    hips_translation_offsets: list[Vector] = []
    for fcurve in get_action_fcurves(action):
        if fcurve.mute:
            continue
        if not fcurve.is_valid:
            continue

        bone_and_property_name = data_path_to_bone_and_property_name.get(
            fcurve.data_path
        )
        if not bone_and_property_name:
            continue
        bone, property_name = bone_and_property_name
        for frame in range(frame_start, frame_end + 1):
            offset = frame - frame_start
            value = float(fcurve.evaluate(frame))
            if property_name == "rotation_quaternion":
                quaternion_offsets = bone_name_to_quaternion_offsets.get(bone.name)
                if quaternion_offsets is None:
                    quaternion_offsets = []
                    bone_name_to_quaternion_offsets[bone.name] = quaternion_offsets
                if offset < len(quaternion_offsets):
                    quaternion_offset = quaternion_offsets[offset]
                else:
                    quaternion_offset = Quaternion()
                    quaternion_offsets.append(quaternion_offset)
                quaternion_offset[fcurve.array_index] = value
            elif property_name == "rotation_axis_angle":
                axis_angle_offsets = bone_name_to_axis_angle_offsets.get(bone.name)
                if axis_angle_offsets is None:
                    axis_angle_offsets = []
                    bone_name_to_axis_angle_offsets[bone.name] = axis_angle_offsets
                if offset < len(axis_angle_offsets):
                    axis_angle_offset = axis_angle_offsets[offset]
                else:
                    axis_angle_offset = [0.0, 0.0, 0.0, 0.0]
                    axis_angle_offsets.append(axis_angle_offset)
                axis_angle_offset[fcurve.array_index] = value
            elif property_name == "rotation_euler":
                euler_offsets = bone_name_to_euler_offsets.get(bone.name)
                if euler_offsets is None:
                    euler_offsets = []
                    bone_name_to_euler_offsets[bone.name] = euler_offsets
                if offset < len(euler_offsets):
                    euler_offset = euler_offsets[offset]
                else:
                    euler_offset = Euler((0, 0, 0))
                    euler_offsets.append(euler_offset)
                indices = {
                    "XYZ": [0, 1, 2],
                    "XZY": [0, 2, 1],
                    "YXZ": [1, 0, 2],
                    "YZX": [1, 2, 0],
                    "ZXY": [2, 0, 1],
                    "ZYX": [2, 1, 0],
                }
                index = indices.get(bone.rotation_mode)
                if index is None:
                    continue
                euler_offset[index[fcurve.array_index]] = value
            elif property_name == "location":
                if offset < len(hips_translation_offsets):
                    translation_offset = hips_translation_offsets[offset]
                else:
                    translation_offset = Vector((0.0, 0.0, 0.0))
                    hips_translation_offsets.append(translation_offset)
                translation_offset[fcurve.array_index] = value

    bone_name_to_quaternions: dict[str, list[Quaternion]] = {}
    for bone_name, quaternion_offsets in bone_name_to_quaternion_offsets.items():
        base_quaternion = bone_name_to_base_quaternion.get(bone_name)
        if base_quaternion is None:
            continue
        bone_name_to_quaternions[bone_name] = [
            # Muted items and other factors may cause quaternion values to be
            # denormalized, so we normalize them
            base_quaternion @ quaternion_offset.normalized()
            for quaternion_offset in quaternion_offsets
        ]
    for bone_name, euler_offsets in bone_name_to_euler_offsets.items():
        base_quaternion = bone_name_to_base_quaternion.get(bone_name)
        if base_quaternion is None:
            continue
        bone_name_to_quaternions[bone_name] = [
            base_quaternion @ euler.to_quaternion() for euler in euler_offsets
        ]
    for bone_name, axis_angle_offsets in bone_name_to_axis_angle_offsets.items():
        base_quaternion = bone_name_to_base_quaternion.get(bone_name)
        if base_quaternion is None:
            continue
        bone_name_to_quaternions[bone_name] = [
            base_quaternion
            @ Quaternion(
                (axis_angle_offset[0], axis_angle_offset[1], axis_angle_offset[2]),
                axis_angle_offset[3],
            ).normalized()
            for axis_angle_offset in axis_angle_offsets
        ]

    # Export rotation
    for bone_name, quaternions in bone_name_to_quaternions.items():
        human_bone_name = next(
            (
                n
                for n, human_bone in human_bone_name_to_human_bone.items()
                if human_bone.node.bone_name == bone_name
            ),
            None,
        )
        if human_bone_name is None:
            logger.error("Failed to find human bone name for bone %s", bone_name)
            continue
        if human_bone_name in [HumanBoneName.RIGHT_EYE, HumanBoneName.LEFT_EYE]:
            continue
        node_index = bone_name_to_node_index.get(bone_name)
        if not isinstance(node_index, int):
            logger.error("Failed to find node index for bone %s", bone_name)
            continue

        input_byte_offset = len(buffer0_bytearray)
        input_floats = [
            frame * frame_to_timestamp_factor for frame, _ in enumerate(quaternions)
        ]
        input_bytes = struct.pack("<" + "f" * len(input_floats), *input_floats)
        buffer0_bytearray.extend(input_bytes)
        while len(buffer0_bytearray) % 32 != 0:  # TODO: Find the correct alignment
            buffer0_bytearray.append(0)
        input_buffer_view_index = len(buffer_view_dicts)
        input_buffer_view_dict: dict[str, Json] = {
            "buffer": 0,
            "byteLength": len(input_bytes),
        }
        if input_byte_offset > 0:
            input_buffer_view_dict["byteOffset"] = input_byte_offset
        buffer_view_dicts.append(input_buffer_view_dict)

        output_byte_offset = len(buffer0_bytearray)
        gltf_quaternions = [
            (
                quaternion.x,
                quaternion.z,
                -quaternion.y,
                quaternion.w,
            )
            for quaternion in quaternions
        ]
        quaternion_floats: list[float] = list(itertools.chain(*gltf_quaternions))
        quaternion_bytes = struct.pack(
            "<" + "f" * len(quaternion_floats), *quaternion_floats
        )
        buffer0_bytearray.extend(quaternion_bytes)
        while len(buffer0_bytearray) % 32 != 0:  # TODO: Find the correct alignment
            buffer0_bytearray.append(0)
        output_buffer_view_index = len(buffer_view_dicts)
        output_buffer_view_dict: dict[str, Json] = {
            "buffer": 0,
            "byteLength": len(quaternion_bytes),
        }
        if output_byte_offset > 0:
            output_buffer_view_dict["byteOffset"] = output_byte_offset
        buffer_view_dicts.append(output_buffer_view_dict)

        input_accessor_index = len(accessor_dicts)
        accessor_dicts.append(
            {
                "bufferView": input_buffer_view_index,
                "componentType": GL_FLOAT,
                "count": len(input_floats),
                "type": "SCALAR",
                "min": [min(input_floats)],
                "max": [max(input_floats)],
            }
        )

        output_accessor_index = len(accessor_dicts)
        accessor_dicts.append(
            {
                "bufferView": output_buffer_view_index,
                "componentType": GL_FLOAT,
                "count": len(quaternions),
                "type": "VEC4",
                "min": [
                    min(values)
                    for values in [
                        [gltf_quaternion[i] for gltf_quaternion in gltf_quaternions]
                        for i in range(4)
                    ]
                ],
                "max": [
                    max(values)
                    for values in [
                        [gltf_quaternion[i] for gltf_quaternion in gltf_quaternions]
                        for i in range(4)
                    ]
                ],
            }
        )

        animation_sampler_index = len(animation_sampler_dicts)
        animation_sampler_dicts.append(
            {
                "input": input_accessor_index,
                "output": output_accessor_index,
            }
        )
        animation_channel_dicts.append(
            {
                "sampler": animation_sampler_index,
                "target": {"node": node_index, "path": "rotation"},
            }
        )

    # Export hips translation
    hips_bone_name = human_bones.hips.node.bone_name

    hips_bone = armature.pose.bones.get(hips_bone_name)
    if not hips_bone:
        return

    hips_node_index = bone_name_to_node_index.get(hips_bone_name)
    if not isinstance(hips_node_index, int):
        return

    if hips_bone.parent:
        base_matrix = hips_bone.parent.matrix.inverted_safe()
    else:
        base_matrix = Matrix()
    hips_translations = [
        # TODO: Find the correct alignment
        base_matrix @ hips_bone.matrix @ hips_translation_offset
        for hips_translation_offset in hips_translation_offsets
    ]

    if not hips_translations:
        return

    input_byte_offset = len(buffer0_bytearray)
    input_floats = [
        frame * frame_to_timestamp_factor for frame, _ in enumerate(hips_translations)
    ]
    input_bytes = struct.pack("<" + "f" * len(input_floats), *input_floats)
    buffer0_bytearray.extend(input_bytes)
    while len(buffer0_bytearray) % 32 != 0:  # TODO: Find the correct alignment
        buffer0_bytearray.append(0)
    input_buffer_view_index = len(buffer_view_dicts)
    input_buffer_view_dict = {
        "buffer": 0,
        "byteLength": len(input_bytes),
    }
    if input_byte_offset > 0:
        input_buffer_view_dict["byteOffset"] = input_byte_offset
    buffer_view_dicts.append(input_buffer_view_dict)

    output_byte_offset = len(buffer0_bytearray)
    gltf_translations = [
        (
            translation.x,
            translation.z,
            -translation.y,
        )
        for translation in hips_translations
    ]
    translation_floats: list[float] = list(itertools.chain(*gltf_translations))
    translation_bytes = struct.pack(
        "<" + "f" * len(translation_floats), *translation_floats
    )
    buffer0_bytearray.extend(translation_bytes)
    while len(buffer0_bytearray) % 32 != 0:  # TODO: Find the correct alignment
        buffer0_bytearray.append(0)
    output_buffer_view_index = len(buffer_view_dicts)
    output_buffer_view_dict = {
        "buffer": 0,
        "byteLength": len(translation_bytes),
    }
    if output_byte_offset > 0:
        output_buffer_view_dict["byteOffset"] = output_byte_offset
    buffer_view_dicts.append(output_buffer_view_dict)

    input_accessor_index = len(accessor_dicts)
    accessor_dicts.append(
        {
            "bufferView": input_buffer_view_index,
            "componentType": GL_FLOAT,
            "count": len(input_floats),
            "type": "SCALAR",
            "min": [min(input_floats)],
            "max": [max(input_floats)],
        }
    )

    output_accessor_index = len(accessor_dicts)
    gltf_translation_x_values = [t[0] for t in gltf_translations]
    gltf_translation_y_values = [t[1] for t in gltf_translations]
    gltf_translation_z_values = [t[2] for t in gltf_translations]
    accessor_dicts.append(
        {
            "bufferView": output_buffer_view_index,
            "componentType": GL_FLOAT,
            "count": len(hips_translations),
            "type": "VEC3",
            "min": [
                min(gltf_translation_x_values),
                min(gltf_translation_y_values),
                min(gltf_translation_z_values),
            ],
            "max": [
                max(gltf_translation_x_values),
                max(gltf_translation_y_values),
                max(gltf_translation_z_values),
            ],
        }
    )

    animation_sampler_index = len(animation_sampler_dicts)
    animation_sampler_dicts.append(
        {
            "input": input_accessor_index,
            "output": output_accessor_index,
        }
    )
    animation_channel_dicts.append(
        {
            "sampler": animation_sampler_index,
            "target": {"node": hips_node_index, "path": "translation"},
        }
    )


def get_action_fcurves(action: Action) -> Sequence[FCurve]:
    if bpy.app.version < (4, 4):
        return list(action.fcurves)

    from bpy.types import ActionKeyframeStrip

    # https://developer.blender.org/docs/release_notes/4.4/python_api/#deprecated
    layers = action.layers
    if not layers:
        return []
    layer = layers[0]

    strips = layer.strips
    if not strips:
        return []
    strip = strips[0]
    if not isinstance(strip, ActionKeyframeStrip):
        return []

    slots = action.slots
    if not slots:
        return []
    slot = slots[0]

    channelbag = strip.channelbag(slot)
    return list(channelbag.fcurves)
