import { assert } from "../../../core/assert.js";
import AABB2 from "../../../core/geom/2d/aabb/AABB2.js";
import { spline3_hermite_derivative } from "../../../core/math/spline/spline3_hermite_derivative.js";
import { animation_curve_compute_aabb } from "./animation_curve_compute_aabb.js";
import { evaluate_two_key_curve } from "./evaluate_two_key_curve.js";

const bounds = new AABB2();

// Scaling factor to project slope error into value error.
// 0.125 (1/8th) is a standard approximation for cubic bezier error bounds.
const TANGENT_ERROR_WEIGHT = 0.125;

/**
 * Computes importance by checking Position AND Slope integrity.
 * Uses exact derivative calculation instead of neighbor sampling
 *
 * @param {Keyframe} key_middle
 * @param {Keyframe} key_previous
 * @param {Keyframe} key_next
 * @return {number} value delta if the middle frame is removed, the higher this value - the more important the middle keyframe is
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
function compute_keyframe_value_effect(
    key_middle,
    key_previous,
    key_next
) {
    const duration = key_next.time - key_previous.time;

    // Safety check for zero-duration (duplicate keys handled by outer loop, but safe to guard)
    if (duration < 1e-9){
        return 0;
    }

    // 1. Calculate Normalized Time t [0, 1]
    const t_relative = key_middle.time - key_previous.time;
    const t_normalized = t_relative / duration;

    // 2. Calculate Position Error (The "Hit Test")
    // Does the simplified curve actually hit the middle key's value?
    const v_actual = evaluate_two_key_curve(key_middle.time, key_previous, key_next);
    const v_error = Math.abs(v_actual - key_middle.value);

    // 3. Calculate Derivative Error (The "Shape Test")
    // Does the simplified curve flow in the same direction as the original key?

    // NOTE: Hermite basis requires tangents scaled by duration (m0, m1)
    // We assume keyframes have .outTangent and .inTangent or similar.
    // If you use standard Hermite data, it looks like this:
    const m0 = key_previous.outTangent * duration;
    const m1 = key_next.inTangent * duration;

    // This returns the derivative in "Value per Normalized Time"
    const slope_normalized = spline3_hermite_derivative(
        t_normalized,
        key_previous.value,
        key_next.value,
        m0,
        m1
    );

    // Convert to "Value per Second" to match the keyframe's stored tangent
    const slope_actual = slope_normalized / duration;

    // We compare against the explicit tangent stored on the middle key (or calculated implicit tangent)
    // Assuming the key has a unified tangent or we use the inTangent
    const slope_expected = key_middle.inTangent; // or .outTangent, or average if smooth

    const slope_delta = Math.abs(slope_actual - slope_expected);

    // 4. Project Slope Delta into Value Units
    // If the slope is off by 1 unit/sec, how much drift does that cause over the duration?
    const slope_error_projected = slope_delta * duration * TANGENT_ERROR_WEIGHT;

    return Math.max(v_error, slope_error_projected);
}

/**
 * Will remove keyframes that do not affect the shape of the curve significantly.
 * The error tolerance determines the significance degree.
 * Intended to reduce the complexity of a curve to improve performance and lower memory usage
 * @param {AnimationCurve} curve The curve to optimize. The curve is modified in place.
 * @param {number} [error_tolerance] how much of a loss to accept, this is relative to normalized value bounds of the curve
 * @returns {number} number of removed keys, 0 if curve was unchanged
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export function animation_curve_optimize(
    curve,
    error_tolerance = 1e-3
) {
    assert.lessThan(error_tolerance, 1, 'error_tolerance must be less than 1');

    let key_count = curve.length;

    if (key_count <= 1) {
        // nothing to remove
        return 0;
    }

    animation_curve_compute_aabb(bounds, curve);

    const EPSILON = 1e-9;

    // Clamp against epsilon to handle the flat-line case or very low tolerances resulting in precision issues
    const absolute_error_tolerance = Math.max(
        bounds.height * error_tolerance,
        EPSILON,
    );

    const keyframes = curve.keys;

    const initial_key_count = key_count;

    for (let i = 1; i < key_count; i++) {
        const key_previous = keyframes[i - 1];
        const key_current = keyframes[i];

        let should_remove = false;

        if (key_current.equals(key_previous)) {
            // adds no semantic value
            should_remove = true;
        } else if (i < key_count - 1) {

            const key_next = keyframes[i + 1];

            const max_error = compute_keyframe_value_effect(key_current, key_previous, key_next);

            if (max_error <= absolute_error_tolerance) {
                // does not significantly affect the curve shape
                should_remove = true;
            }

        }


        if (should_remove) {

            curve.remove(key_current);

            // update iterator
            i--;
            key_count--;

        }
    }

    // number of removed keys
    return initial_key_count - key_count;
}