import { AnimationAction } from "./AnimationAction.js";
import { cloneAnimationValue, normalizeQuat, slerpQuat } from "./Keyframe.js";
import { applyRootMotion, extractRootMotion } from "./RootMotion.js";
export class AnimationMixer {
    options;
    target;
    timeScale = 1;
    actions = [];
    layers = [];
    values = new Map();
    applyErrors = [];
    eventListeners = new Set();
    disposed = false;
    constructor(target, options = {}) {
        this.options = options;
        this.target = target;
    }
    play(clip) {
        this.assertAlive();
        const action = new AnimationAction(clip).play();
        this.actions.push(action);
        return action;
    }
    addAction(action) {
        this.assertAlive();
        if (!this.actions.includes(action)) {
            this.actions.push(action);
        }
    }
    addLayer(layer) {
        this.assertAlive();
        if (!this.layers.includes(layer)) {
            this.layers.push(layer);
        }
    }
    stopAll() {
        this.assertAlive();
        for (const action of this.actions) {
            action.stop();
        }
    }
    crossFade(from, to, duration) {
        this.assertAlive();
        from.fadeTo(0, duration);
        to.play().fadeTo(1, duration);
        this.addAction(to);
    }
    update(delta) {
        this.assertAlive();
        if (!Number.isFinite(delta) || delta < 0) {
            throw new Error("AnimationMixer delta must be finite and non-negative.");
        }
        const events = [];
        const accumulators = new Map();
        for (const action of this.actions) {
            const previousTime = action.time;
            const actionEvents = action.update(delta * this.timeScale);
            if (actionEvents.length > 0) {
                events.push(...actionEvents);
            }
            if (!action.playing && action.weight <= 0) {
                continue;
            }
            this.applyActionRootMotion(action, previousTime);
            for (const track of action.clip.tracks) {
                const sample = track.sample(action.time);
                for (const layer of this.layersForAction(action)) {
                    if (layer.capturesTarget(track.target)) {
                        blendInto(accumulators, track.target, track.valueType, sample, action.weight * layer.weight, layer.additive);
                    }
                }
            }
        }
        this.values.clear();
        this.applyErrors = [];
        for (const [target, accumulator] of accumulators) {
            const value = finalizeTargetBlend(accumulator);
            this.values.set(target, value);
            try {
                this.target?.setAnimationValue?.(target, value);
            }
            catch (error) {
                this.applyErrors.push({
                    target,
                    message: error instanceof Error ? error.message : String(error)
                });
            }
        }
        for (const event of events) {
            for (const listener of Array.from(this.eventListeners)) {
                listener(event);
            }
        }
        return events;
    }
    getValue(target) {
        const value = this.values.get(target);
        return value === undefined ? undefined : cloneAnimationValue(value);
    }
    onEvent(listener) {
        this.assertAlive();
        this.eventListeners.add(listener);
        return () => this.eventListeners.delete(listener);
    }
    snapshot() {
        const values = {};
        for (const [target, value] of this.values) {
            values[target] = cloneAnimationValue(value);
        }
        return {
            timeScale: this.timeScale,
            actionCount: this.actions.length,
            actions: this.actions.map((action) => action.snapshot()),
            layers: this.layers.map((layer) => layer.snapshot()),
            values,
            applyErrors: this.applyErrors.map((error) => ({ ...error }))
        };
    }
    dispose() {
        this.actions.length = 0;
        this.layers.length = 0;
        this.values.clear();
        this.applyErrors = [];
        this.eventListeners.clear();
        this.disposed = true;
    }
    assertAlive() {
        if (this.disposed) {
            throw new Error("AnimationMixer has been disposed.");
        }
    }
    applyActionRootMotion(action, previousTime) {
        if (!this.options.applyRootMotion || action.weight <= 0)
            return;
        const rootMotionTarget = this.options.rootMotionTrack ?? "root.position";
        if (!action.clip.tracks.some((track) => track.target === rootMotionTarget))
            return;
        const sample = extractRootMotion(action.clip, {
            fromTime: previousTime,
            toTime: action.time,
            loop: action.loopMode === "repeat",
            target: rootMotionTarget
        });
        const scaledSample = action.weight === 1
            ? sample
            : {
                ...sample,
                delta: [
                    sample.delta[0] * action.weight,
                    sample.delta[1] * action.weight,
                    sample.delta[2] * action.weight
                ]
            };
        this.target?.applyRootMotion?.(scaledSample);
        if (this.target?.position) {
            applyRootMotion(this.target, scaledSample, this.options.rootMotionScale ?? 1);
        }
    }
    layersForAction(action) {
        if (this.layers.length === 0) {
            return UNMASKED_LAYERS;
        }
        const owningLayers = this.layers.filter((layer) => layer.actions.includes(action));
        return owningLayers.length > 0 ? owningLayers : UNMASKED_LAYERS;
    }
}
const UNMASKED_LAYER = {
    name: "default",
    weight: 1,
    additive: false,
    mask: [],
    actions: [],
    capturesTarget: () => true,
    snapshot: () => ({ name: "default", weight: 1, additive: false, mask: [], actions: [] })
};
const UNMASKED_LAYERS = [UNMASKED_LAYER];
function blendInto(accumulators, target, type, value, weight, additive) {
    if (weight <= 0) {
        return;
    }
    const current = accumulators.get(target);
    if (!current) {
        const accumulator = { type };
        if (additive) {
            accumulator.additive = additiveContribution(type, value, weight);
        }
        else {
            accumulator.base = { value: cloneAnimationValue(value), weight, type };
        }
        accumulators.set(target, accumulator);
        return;
    }
    if (current.type !== type) {
        throw new Error(`Cannot blend ${type} animation track into existing ${current.type} target ${target}.`);
    }
    if (additive) {
        current.additive = current.additive === undefined
            ? additiveContribution(type, value, weight)
            : combineAdditive(type, current.additive, value, weight);
        return;
    }
    if (!current.base) {
        current.base = { value: cloneAnimationValue(value), weight, type };
        return;
    }
    blendBase(current.base, type, value, weight);
}
function blendBase(current, type, value, weight) {
    const total = current.weight + weight;
    const t = weight / total;
    if (type === "scalar") {
        current.value = current.value + (value - current.value) * t;
    }
    else if (type === "vector3") {
        const a = current.value;
        const b = value;
        current.value = [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
    }
    else if (type === "quaternion") {
        current.value = slerpQuat(current.value, value, t);
    }
    else if (type === "number-array") {
        const a = current.value;
        const b = value;
        if (a.length !== b.length) {
            throw new Error("Cannot blend number-array animation values with different lengths.");
        }
        current.value = a.map((component, index) => component + (b[index] - component) * t);
    }
    else {
        current.value = cloneAnimationValue(value);
    }
    current.weight = total;
}
function finalizeTargetBlend(accumulator) {
    const base = accumulator.base
        ? finalizeBaseBlend(accumulator.base)
        : accumulator.additive !== undefined
            ? additiveNeutral(accumulator.type, accumulator.additive)
            : undefined;
    if (base === undefined) {
        throw new Error("Animation target accumulator has no sampled value.");
    }
    if (accumulator.additive === undefined) {
        return base;
    }
    return applyAdditive(accumulator.type, base, accumulator.additive);
}
function finalizeBaseBlend(accumulator) {
    if (accumulator.type === "quaternion") {
        return normalizeQuat(accumulator.value);
    }
    return cloneAnimationValue(accumulator.value);
}
function additiveContribution(type, value, weight) {
    if (type === "scalar") {
        return value * weight;
    }
    if (type === "vector3") {
        const vector = value;
        return [vector[0] * weight, vector[1] * weight, vector[2] * weight];
    }
    if (type === "number-array") {
        return value.map((component) => component * weight);
    }
    if (type === "quaternion") {
        return slerpQuat([0, 0, 0, 1], normalizeQuat(value), weight);
    }
    throw new Error("Additive animation layers require numeric, vector, quaternion, or number-array tracks.");
}
function combineAdditive(type, current, value, weight) {
    return applyAdditive(type, current, additiveContribution(type, value, weight));
}
function applyAdditive(type, base, delta) {
    if (type === "scalar") {
        return base + delta;
    }
    if (type === "vector3") {
        const a = base;
        const b = delta;
        return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
    }
    if (type === "number-array") {
        const a = base;
        const b = delta;
        if (a.length !== b.length) {
            throw new Error("Cannot add number-array animation values with different lengths.");
        }
        return a.map((component, index) => component + b[index]);
    }
    if (type === "quaternion") {
        return normalizeQuat(multiplyQuat(base, delta));
    }
    throw new Error("Additive animation layers require numeric, vector, quaternion, or number-array tracks.");
}
function additiveNeutral(type, value) {
    if (type === "scalar") {
        return 0;
    }
    if (type === "vector3") {
        return [0, 0, 0];
    }
    if (type === "number-array") {
        return value.map(() => 0);
    }
    if (type === "quaternion") {
        return [0, 0, 0, 1];
    }
    throw new Error("Additive animation layers require numeric, vector, quaternion, or number-array tracks.");
}
function multiplyQuat(a, b) {
    return [
        a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1],
        a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0],
        a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3],
        a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2]
    ];
}
//# sourceMappingURL=AnimationMixer.js.map