/*
 * steering_pid.c — closed-loop yaw-rate PID for differential drive (Issue #616).
 *
 * Converts Jetson Twist.angular.z (encoded as steer * STEER_OMEGA_SCALE deg/s)
 * into a differential wheel speed offset using IMU gyro Z as feedback.
 *
 * Anti-windup:   integral clamped to ±STEER_INTEGRAL_MAX before Ki multiply.
 * Rate limiter:  output changes at most STEER_RAMP_RATE_PER_MS per ms so that
 *                a step in steering demand cannot disturb the balance PID.
 */

#include "steering_pid.h"
#include "jlink.h"

/* ---- steering_pid_init() ---- */
void steering_pid_init(steering_pid_t *s)
{
    s->target_omega_dps = 0.0f;
    s->actual_omega_dps = 0.0f;
    s->integral         = 0.0f;
    s->prev_error       = 0.0f;
    s->output           = 0;
    s->enabled          = true;
    /* Initialize so first send_tlm call fires immediately */
    s->last_tlm_ms = (uint32_t)(-(uint32_t)(1000u / (STEER_TLM_HZ > 0u ? STEER_TLM_HZ : 1u)));
}

/* ---- steering_pid_reset() ---- */
void steering_pid_reset(steering_pid_t *s)
{
    s->target_omega_dps = 0.0f;
    s->integral         = 0.0f;
    s->prev_error       = 0.0f;
    s->output           = 0;
}

/* ---- steering_pid_set_target() ---- */
void steering_pid_set_target(steering_pid_t *s, float omega_dps)
{
    if (!s->enabled) return;
    s->target_omega_dps = omega_dps;
}

/* ---- steering_pid_update() ---- */
int16_t steering_pid_update(steering_pid_t *s, float actual_omega_dps, float dt)
{
    if (!s->enabled || dt <= 0.0f) {
        s->output = 0;
        return 0;
    }

    s->actual_omega_dps = actual_omega_dps;

    /* PID error */
    float error = s->target_omega_dps - actual_omega_dps;

    /* Proportional */
    float p_term = STEER_KP * error;

    /* Integral with anti-windup clamp */
    s->integral += error * dt;
    if (s->integral >  STEER_INTEGRAL_MAX) s->integral =  STEER_INTEGRAL_MAX;
    if (s->integral < -STEER_INTEGRAL_MAX) s->integral = -STEER_INTEGRAL_MAX;
    float i_term = STEER_KI * s->integral;

    /* Derivative on error (avoids setpoint kick for smooth yaw changes) */
    float d_term = 0.0f;
    if (dt > 0.0f) {
        d_term = STEER_KD * (error - s->prev_error) / dt;
    }
    s->prev_error = error;

    /* Sum and clamp raw output */
    float raw = p_term + i_term + d_term;
    if (raw >  (float)STEER_OUTPUT_MAX) raw =  (float)STEER_OUTPUT_MAX;
    if (raw < -(float)STEER_OUTPUT_MAX) raw = -(float)STEER_OUTPUT_MAX;

    /* Rate limiter: bound change per step by STEER_RAMP_RATE_PER_MS * dt */
    float max_step = (float)STEER_RAMP_RATE_PER_MS * (dt * 1000.0f);
    float delta = raw - (float)s->output;
    if (delta >  max_step) delta =  max_step;
    if (delta < -max_step) delta = -max_step;

    float limited = (float)s->output + delta;
    /* Final clamp after rate limit */
    if (limited >  (float)STEER_OUTPUT_MAX) limited =  (float)STEER_OUTPUT_MAX;
    if (limited < -(float)STEER_OUTPUT_MAX) limited = -(float)STEER_OUTPUT_MAX;

    s->output = (int16_t)limited;
    return s->output;
}

/* ---- steering_pid_get_output() ---- */
int16_t steering_pid_get_output(const steering_pid_t *s)
{
    return s->output;
}

/* ---- steering_pid_set_enabled() ---- */
void steering_pid_set_enabled(steering_pid_t *s, bool en)
{
    if (!en && s->enabled) {
        /* Disabling: zero out state */
        s->target_omega_dps = 0.0f;
        s->integral         = 0.0f;
        s->prev_error       = 0.0f;
        s->output           = 0;
    }
    s->enabled = en;
}

/* ---- steering_pid_send_tlm() ---- */
void steering_pid_send_tlm(const steering_pid_t *s, uint32_t now_ms)
{
    if (STEER_TLM_HZ == 0u) return;

    uint32_t interval_ms = 1000u / STEER_TLM_HZ;
    if ((now_ms - s->last_tlm_ms) < interval_ms) return;
    /* Cast away const for timestamp update — only mutable field */
    ((steering_pid_t *)s)->last_tlm_ms = now_ms;

    jlink_tlm_steering_t tlm;

    /* Scale to ×10 for 0.1 deg/s resolution, clamped to int16 range */
    float t = s->target_omega_dps * 10.0f;
    if (t >  32767.0f) t =  32767.0f;
    if (t < -32768.0f) t = -32768.0f;
    tlm.target_x10 = (int16_t)t;

    float a = s->actual_omega_dps * 10.0f;
    if (a >  32767.0f) a =  32767.0f;
    if (a < -32768.0f) a = -32768.0f;
    tlm.actual_x10 = (int16_t)a;

    tlm.output  = s->output;
    tlm.enabled = s->enabled ? 1u : 0u;
    tlm._pad    = 0u;

    jlink_send_steering_tlm(&tlm);
}
