#import "modules/std.sx";
#import "modules/math";

// --- Lerpable protocol (inline — static dispatch, no vtable) ---

Lerpable :: protocol #inline {
    lerp :: (b: Self, t: f32) -> Self;
}

// --- Easing Functions ---

ease_linear :: (t: f32) -> f32 { t }
ease_in_quad :: (t: f32) -> f32 { t * t }
ease_out_quad :: (t: f32) -> f32 { t * (2.0 - t) }
ease_in_out_quad :: (t: f32) -> f32 {
    if t < 0.5 then 2.0 * t * t
    else -1.0 + (4.0 - 2.0 * t) * t
}
ease_out_cubic :: (t: f32) -> f32 { u := t - 1.0; u * u * u + 1.0 }

// --- AnimatedFloat — duration-based ---

AnimatedFloat :: struct {
    current: f32;
    from: f32;
    to: f32;
    elapsed: f32;
    duration: f32;
    easing: ?Closure(f32) -> f32;
    active: bool;

    make :: (value: f32) -> AnimatedFloat {
        AnimatedFloat.{
            current = value,
            from = value,
            to = value,
            elapsed = 0.0,
            duration = 0.0,
            easing = null,
            active = false
        }
    }

    animate_to :: (self: *AnimatedFloat, target: f32, dur: f32, ease: Closure(f32) -> f32) {
        self.from = self.current;
        self.to = target;
        self.elapsed = 0.0;
        self.duration = dur;
        self.easing = ease;
        self.active = true;
    }

    tick :: (self: *AnimatedFloat, dt: f32) {
        if !self.active { return; }
        self.elapsed += dt;
        t := clamp(self.elapsed / self.duration, 0.0, 1.0);
        eased := if ease := self.easing { ease(t) } else { t };
        self.current = self.from + (self.to - self.from) * eased;
        if t >= 1.0 {
            self.current = self.to;
            self.active = false;
        }
    }
}

// --- SpringFloat — physics-based ---

SpringFloat :: struct {
    current: f32;
    velocity: f32;
    target: f32;
    stiffness: f32;
    damping: f32;
    mass: f32;
    threshold: f32;

    make :: (value: f32) -> SpringFloat {
        SpringFloat.{
            current = value,
            velocity = 0.0,
            target = value,
            stiffness = 200.0,
            damping = 20.0,
            mass = 1.0,
            threshold = 0.01
        }
    }

    snappy :: (value: f32) -> SpringFloat {
        SpringFloat.{
            current = value,
            velocity = 0.0,
            target = value,
            stiffness = 300.0,
            damping = 25.0,
            mass = 1.0,
            threshold = 0.01
        }
    }

    tick :: (self: *SpringFloat, dt: f32) {
        if self.is_settled() { return; }
        force := 0.0 - self.stiffness * (self.current - self.target);
        damping_force := 0.0 - self.damping * self.velocity;
        accel := (force + damping_force) / self.mass;
        self.velocity += accel * dt;
        self.current += self.velocity * dt;
    }

    is_settled :: (self: *SpringFloat) -> bool {
        abs(self.current - self.target) < self.threshold
        and abs(self.velocity) < self.threshold
    }
}

// --- Animated(T) — generic duration-based animation for any Lerpable type ---

Animated :: struct ($T: Lerpable) {
    current: T;
    from: T;
    to: T;
    elapsed: f32;
    duration: f32;
    active: bool;

    make :: (value: T) -> Animated(T) {
        Animated(T).{
            current = value,
            from = value,
            to = value,
            elapsed = 0.0,
            duration = 0.0,
            active = false
        }
    }

    // Jump immediately to value (no animation). Used to avoid animating from zero on first layout.
    set_immediate :: (self: *Animated(T), value: T) {
        self.current = value;
        self.from = value;
        self.to = value;
        self.elapsed = 0.0;
        self.active = false;
    }

    // Start animating towards target.
    animate_to :: (self: *Animated(T), target: T, dur: f32) {
        self.from = self.current;
        self.to = target;
        self.elapsed = 0.0;
        self.duration = dur;
        self.active = true;
    }

    tick :: (self: *Animated(T), dt: f32) {
        if !self.active { return; }
        self.elapsed += dt;
        t := clamp(self.elapsed / self.duration, 0.0, 1.0);
        self.current = self.from.lerp(self.to, t);
        if t >= 1.0 {
            self.current = self.to;
            self.active = false;
        }
    }

    is_animating :: (self: *Animated(T)) -> bool { self.active }
}
