/**
 * @file pwm.h (families/t06211/compact_src)
 * @brief Compact single-header API for the t06211 PWM controller.
 *
 * Mirrors the public shape of the default family's `compact_src/pwm.h`
 * but tailored to t06211's 5-stage pipeline (no standalone max_cl_error
 * lookup, single-submap setpoint, factor+offset target compute).
 *
 * Public API is just two functions:
 *   pwm_init()    — one-time setup
 *   pwm_service() — per-cycle update (pulls inputs via getters,
 *                          runs the 5-stage pipeline, writes rt outputs)
 *
 * External inputs arrive through a getter vtable; each callback returns
 * one signal in native PWM units.
 *
 * Every arithmetic choice mirrors the MCS-96 assembly; see
 * families/t06211/src/ for per-stage commentary.
 */
#ifndef PWM_H
#define PWM_H

#include <stdint.h>
#include <stdbool.h>
#include <string.h>

/* ── MCS-96 arithmetic primitives ───────────────────────────────────── */
#define MUL_S16(a, b)  ((int32_t)(int16_t)(a) * (int32_t)(int16_t)(b))
#define MULU_U16(a, b) ((uint32_t)(uint16_t)(a) * (uint32_t)(uint16_t)(b))
#define CLAMP(v, lo, hi) ((v) < (lo) ? (lo) : (v) > (hi) ? (hi) : (v))

static inline int16_t shra16(int16_t v, unsigned n) {
    if (!n) return v;
    uint16_t u = (uint16_t)v, sign = (u >> 15) & 1u;
    uint16_t m = sign ? (uint16_t)(((uint16_t)~0u) << (16u - n)) : 0u;
    return (int16_t)((u >> n) | m);
}
static inline int32_t shra32(int32_t v, unsigned n) {
    if (!n) return v;
    uint32_t u = (uint32_t)v, sign = (u >> 31) & 1u;
    uint32_t m = sign ? (~(uint32_t)0u << (32u - n)) : 0u;
    return (int32_t)((u >> n) | m);
}

/* ══════════════════════════════════════════════════════════════════════
 * 1D INTERPOLATION SLOT
 * Layout matches family-1's pwm_interp_slot_t and the family-1 ROM
 * convention [count*2, range, offset, seg_bytes] — t06211's FUN_6fb8
 * fingerprints identically, so the same layout applies.
 * ══════════════════════════════════════════════════════════════════════ */
typedef struct pwm_interp_slot {
    int16_t row_stride;   /* count * 2 (byte stride for Y-table rows) */
    int16_t x_interval;   /* x[k-1] - x[k] */
    int16_t x_offset;     /* input - x[k] */
    int16_t y_byte_off;   /* k * 2 */
} pwm_interp_slot_t;

/* ══════════════════════════════════════════════════════════════════════
 * EXTERNAL INPUTS
 * Populated each cycle by pwm_service via the getter vtable.
 * t06211-specific set: no temperature, no angle_dec_cmd in the setpoint
 * (the family-2 setpoint is single-submap RPM-indexed).
 * ══════════════════════════════════════════════════════════════════════ */
typedef struct pwm_inputs {
    int16_t  ckp_in;         /* 0x02f8 — sensed position */
    uint16_t rpm;            /* 0x0040 — engine RPM */
    int16_t  angle_dec_cmd;  /* 0x0042 — accepted for family-1 API parity; unused here */
    int16_t  inj_qty_demand; /* 0x0044 — CAN: inj quantity demand (PI large-neg gate) */
    int16_t  b_fb_kw;        /* CAN: plunger feedback baseline. Gets copied into
                                can_raw_b_fb_kw and decoded by s_setpoint_can_decode
                                (port of FUN_64c3) to produce target. */
    int16_t  cl_gate_input;      /* 0x0130 — CAN: open/closed-loop discriminant */
    uint16_t supply_voltage; /* 0x0142 — shape_eval submap input */
    int16_t  temperature;    /* 0x0146 — accepted for family-1 API parity; unused here */
} pwm_inputs_t;

/** Getter vtable. 8-callback layout matches family-1's pwm_input_getters_t
 *  so a single FBKW.c can drive either family's pwm.c without changes.
 *  t06211 does not consume angle_dec_cmd or temperature; those getters are
 *  called and stored into rt->inputs but the pipeline ignores them. */
typedef struct pwm_input_getters {
    int16_t  (*ckp_in)        (void *ctx);
    uint16_t (*rpm)           (void *ctx);
    int16_t  (*angle_dec_cmd) (void *ctx);     /* accepted; unused by t06211 */
    int16_t  (*inj_qty_demand)(void *ctx);
    int16_t  (*b_fb_kw)       (void *ctx);
    int16_t  (*cl_gate_input)     (void *ctx);
    uint16_t (*supply_voltage)(void *ctx);
    int16_t  (*temperature)   (void *ctx);     /* accepted; unused by t06211 */
    void     *ctx;
} pwm_input_getters_t;

typedef struct pwm_calibration pwm_calibration_t;
typedef struct pwm_submap_descr pwm_submap_descr_t;

/* ══════════════════════════════════════════════════════════════════════
 * RUNTIME STATE
 * RAM-address comments reference the t06211 MCS-96 map (see
 * families/t06211/docs/variable-glossary.md).
 * ══════════════════════════════════════════════════════════════════════ */
typedef struct pwm_runtime {
    /* External inputs — refreshed each cycle from getters */
    pwm_inputs_t inputs;

    /* Async flags.
     *   reset_flag       — set on reset edge; supervisor clears.
     *   system_flags_110 — bit 5 forces open-loop; bit 0 is fast-recovery latch.
     */
    uint8_t reset_flag;           /* 0x002e */
    uint8_t system_flags_110;     /* 0x0110 */

    /* ── Dual-target setpoint architecture ── */
    /* target (RW5E = RAM[0x005e]): PRIMARY CAN-driven setpoint
     * computed by FUN_64c3 (called from CAN parser FUN_6192). Drives
     * the PI controller in both open-loop (FUN_67c4:67e5) and
     * closed-loop (FUN_67c4:680a) paths, and supervisor error compute
     * (FUN_7beb:7c20). Formula: raw/2 + setpoint_offset + rw42_state,
     * clamped >= cal->target_5e_min_clamp. */
    int16_t target_5e;            /* RW5E — primary CAN-decoded setpoint */
    /* pi_high_clamp_ceiling (DAT_0336): SECONDARY RPM-derived ceiling
     * computed by FUN_7168(setpoint_descr) + FUN_77b3:77c7. Used ONLY as
     * upper-clamp in FUN_672b PI compensation (0x67a9/0x67b0) and
     * FUN_7beb supervisor (0x7c30/0x7c37). Does not drive control
     * directly. (Was misnamed `target_336` across earlier ports — the
     * address-suffixed style hid that this is a PI clamp, not a target.) */
    int16_t pi_high_clamp_ceiling;
    /* CAN-staging buffers — written externally before pwm_service so
     * s_setpoint_can_decode can transform them into target. */
    int16_t can_raw_b_fb_kw;      /* 0x012c — raw b_fb_kw from CAN */
    int16_t can_aux_12e;          /* 0x012e — secondary CAN field (drives rw42_state) */
    int16_t can_half_12a;         /* 0x012a — cached raw>>1 (debug visibility) */
    /* setpoint_offset (RAM[0x0150]) is copied at runtime_init from
     * cal.setpoint_offset (mirrors family-1's pattern at
     * compact_src/pwm.c:66). The REAL ROM derives it at boot via
     * FUN_6b7b @ 0x6bb5: RAM[0x150] = cal+0x4c - RW1E. Because RW1E's
     * source at the call site (0x6c46) is in a Ghidra-unrecognised code
     * region, we expose the field directly through cal so the user can
     * set it to the match-fitted value without needing to simulate the
     * full FUN_6b7b boot path. */
    int16_t setpoint_offset;      /* runtime copy of cal->setpoint_offset (DAT_0150 in ROM) */
    int16_t rw42_state;           /* RW42 register — = can_aux_12e when valid */
    /* DAT_033a — supervisor reset stash: snapshots pi_integ_hi at the
     * reset edge (FUN_7beb:7c05-7c0a). Dead store (no readers anywhere
     * in the ROM); kept here for parity with the disasm. NOT a baseline
     * of b_fb_kw — that name was inherited from the family-1 idiom and
     * is incorrect for this variant. */
    int16_t pi_integ_hi_snapshot;
    /* PI P-term — produced by FUN_672b @ 0x6775 as `(p_gain_normal * err) >> 8`
     * and consumed by the supervisor at FUN_7beb:0x7c26 as the additive
     * shaping term on the error. (DAT_02b8 in ROM; was named
     * `compensation_angle` in earlier ports.) */
    int16_t pi_p_term;
    int16_t angle_error_raw;      /* DAT_033e — supervisor output (raw + ceiling-clamped) */
    uint16_t cl_enable_counter;   /* DAT_033c */

    /* ── CL correction ── */
    int16_t cl_correction_raw;        /* DAT_0176 */
    int16_t angle_offset;             /* DAT_017c */
    int16_t supervisor_state;         /* RW17E — CL accumulator */
    int16_t pos_error_normalizer;     /* runtime copy of cal->init_pos_error_normalizer (DAT_0332 in ROM) */
    int16_t neg_error_normalizer;     /* runtime copy of cal->init_neg_error_normalizer (DAT_0334 in ROM) */

    /* ── Publish + PI ── */
    int16_t estimated_angle;      /* DAT_02cc */
    int16_t angle_error_pi;       /* DAT_02be — target_5e − estimated_angle */
    int16_t active_request;       /* RW46 — published PI output / PWM feed-forward */
    uint8_t pi_open_loop_flag;    /* DAT_02c4 */
    uint8_t pi_shape_flag;        /* DAT_02c5 */
    uint8_t pi_flag_c6;           /* DAT_02c6 — previous-cycle pi_shape_flag latch */
    uint8_t pi_flag_338;          /* DAT_0338 — Block-4 cal-pinned latch */

    /* P-shape segment bounds (boot-init from cal+0x10A/0x10C). PI Block-4
     * error-window bounds (independent int16s, NOT a 32-bit integrator).
     * Trim bytes at RAM[0x0414]/[0x0416] have no writers in the ROM, so
     * bounds resolve to the cal bases verbatim. See open-questions §2. */
    int16_t p_shape_bound_pos;    /* DAT_0450 */
    int16_t p_shape_bound_neg;    /* DAT_0452 */

    int16_t pi_state_118;         /* DAT_0118 — recovery counter */
    int16_t pi_state_c2;          /* DAT_02c2 — cooldown counter */

    /* PI integrator pair {hi:lo} at {DAT_02b4 : DAT_02b6}. Disasm
     * `disasm_nav rw 0x02b4` shows only internal writers (FUN_672b,
     * FUN_76aa, FUN_7c85), confirming this is a PI integrator state,
     * not an external/CAN input. (Was misnamed `b_fb_kw` in the original
     * glossary; renamed via the address-suffixed `pi_b4_state`/`pi_b6_state`
     * intermediate; final semantic name aligns with the cross-family pair.) */
    int16_t pi_integ_hi;          /* DAT_02b4 — integrator high word */
    int16_t pi_integ_lo;          /* DAT_02b6 — integrator low word */

    /* PI gains — boot-set by FUN_76aa from cal scalars (with optional
     * trim bytes at RAM[0x0410]/[0x0412] which have no writers anywhere
     * in the ROM, so the cal bases pass through unchanged). */
    int16_t p_gain_normal;        /* DAT_0454 — boot from cal+0x118; multiplier in pi_p_term = (gain*err)>>8 */
    int16_t integ_step_normal;    /* DAT_0456 — boot from cal+0x11A; multiplier in FUN_7c85 integrator step */
    int16_t open_loop_p_gain;     /* DAT_0330 — boot from cal+0x11C byte (clamped ≤15); used in `pi_integ_hi = (gain*err)>>4 + ckp` reset branch */

    /* Anti-windup flag set by FUN_672b clamps; consulted by FUN_7c85
     * to gate integration direction. Values: 0 (in-range), 1 (clamped
     * low), 2 (clamped high). */
    uint8_t  pi_flag_c7;          /* DAT_02c7 */

    /* ── PWM output ── */
    uint16_t pwm_duty;            /* 0x02d2 */
    uint16_t pwm_on_time;         /* 0x02ce */
    uint16_t pwm_off_time;        /* 0x02d0 */
    /* pwm_period — total period in Timer2 ticks = on_time + off_time.
     * Mirrored from t06211 RAM[0x02e4] (computed each cycle by the PWM
     * output stage). Exposed for family-1 FBKW.c API parity
     * (UpdatePWM(&htim4, ch, pwm_on_time, pwm_period)). */
    uint16_t pwm_period;
    uint8_t  pwm_duty_range_flag; /* 0x00d1 */
    pwm_interp_slot_t pwm_slot_a;   /* 0x02d4 */
    pwm_interp_slot_t pwm_slot_b;   /* 0x02dc */
    /* pwm_shape_state[6] mirrors RAM[0x02e4..0x02ee]:
     *   [0] pwm_period working value        (RAM 0x02e4)
     *   [1] (unused — was slew_increment, broken out as field below)
     *   [2] (RAM 0x02e8 — ROM band-array ptr, unused in C)
     *   [3] (RAM 0x02ea — ROM halfwidth ptr, unused in C)
     *   [4] (RAM 0x02ec — slew step magnitude, unused; read from cal)
     *   [5] shape_height                     (RAM 0x02ee, E2 output) */
    int16_t  pwm_shape_state[6];  /* 0x02e4-0x02ee */
    /* Persistent slew_increment (RAM[0x02e6]). Carried across cycles so
     * the hysteresis-margin HOLD path can preserve previous direction.
     * Subtracted from pwm_period each cycle. See open-questions §5. */
    int16_t  pwm_slew_increment;  /* 0x02e6 */

    /* ── Bindings (set once by pwm_init) ── */
    const pwm_calibration_t   *bound_cal;
    const pwm_input_getters_t *bound_getters;
} pwm_runtime_t;

/* ── Calibration (decoded ROM values) ───────────────────────────────── */
struct pwm_calibration {
    /* Scalars (from families/t06211/cal_offsets.py FLASH_OFFSETS) */
    int16_t large_pos_error_thresh;   /* cal+0x10E */
    int16_t large_neg_error_thresh;   /* cal+0x110 */
    int16_t pi_low_clamp;             /* cal+0x120 = -512 — Block-4 low clamp + open-loop lower bound */
    int16_t pi_high_clamp;            /* cal+0x124 = +1707 — Block-4 high clamp */

    /* CAN-decoded setpoint (FUN_64c3) cal constants */
    int16_t b_fb_kw_upper_bound;      /* cal+0x004 = +7680 — raw b_fb_kw upper sanity bound */
    int16_t b_fb_kw_lower_bound;      /* cal+0x006 = -768  — raw b_fb_kw lower sanity bound */

    /* setpoint_offset — static bias added after halving raw b_fb_kw.
     * Family-1 analog: CAL+0x0052 - CAL+0x0054 (compact_src/pwm.h:210).
     * For t06211, FUN_6b7b computes the equivalent as cal+0x4c - cal+0x4e
     * (= 3499 - 4156 = -657) at boot and stores at RAM[0x150].
     * Copied into runtime.setpoint_offset by runtime_reset. */
    int16_t setpoint_offset;          /* = cal+0x4c - cal+0x4e */
    int16_t target_5e_min_clamp;      /* cal+0x122 = -512  — RW5E lower clamp */
    int16_t can_aux_12e_max;          /* cal+0x002 — upper bound for RAM[0x12e] (FUN_649e) */
    int16_t error_thresh_114;         /* cal+0x114 */
    int16_t pi_thresh_116;            /* cal+0x116 */
    /* pi_state_118 saturation threshold — when the recovery counter
     * reaches this, FUN_66a8 latches bit0 of system_flags_110, zeroes
     * the counter, and reloads pi_state_c2 from error_thresh_114.
     * Boot-cached at RAM[0x02c0] by FUN_6b7b:0x6bd4. ROM literal 0x0320 = 800. */
    int16_t pi_sat_count_threshold;   /* cal+0x112 */
    int16_t rpm_threshold_11E;        /* cal+0x11E — RPM gate inside FUN_66a8 */
    /* Closed-loop entry RPM floor (FUN_67c4:0x67d9). Sourced from
     * RAM[0x605c] (flash mirror; ROM literal 0x01A4 = 420). */
    int16_t pi_cl_rpm_floor;

    int16_t pwm_detail_x0;            /* cal+0x0EE */
    int16_t pwm_detail_x1;            /* cal+0x0F0 */
    int16_t pwm_cached_ptr_0F2;       /* cal+0x0F2 — first RPM-window breakpoint (legacy scalar) */
    int16_t pwm_cached_ptr_102;       /* cal+0x102 — window halfwidth (legacy scalar) */
    int16_t pwm_const_104;            /* cal+0x104 */

    /* RPM-window matching for pwm_period slew (ROM 0x538b-0x5575).
     * Eight breakpoints at cal+0xF2 define four bands (lo,hi); the
     * cal+0x102 halfwidth adds hysteresis. RPM INSIDE any band drives
     * pwm_period toward pwm_period_min (→ non-zero shape contribution
     * adds ~49 duty ticks). RPM OUTSIDE all bands drives pwm_period
     * toward pwm_period_max (→ zero contribution; Y-table floor). */
    int16_t pwm_rpm_windows[8];       /* cal+0xF2 — 4 (lo,hi) pairs */
    int16_t pwm_window_halfwidth;     /* cal+0x102 — hysteresis halfwidth */
    /* Per-cycle slew step magnitude for pwm_period. Sourced from
     * cal+0x104 (= 354); cached to RAM[0x02ec] at FUN_5314:0x53b9 and
     * read at the Phase 1 and Phase 3 sites. Aliases pwm_const_104 —
     * same offset, semantic name. */
    int16_t pwm_slew_step;            /* cal+0x104 — slew magnitude */

    const int16_t *pwm_y_table;       /* cal+0x154 (indirect) */
    const int16_t *shape_y_table;     /* cal+0x15E (indirect) */

    int16_t closed_loop_gain_const;   /* cached at ROM 0x6056 = 0x000A */

    /* PI runtime-reset values — boot-derived in ROM (FUN_76aa @ 0x76aa);
     * cal-resident here so each variant carries its own values without
     * code edits. Trim bytes at RAM[0x0410-0x0416] have no writers in the
     * ROM (EEPROM trim slots, default 0), so each cal base is used
     * verbatim — see open-questions §2 closeout. Field shape mirrors
     * T06215's pwm_calibration_t for cross-family consistency. */
    int16_t init_p_shape_bound_pos;   /* cal+0x10A — copied to rt->p_shape_bound_pos */
    int16_t init_p_shape_bound_neg;   /* cal+0x10C — copied to rt->p_shape_bound_neg */
    int16_t init_p_gain_normal;       /* cal+0x118 — copied to rt->p_gain_normal */
    int16_t init_integ_step_normal;   /* cal+0x11A — copied to rt->integ_step_normal */
    int16_t init_open_loop_p_gain;    /* cal+0x11C byte clamped to 15 — copied to rt->open_loop_p_gain */
    int16_t init_pos_error_normalizer;/* cal+0x108 — copied to rt->pos_error_normalizer */
    int16_t init_neg_error_normalizer;/* cal+0x106 — copied to rt->neg_error_normalizer */

    uint16_t pwm_period_min;          /* hypothesised; see cal_tables_rom.c */
    uint16_t pwm_period_max;          /* hypothesised */

    /* PWM duty clamp bounds — RAM[0x6058]/RAM[0x605a] flash mirrors.
     * Defaults 0x00CD/0x0F32 match the default family's pwm_min/pwm_max. */
    uint16_t pwm_min;                 /* RAM[0x6058] cache */
    uint16_t pwm_max;                 /* RAM[0x605a] cache */
};

/* ── Submap descriptor ──────────────────────────────────────────────── */
struct pwm_submap_descr {
    uint16_t        flags;       /* +0 */
    const int16_t  *input_ptr;   /* +2 (bound at runtime) */
    uint16_t        count;       /* +4 */
    const int16_t  *x;           /* +6 */
    uint16_t        input_addr;
};

/** Bind descriptor input_ptr fields to rt inputs. */
static inline void pwm_bind_submap_inputs(
    pwm_runtime_t *rt,
    pwm_submap_descr_t *descrs,
    uint16_t n)
{
    for (uint16_t i = 0; i < n; i++) {
        switch (descrs[i].input_addr) {
            case 0x0040: descrs[i].input_ptr = (const int16_t *)&rt->inputs.rpm;            break;
            case 0x0044: descrs[i].input_ptr = &rt->inputs.inj_qty_demand;                  break;
            case 0x0046: descrs[i].input_ptr = &rt->active_request;                         break;
            case 0x0142: descrs[i].input_ptr = (const int16_t *)&rt->inputs.supply_voltage; break;
            default:     descrs[i].input_ptr = NULL;                                        break;
        }
    }
}

/* ══════════════════════════════════════════════════════════════════════
 * PUBLIC API
 * ══════════════════════════════════════════════════════════════════════ */

/** pwm_flash_t — placeholder for family-1 API parity. Family-1 has Y-tables
 *  in a separate `pwm_flash_t` struct; t06211 keeps them inside
 *  pwm_calibration_t so this struct is empty. Kept so FBKW.c / callers
 *  can pass &pwm_flash_rom without the call failing. */
typedef struct pwm_flash { char _unused; } pwm_flash_t;

/** 4-argument init matching family-1's pwm_init signature. The `flash`
 *  pointer is accepted (must be non-NULL — pass &pwm_flash_rom) and
 *  ignored by the t06211 pipeline. */
void pwm_init(pwm_runtime_t *rt,
              const pwm_calibration_t   *cal,
              const pwm_flash_t         *flash,
              const pwm_input_getters_t *getters);

void pwm_service(pwm_runtime_t *rt);

/** @brief CKP-edge reset hook — call from the CKP interrupt handler.
 *
 *  Sets rt->reset_flag = 1 so the next pwm_service() call zeroes the
 *  closed-loop accumulator (supervisor_state) and the enable counter
 *  (cl_enable_counter). Byte store is atomic on all mainstream targets;
 *  no locking required as long as it is not called concurrently with
 *  pwm_service().
 *
 *  Pattern parity with the default family. See
 *  docs/re-guide-ckp-reset-pattern.md for the full rationale.
 *
 *  **Variant note for t06211:** the reset_flag producer is **absent from
 *  the ROM** (no STB #1 site writing to 0x002e). The supervisor at 0x7beb
 *  still consumes reset_flag == 1 and clears it, but nothing inside the
 *  ROM ever sets it. Consequence: the accumulator walk-off hazard is
 *  structurally identical to family-1, and the application layer must
 *  drive this hook itself at engine-rev rate (typically 4 pulses per
 *  revolution on VP44, ~12–15 scheduler cycles apart at 1200 rpm with a
 *  1 kHz scheduler). Without it, supervisor_state integrates forever.
 */
static inline void pwm_ckp_isr(pwm_runtime_t *rt) { rt->reset_flag = 1; }

/** Utility: 1D descending-X piecewise-linear lookup. */
int16_t pwm_interp_lookup(const int16_t *x, const int16_t *y,
                                  uint16_t n, int16_t in);

/** Utility: bypass-PI bilinear LUT — eval(pwm_A, rpm), eval(pwm_B, fbkw),
 *  combine over Y-table, then apply [205, 3890] clamp. Use this to query
 *  the ROM Y-table directly as a (rpm, fbkw) lookup, skipping the PI
 *  controller entirely. Returns the clamped duty. */
uint16_t pwm_lut_duty(const pwm_calibration_t *cal,
                              uint16_t rpm, int16_t fbkw);

/* ── ROM-decoded cal + flash placeholder (defined in cal_tables_rom.c) ── */
extern const pwm_calibration_t pwm_cal_rom;
extern const pwm_flash_t       pwm_flash_rom;

/** Descriptor array indices. */
enum pwm_submap_id {
    PWM_SUBMAP_SETPOINT_INTERP = 0,
    PWM_SUBMAP_PWM_A           = 1,
    PWM_SUBMAP_PWM_B           = 2,
    PWM_SUBMAP_SHAPE_EVAL      = 3,
    PWM_SUBMAP_COUNT           = 4
};

extern pwm_submap_descr_t pwm_submap_descrs[PWM_SUBMAP_COUNT];
extern const int16_t *pwm_submap_y_of(uint16_t idx);

#endif /* PWM_H */
