/**
 * EmoteGlbParser — конвертирует Babylon AnimationGroup (из GLB-файла) в
 * R15Animator-spec формат: { length, loop, perBoneFrames }.
 *
 * perBoneFrames: { <logicalR15Name>: { times: [...], deltas: [Quaternion, ...] } }
 *   delta — это quaternion от rest-pose ИСХОДНОГО скелета.
 *
 * При проигрывании на бекона: target = rest_bacon × delta. Это даёт
 * корректный retargeting если локальные оси костей одинаково ориентированы
 * (Mixamo → R15-bacon: да, обе используют ту же конвенцию).
 *
 * Использование:
 *   const spec = parseEmoteGlbToSpec(animationGroup, importedSkeleton);
 *   r15Animator.playCustomEmote(spec);
 */

import { Quaternion, Vector3 } from '@babylonjs/core';

// R15-логическое имя → подстроки имени Mixamo-кости (приоритет сверху-вниз).
// Должно совпадать с R15Skeleton.R15_TO_MIXAMO, но мы здесь работаем с именами
// нод GLB напрямую (а не через R15Skeleton-резолвер).
const R15_KEYWORDS = {
    HumanoidRootPart: ['hips', 'humanoidrootpart', 'root'],
    LowerTorso:       ['spine1', 'spine', 'lowertorso'],
    UpperTorso:       ['spine2', 'uppertorso'],
    Head:             ['head'],
    Neck:             ['neck'],
    LeftUpperArm:     ['leftarm', 'leftupperarm', 'leftshoulder'],
    LeftLowerArm:     ['leftforearm', 'leftlowerarm'],
    LeftHand:         ['lefthand'],
    RightUpperArm:    ['rightarm', 'rightupperarm', 'rightshoulder'],
    RightLowerArm:    ['rightforearm', 'rightlowerarm'],
    RightHand:        ['righthand'],
    LeftUpperLeg:     ['leftupleg', 'leftupperleg'],
    LeftLowerLeg:     ['leftleg', 'leftlowerleg'],
    LeftFoot:         ['leftfoot'],
    RightUpperLeg:    ['rightupleg', 'rightupperleg'],
    RightLowerLeg:    ['rightleg', 'rightlowerleg'],
    RightFoot:        ['rightfoot'],
};

function normName(raw) {
    return String(raw || '')
        .toLowerCase()
        .replace(/mixamorig/g, '')
        .replace(/[:_\s.-]/g, '');
}

function resolveLogicalR15(boneName) {
    const norm = normName(boneName);
    for (const logical of Object.keys(R15_KEYWORDS)) {
        const kws = R15_KEYWORDS[logical];
        for (const kw of kws) {
            if (norm.includes(kw)) return logical;
        }
    }
    return null;
}

/**
 * Конвертировать AnimationGroup в delta-spec.
 *
 * @param {AnimationGroup} group — из импортированного GLB
 * @returns {object} spec = { length, loop, perBoneFrames }
 */
export function parseEmoteGlbToSpec(group, loop = false) {
    if (!group) return null;
    const length = group.to / 60;   // glTF samplers времена в секундах,
    // но Babylon хранит их в frames (60 FPS) — group.from/to это frames.
    // ИСПРАВЛЕНО: используем длительность из targetedAnimations[0].animation
    // если есть, иначе через group.to / 60.

    // Берём реальную длительность из первой анимации (там times в секундах).
    let realLength = 0;
    if (group.targetedAnimations && group.targetedAnimations.length > 0) {
        for (const ta of group.targetedAnimations) {
            const anim = ta.animation;
            const keys = anim?.getKeys?.() || [];
            if (keys.length > 0) {
                const lastFrame = keys[keys.length - 1].frame;
                const fps = anim.framePerSecond || 60;
                realLength = Math.max(realLength, lastFrame / fps);
            }
        }
    }
    if (realLength === 0) realLength = (group.to - group.from) / 60;

    // Для каждой target-ноды: собираем keyframes её rotation_local + rest
    const perBone = {};      // logical → { restRotQ, times, rots }

    for (const ta of group.targetedAnimations || []) {
        const target = ta.target;
        if (!target) continue;
        const tgtName = target.name || target.id || '';
        const logical = resolveLogicalR15(tgtName);
        if (!logical) continue;

        const anim = ta.animation;
        if (!anim) continue;
        const propName = anim.targetProperty;
        // Нас интересуют только rotation/rotationQuaternion-каналы.
        // Position-каналы (translation корня Hips) пропускаем — у бекона
        // позиция корня управляется движком (физика), нельзя её рулить.
        const isRot = propName === 'rotationQuaternion' || propName === 'rotation';
        if (!isRot) continue;

        const keys = anim.getKeys() || [];
        if (keys.length === 0) continue;
        const fps = anim.framePerSecond || 60;

        if (!perBone[logical]) {
            // rest = текущее значение target до анимации (= bind pose)
            const restQ = target.rotationQuaternion
                ? target.rotationQuaternion.clone()
                : (target.rotation
                    ? Quaternion.FromEulerAngles(
                        target.rotation.x, target.rotation.y, target.rotation.z)
                    : Quaternion.Identity());
            perBone[logical] = {
                restQ,
                times: [],
                rots: [],
            };
        }
        const slot = perBone[logical];

        // Складываем keyframes. Если есть несколько каналов на одну кость
        // (например, rotation_x + rotation_y отдельно — для anim_type=Euler)
        // — приходится их сэмплировать на общей сетке. Для простоты:
        // если propName === 'rotation' (Vector3), конвертируем каждый кадр.
        for (const k of keys) {
            const t = k.frame / fps;
            let q;
            if (propName === 'rotationQuaternion') {
                q = (k.value && k.value.clone) ? k.value.clone() : null;
            } else {
                // rotation (Vector3 euler XYZ)
                if (k.value && typeof k.value.x === 'number') {
                    q = Quaternion.FromEulerAngles(k.value.x, k.value.y, k.value.z);
                }
            }
            if (!q) continue;
            // Если в этом времени уже есть запись — мерджим (умножаем).
            // На практике одна кость → один канал rotation, мердж не нужен.
            const existingIdx = slot.times.indexOf(t);
            if (existingIdx >= 0) {
                slot.rots[existingIdx] = slot.rots[existingIdx].multiply(q);
            } else {
                slot.times.push(t);
                slot.rots.push(q);
            }
        }
    }

    // Сортируем keyframes по времени внутри каждой кости
    for (const logical of Object.keys(perBone)) {
        const slot = perBone[logical];
        const pairs = slot.times.map((t, i) => [t, slot.rots[i]]);
        pairs.sort((a, b) => a[0] - b[0]);
        slot.times = pairs.map(p => p[0]);
        slot.rots = pairs.map(p => p[1]);

        // Конвертируем абсолютные rot в delta: delta = restQ⁻¹ × rot.
        const restInv = Quaternion.Inverse(slot.restQ);
        slot.deltas = slot.rots.map(q => restInv.multiply(q));
        delete slot.rots;
    }

    return {
        length: realLength || 1.0,
        loop: !!loop,
        perBoneFrames: perBone,
    };
}
