#include "HeaveController.hpp"
#include <Arduino.h>
#include <avr/pgmspace.h>

static const float MAX_INTEGRAL = 1e4f;

// Gap [mm] → equilibrium PWM (+ = repel, - = attract).
// 64 entries over 3–20 mm at 0.269841 mm steps. Copied from Controller.cpp.
static const int16_t HEAVE_FF_LUT[HEAVE_FF_LUT_SIZE] PROGMEM = {
   238,  238,  238,  238,  238,  238,  238,  238,
   238,  238,  238,  238,  238,  238,  238,  238,
   238,  238,  234,  219,  204,  188,  172,  157,
   141,  125,  109,   93,   77,   61,   45,   29,
    13,   -3,  -19,  -35,  -51,  -67,  -84, -100,
  -116, -133, -150, -166, -183, -200, -217, -234,
  -250, -250, -250, -250, -250, -250, -250, -250,
  -250, -250, -250, -250, -250, -250, -250, -250
};

HeaveController::HeaveController(
    IndSensorL& f, IndSensorL& b,
    HeavePIDGains g, float avgRef, float rampStep_, bool useFeedforward)
  : oor(false), outputOn(false),
    Front(f), Back(b),
    gains(g), state({0, 0, 0}),
    AvgRef(avgRef), targetRef(avgRef), rampStep(rampStep_),
    avg(0), PWM(0), ffPWM(0),
    fullAttract(false), ffEnabled(useFeedforward),
    prevPidActive(false)
{}

void HeaveController::update() {
  Front.readMM();
  Back.readMM();

  oor = Front.oor || Back.oor;

  avg = (Front.mmVal + Back.mmVal) * 0.5f;

  // Seed AvgRef from current position on entry to PID-active mode, then
  // step it toward targetRef by rampStep each tick. This applies to both
  // off→on (cmd '1') and full-attract→PID (cmd '2'→'1') transitions.
  bool pidActive = outputOn && !fullAttract;
  if (pidActive) {
    if (!prevPidActive) {
      AvgRef = avg;
      state.eInt = 0;
    }
    float delta = targetRef - AvgRef;
    if (delta >  rampStep)       AvgRef += rampStep;
    else if (delta < -rampStep)  AvgRef -= rampStep;
    else                         AvgRef = targetRef;
  }
  prevPidActive = pidActive;

  float e = AvgRef - avg;
  state.eDiff = e - state.e;
  if (!oor && !fullAttract) {
    state.eInt += e;
    state.eInt = constrain(state.eInt, -MAX_INTEGRAL, MAX_INTEGRAL);
  }
  state.e = e;

  ffPWM = ffEnabled ? feedforward(avg) : 0;

  if (fullAttract) {
    PWM = -HEAVE_CAP;  // manual override — ignore OOR, drive coils unconditionally
  } else if (oor) {
    // Sensor out of LUT range → trust feedforward alone (PID input is clamped).
    // avg is clamped to [mmMin, mmMax] so FF saturates toward repel when too close,
    // attract when too far — which is exactly the bring-up behavior.
    PWM = ffPWM;
  } else {
    // Gain-schedule PID output by (z/z_ref)² to linearize 1/z² force law.
    // Floor z to 2mm so sensor dropouts don't collapse control authority.
    // float z = max(avg, 2.0f);
    // float scale = (z * z) / (AvgRef * AvgRef);
    // PWM = constrain(ffPWM + (int16_t)(scale * pidCompute()), -HEAVE_CAP, HEAVE_CAP);    // In range: feedforward provides gravity/equilibrium bias, PID corrects residual.
    PWM = constrain(ffPWM + pidCompute(), -HEAVE_CAP, HEAVE_CAP);
  }
}

// Linearly interpolate the PROGMEM feedforward LUT.
int16_t HeaveController::feedforward(float gapMM) {
  if (gapMM <= HEAVE_FF_GAP_MIN) return (int16_t)pgm_read_word(&HEAVE_FF_LUT[0]);
  if (gapMM >= HEAVE_FF_GAP_MAX) return (int16_t)pgm_read_word(&HEAVE_FF_LUT[HEAVE_FF_LUT_SIZE - 1]);

  float idx_f = (gapMM - HEAVE_FF_GAP_MIN) / HEAVE_FF_GAP_STEP;
  uint8_t idx = (uint8_t)idx_f;
  if (idx >= HEAVE_FF_LUT_SIZE - 1) idx = HEAVE_FF_LUT_SIZE - 2;

  int16_t v0 = (int16_t)pgm_read_word(&HEAVE_FF_LUT[idx]);
  int16_t v1 = (int16_t)pgm_read_word(&HEAVE_FF_LUT[idx + 1]);
  float frac = idx_f - (float)idx;
  return (int16_t)(v0 + frac * (v1 - v0));
}

int16_t HeaveController::pidCompute() {
  if (oor) return 0;
  float out = gains.kp * state.e
            + gains.ki * state.eInt
            + gains.kd * state.eDiff;
  return (int16_t)constrain(out, -HEAVE_CAP, HEAVE_CAP);
}

void HeaveController::zeroPWMs() {
  PWM = 0;
}

// Drive all four motor channels identically. Direction bit mirrors the sign
// convention from Controller.cpp: LOW = repelling (PWM > 0), HIGH = attracting.
void HeaveController::sendOutputs() {
  if (!outputOn) zeroPWMs();

  bool attract = (PWM < 0);
  uint8_t mag = (uint8_t)abs(PWM);

  digitalWrite(dirFL, attract);
  digitalWrite(dirBL, attract);
  digitalWrite(dirFR, attract);
  digitalWrite(dirBR, attract);

  OCR2A = mag;  // Pin 11 (FL)
  OCR1A = mag;  // Pin 9  (FR)
  OCR2B = mag;  // Pin 3  (BL)
  OCR1B = mag;  // Pin 10 (BR)
}

void HeaveController::report() {
  // CSV: Front,Back,Avg,ActiveRef,PWM,ControlOn
  Serial.print(Front.mmVal); Serial.print(',');
  Serial.print(Back.mmVal);  Serial.print(',');
  Serial.print(avg);         Serial.print(',');
  Serial.print(AvgRef);      Serial.print(',');
  Serial.print(PWM);         Serial.print(',');
  Serial.println(outputOn);
}

void HeaveController::updatePID(HeavePIDGains g) { gains = g; }
void HeaveController::updateReference(float avgReference) { targetRef = avgReference; }
void HeaveController::setFullAttract(bool enabled) {
  fullAttract = enabled;
  if (enabled) state.eInt = 0;  // drop stale integral so PID resume is clean
}
