import { serializable } from "../engine/engine_serialization_decorator.js";

/**
 * Keyframe is a representation of a keyframe in an AnimationCurve.
 */
export class Keyframe {
    @serializable()
    time: number = 0;
    @serializable()
    value: number = 0;

    @serializable()
    inTangent: number = Infinity;
    @serializable()
    inWeight?: number;
    @serializable()
    outTangent: number = Infinity;
    @serializable()
    outWeight?: number;
    @serializable()
    weightedMode?: number;

    constructor(time: number = 0, value: number = 0) {
        this.time = time;
        this.value = value;
    }
}

/**
 * AnimationCurve is a representation of a curve that can be used to animate values over time. 
 * 
 * @category Animation
 * @group Utilities 
 */
export class AnimationCurve {

    /**
     * Creates an animation curve that goes from the `from` value to the `to` value over the given `duration`.
     */
    static linearFromTo(from: number, to: number, duration: number): AnimationCurve {
        const curve = new AnimationCurve();
        const keyframe1 = new Keyframe();
        keyframe1.time = 0;
        keyframe1.value = from;
        const keyframe2 = new Keyframe();
        keyframe2.time = duration;
        keyframe2.value = to;
        curve.keys.push(keyframe1, keyframe2);
        return curve;
    }

    /** Creates an animation curve with just one keyframe */
    static constant(value: number): AnimationCurve {
        const curve = new AnimationCurve();
        const keyframe = new Keyframe();
        keyframe.time = 0;
        keyframe.value = value;
        curve.keys.push(keyframe);
        return curve;
    }

    /** 
     * The keyframes that define the curve.
    */
    @serializable(Keyframe)
    keys: Array<Keyframe> = [];

    /** 
     * Clones this AnimationCurve and returns a new instance with the same keyframes (the keyframes are also cloned).
    */
    clone() {
        const curve = new AnimationCurve();
        curve.keys = this.keys?.map(k => {
            const key = new Keyframe();
            key.time = k.time;
            key.value = k.value;
            key.inTangent = k.inTangent;
            key.inWeight = k.inWeight;
            key.outTangent = k.outTangent;
            key.outWeight = k.outWeight;
            key.weightedMode = k.weightedMode;
            return key;
        }) || [];
        return curve;
    }

    /** The duration of the curve, which is the time of the last keyframe. */
    get duration(): number {
        if (!this.keys || this.keys.length == 0) return 0;
        return this.keys[this.keys.length - 1].time;
    }

    /** Evaluates the curve at the given time and returns the value of the curve at that time.
     * @param time The time at which to evaluate the curve.
     * @returns The value of the curve at the given time.
     */
    evaluate(time: number): number {
        if (!this.keys || this.keys.length == 0) return 0;
        if (this.keys.length === 1) {
            return this.keys[0].value;
        }
        // if the first keyframe time is already greater than the time we want to evaluate
        // then we dont need to iterate
        if (this.keys[0].time >= time) {
            return this.keys[0].value;
        }
        for (let i = 0; i < this.keys.length; i++) {
            const kf = this.keys[i];
            if (kf.time <= time) {
                const hasNextKeyframe = i + 1 < this.keys.length;
                if (hasNextKeyframe) {
                    const nextKf = this.keys[i + 1];
                    // if the next
                    if (nextKf.time < time) continue;
                    // tangents are set to Infinity if interpolation is set to constant - in that case we should always return the floored value
                    if (!isFinite(kf.outTangent) || !isFinite(nextKf.inTangent)) return kf.value;
                    return AnimationCurve.interpolateValue(time, kf, nextKf);
                }
                else {
                    return kf.value;
                }
            }
        }
        return this.keys[this.keys.length - 1].value;
    }


    static interpolateValue(time: number, keyframe1: Keyframe, keyframe2: Keyframe): number {
        const startTime1 = keyframe1.time;
        const startValue1 = keyframe1.value;
        const outTangent1 = keyframe1.outTangent;

        const startTime2 = keyframe2.time;
        const startValue2 = keyframe2.value;
        const inTangent2 = keyframe2.inTangent;

        // could be precomputed and stored in the keyframes maybe
        const timeDifference = startTime2 - startTime1;
        const timeDifferenceSquared = timeDifference * timeDifference;
        const timeDifferenceCubed = timeDifferenceSquared * timeDifference;

        const a = ((outTangent1 + inTangent2) * timeDifference - 2 * (startValue2 - startValue1)) / timeDifferenceCubed;
        const b = (3 * (startValue2 - startValue1) - (inTangent2 + 2 * outTangent1) * timeDifference) / timeDifferenceSquared;
        const c = outTangent1;
        const d = startValue1;

        const timeDelta = time - startTime1;
        const timeDeltaSquared = timeDelta * timeDelta;
        const timeDeltaCubed = timeDeltaSquared * timeDelta;

        return a * timeDeltaCubed + b * timeDeltaSquared + c * timeDelta + d;
    }

}