/**
 * General damped spring step (under/critically/over-damped), used for
 * landing recovery where a small ring/overshoot is desirable.
 *
 * Uses the half-life form of natural frequency and an explicit damping
 * ratio zeta. zeta=1 → critically damped (no ring), zeta<1 → under-damped
 * (rings), zeta>1 → over-damped (sluggish).
 *
 * Numerically stable for any zeta via the closed-form solutions of the
 * 2nd-order ODE. Reference: Allen Chou,
 * `allenchou.net/2015/04/game-math-precise-control-over-numeric-springing/`.
 *
 * @param {{value:number, velocity:number}} state mutated in place
 * @param {number} target
 * @param {number} halfLife seconds for the *envelope* to halve
 * @param {number} zeta damping ratio (e.g. 0.55 for a slight ring)
 * @param {number} dt
 */
export function dampedSpringStep(state, target, halfLife, zeta, dt) {
    if (halfLife <= 0) {
        state.value = target;
        state.velocity = 0;
        return;
    }

    // omega such that the envelope decays by 1/2 in halfLife
    // for the under-damped case the envelope is exp(-zeta * omega * t)
    // → omega = ln(2) / (zeta * halfLife). For zeta=1 this matches the
    // critically damped form.
    const omega = 0.6931471805599453 / (Math.max(zeta, 1e-4) * halfLife);

    const x = state.value - target;
    const v = state.velocity;

    if (zeta < 1.0) {
        // Under-damped: rings
        const omega_d = omega * Math.sqrt(1 - zeta * zeta);
        const za = zeta * omega;
        const exp_term = Math.exp(-za * dt);
        const cos_term = Math.cos(omega_d * dt);
        const sin_term = Math.sin(omega_d * dt);
        const A = x;
        const B = (v + za * x) / omega_d;

        state.value = target + exp_term * (A * cos_term + B * sin_term);
        state.velocity = exp_term * (
            (-za) * (A * cos_term + B * sin_term)
            + omega_d * (-A * sin_term + B * cos_term)
        );
    } else if (zeta > 1.0) {
        // Over-damped: two real roots
        const za = zeta * omega;
        const zb = omega * Math.sqrt(zeta * zeta - 1);
        const r1 = -za + zb;
        const r2 = -za - zb;
        const c2 = (v - x * r1) / (r2 - r1);
        const c1 = x - c2;
        const e1 = Math.exp(r1 * dt);
        const e2 = Math.exp(r2 * dt);

        state.value = target + c1 * e1 + c2 * e2;
        state.velocity = c1 * r1 * e1 + c2 * r2 * e2;
    } else {
        // Critically damped: shared root
        const exp_term = Math.exp(-omega * dt);
        const A = x;
        const B = v + omega * x;

        state.value = target + exp_term * (A + B * dt);
        state.velocity = exp_term * (B - omega * (A + B * dt));
    }
}
