#include "refs.h"
#include "ble.h"
#include "temp.h"
#include "ad5940.h"
#include <string.h>
#include <stdio.h>
#include <math.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

/* LP RTIA register mapping (mirrors echem.c) */
extern const uint32_t lp_rtia_map[];
extern const float lp_rtia_ohms[];

/* ---- ADC helpers ---- */

static int32_t read_adc_code(void)
{
    AD5940_INTCClrFlag(AFEINTSRC_SINC2RDY);
    AD5940_AFECtrlS(AFECTRL_ADCPWR, bTRUE);
    AD5940_Delay10us(25);
    AD5940_AFECtrlS(AFECTRL_ADCCNV, bTRUE);

    AD5940_ClrMCUIntFlag();
    while (!AD5940_GetMCUIntFlag())
        vTaskDelay(1);

    AD5940_AFECtrlS(AFECTRL_ADCCNV | AFECTRL_ADCPWR, bFALSE);
    AD5940_INTCClrFlag(AFEINTSRC_SINC2RDY);

    uint32_t raw = AD5940_ReadAfeResult(AFERESULT_SINC2);
    return (raw & (1UL << 15)) ? (int32_t)(raw | 0xFFFF0000UL) : (int32_t)raw;
}

/* ---- Clipping detection ---- */

#define ADC_OK          0
#define ADC_CLIPPED     1
#define ADC_OSCILLATING 2
#define CLIP_THRESHOLD  30000
#define PROBE_SAMPLES   8

static void init_lp_for_probe(uint32_t rtia_reg)
{
    CLKCfg_Type clk;
    memset(&clk, 0, sizeof(clk));
    clk.HFOSCEn         = bTRUE;
    clk.HfOSC32MHzMode  = bFALSE;
    clk.SysClkSrc        = SYSCLKSRC_HFOSC;
    clk.ADCCLkSrc        = ADCCLKSRC_HFOSC;
    clk.SysClkDiv        = SYSCLKDIV_1;
    clk.ADCClkDiv        = ADCCLKDIV_1;
    clk.LFOSCEn          = bTRUE;
    clk.HFXTALEn         = bFALSE;
    AD5940_CLKCfg(&clk);

    AFERefCfg_Type ref;
    AD5940_StructInit(&ref, sizeof(ref));
    ref.HpBandgapEn  = bTRUE;
    ref.Hp1V1BuffEn  = bTRUE;
    ref.Hp1V8BuffEn  = bTRUE;
    ref.LpBandgapEn  = bTRUE;
    ref.LpRefBufEn   = bTRUE;
    ref.LpRefBoostEn = bTRUE;
    AD5940_REFCfgS(&ref);

    AD5940_AFEPwrBW(AFEPWR_LP, AFEBW_250KHZ);

    LPLoopCfg_Type lp;
    AD5940_StructInit(&lp, sizeof(lp));
    lp.LpDacCfg.LpdacSel      = LPDAC0;
    lp.LpDacCfg.LpDacSrc      = LPDACSRC_MMR;
    lp.LpDacCfg.LpDacVzeroMux = LPDACVZERO_6BIT;
    lp.LpDacCfg.LpDacVbiasMux = LPDACVBIAS_12BIT;
    lp.LpDacCfg.LpDacSW       = LPDACSW_VZERO2LPTIA | LPDACSW_VBIAS2LPPA;
    lp.LpDacCfg.LpDacRef      = LPDACREF_2P5;
    lp.LpDacCfg.DataRst       = bFALSE;
    lp.LpDacCfg.PowerEn       = bTRUE;
    lp.LpDacCfg.DacData6Bit   = 26;  /* VZERO ~1094mV */
    lp.LpDacCfg.DacData12Bit  = (uint16_t)((1093.75f - 200.0f) / 0.537f + 0.5f); /* 0mV cell */

    lp.LpAmpCfg.LpAmpSel    = LPAMP0;
    lp.LpAmpCfg.LpAmpPwrMod = LPAMPPWR_BOOST3;
    lp.LpAmpCfg.LpPaPwrEn   = bTRUE;
    lp.LpAmpCfg.LpTiaPwrEn  = bTRUE;
    lp.LpAmpCfg.LpTiaRf     = LPTIARF_SHORT;
    lp.LpAmpCfg.LpTiaRload  = LPTIARLOAD_SHORT;
    lp.LpAmpCfg.LpTiaRtia   = rtia_reg;
    lp.LpAmpCfg.LpTiaSW     = LPTIASW(2) | LPTIASW(4) | LPTIASW(5) |
                                LPTIASW(7) | LPTIASW(12);
    AD5940_LPLoopCfgS(&lp);

    ADCBaseCfg_Type adc;
    adc.ADCMuxP = ADCMUXP_LPTIA0_P;
    adc.ADCMuxN = ADCMUXN_LPTIA0_N;
    adc.ADCPga  = ADCPGA_1P5;
    AD5940_ADCBaseCfgS(&adc);

    ADCFilterCfg_Type filt;
    AD5940_StructInit(&filt, sizeof(filt));
    filt.ADCSinc3Osr         = ADCSINC3OSR_4;
    filt.ADCSinc2Osr         = ADCSINC2OSR_667;
    filt.ADCAvgNum           = ADCAVGNUM_16;
    filt.ADCRate             = ADCRATE_800KHZ;
    filt.BpNotch             = bFALSE;
    filt.BpSinc3             = bFALSE;
    filt.Sinc2NotchEnable    = bTRUE;
    filt.Sinc3ClkEnable      = bTRUE;
    filt.Sinc2NotchClkEnable = bTRUE;
    filt.DFTClkEnable        = bFALSE;
    filt.WGClkEnable         = bFALSE;
    AD5940_ADCFilterCfgS(&filt);

    AD5940_INTCCfg(AFEINTC_0, AFEINTSRC_SINC2RDY, bTRUE);
    AD5940_INTCCfg(AFEINTC_1, AFEINTSRC_SINC2RDY, bTRUE);
    AD5940_INTCClrFlag(AFEINTSRC_ALLINT);

    AGPIOCfg_Type gpio;
    AD5940_StructInit(&gpio, sizeof(gpio));
    gpio.FuncSet     = GP0_INT;
    gpio.OutputEnSet = AGPIO_Pin0;
    AD5940_AGPIOCfg(&gpio);

    AD5940_WriteReg(REG_AFE_FIFOCON, 0);
}

static void shutdown_lp(void)
{
    LPLoopCfg_Type lp;
    AD5940_StructInit(&lp, sizeof(lp));
    lp.LpDacCfg.LpdacSel      = LPDAC0;
    lp.LpDacCfg.LpDacSrc      = LPDACSRC_MMR;
    lp.LpDacCfg.LpDacVzeroMux = LPDACVZERO_6BIT;
    lp.LpDacCfg.LpDacVbiasMux = LPDACVBIAS_12BIT;
    lp.LpDacCfg.LpDacSW       = 0;
    lp.LpDacCfg.LpDacRef      = LPDACREF_2P5;
    lp.LpDacCfg.PowerEn       = bFALSE;
    lp.LpDacCfg.DataRst       = bFALSE;
    lp.LpDacCfg.DacData6Bit   = 0;
    lp.LpDacCfg.DacData12Bit  = 0;
    lp.LpAmpCfg.LpAmpSel      = LPAMP0;
    lp.LpAmpCfg.LpAmpPwrMod   = LPAMPPWR_NORM;
    lp.LpAmpCfg.LpPaPwrEn     = bFALSE;
    lp.LpAmpCfg.LpTiaPwrEn    = bFALSE;
    lp.LpAmpCfg.LpTiaRf       = LPTIARF_OPEN;
    lp.LpAmpCfg.LpTiaRload    = LPTIARLOAD_SHORT;
    lp.LpAmpCfg.LpTiaRtia     = LPTIARTIA_OPEN;
    lp.LpAmpCfg.LpTiaSW       = 0;
    AD5940_LPLoopCfgS(&lp);

    SWMatrixCfg_Type sw = { SWD_OPEN, SWP_OPEN, SWN_OPEN, SWT_OPEN };
    AD5940_SWMatrixCfgS(&sw);
}

static int check_adc_clipping(uint8_t lp_rtia_idx)
{
    init_lp_for_probe(lp_rtia_map[lp_rtia_idx]);
    vTaskDelay(pdMS_TO_TICKS(50));

    int clip_count = 0;
    int sign_changes = 0;
    int any_high = 0;
    int32_t prev_sign = 0;

    for (int i = 0; i < PROBE_SAMPLES; i++) {
        int32_t code = read_adc_code();
        int32_t abs_code = code < 0 ? -code : code;

        if (abs_code > CLIP_THRESHOLD) {
            clip_count++;
            any_high = 1;
        }

        int32_t cur_sign = (code > 0) ? 1 : ((code < 0) ? -1 : 0);
        if (i > 0 && cur_sign != 0 && prev_sign != 0 && cur_sign != prev_sign)
            sign_changes++;
        if (cur_sign != 0) prev_sign = cur_sign;
    }

    shutdown_lp();
    AD5940_AFECtrlS(AFECTRL_ALL, bFALSE);

    if (sign_changes >= 3 && any_high)
        return ADC_OSCILLATING;
    if (clip_count > PROBE_SAMPLES / 2)
        return ADC_CLIPPED;
    return ADC_OK;
}

static void lp_find_valid_range(LpRtiaRange *range)
{
    int low_clip = check_adc_clipping(0);
    int high_clip = check_adc_clipping(LP_RTIA_512K);

    uint8_t lo = 0, hi = LP_RTIA_512K;

    if (low_clip != ADC_OK) {
        /* binary search upward for first non-clipping */
        uint8_t a = 0, b = LP_RTIA_512K;
        while (a < b) {
            uint8_t m = (a + b) / 2;
            if (check_adc_clipping(m) != ADC_OK)
                a = m + 1;
            else
                b = m;
        }
        lo = a;
    }

    if (high_clip != ADC_OK) {
        /* binary search downward for last non-clipping */
        uint8_t a = lo, b = LP_RTIA_512K;
        while (a < b) {
            uint8_t m = (a + b + 1) / 2;
            if (check_adc_clipping(m) != ADC_OK)
                a = m;
            else
                b = m - 1;
        }
        hi = a;
    }

    range->low_idx = lo;
    range->high_idx = hi;
    range->valid = (lo <= hi) ? 1 : 0;
}

/* ---- Collection ---- */

void refs_collect(RefStore *store, const EISConfig *cfg)
{
    memset(store, 0, sizeof(*store));

    /* EIS phase: sweep each RTIA 0-7 with RCAL=3K */
    EISConfig ref_cfg = *cfg;
    ref_cfg.rcal = RCAL_3K;

    for (int r = 0; r < REF_EIS_RTIA_COUNT; r++) {
        ref_cfg.rtia = (EISRtia)r;
        eis_init(&ref_cfg);

        ble_send_ref_frame(REF_MODE_EIS, (uint8_t)r);

        uint32_t n = eis_calc_num_points(&ref_cfg);
        ble_send_sweep_start(n, ref_cfg.freq_start_hz, ref_cfg.freq_stop_hz);

        int got = eis_sweep(store->eis[r].pts, n, ble_send_eis_point);
        store->eis[r].n_points = (uint32_t)got;
        store->eis[r].valid = (got > 0) ? 1 : 0;

        ble_send_sweep_end();
        printf("Ref EIS RTIA %d: %d pts\n", r, got);
    }

    /* LP phase: binary search valid RTIA range for each LP mode */
    printf("Ref: LP range search (LSV)\n");
    ble_send_ref_frame(REF_MODE_LSV, 0);
    lp_find_valid_range(&store->lsv_range);
    if (store->lsv_range.valid)
        ble_send_ref_lp_range(REF_MODE_LSV, store->lsv_range.low_idx, store->lsv_range.high_idx);
    printf("Ref LSV range: %u-%u\n", store->lsv_range.low_idx, store->lsv_range.high_idx);

    printf("Ref: LP range search (Amp)\n");
    ble_send_ref_frame(REF_MODE_AMP, 0);
    lp_find_valid_range(&store->amp_range);
    if (store->amp_range.valid)
        ble_send_ref_lp_range(REF_MODE_AMP, store->amp_range.low_idx, store->amp_range.high_idx);
    printf("Ref Amp range: %u-%u\n", store->amp_range.low_idx, store->amp_range.high_idx);

    printf("Ref: LP range search (Cl)\n");
    ble_send_ref_frame(REF_MODE_CL, 0);
    lp_find_valid_range(&store->cl_range);
    if (store->cl_range.valid)
        ble_send_ref_lp_range(REF_MODE_CL, store->cl_range.low_idx, store->cl_range.high_idx);
    printf("Ref Cl range: %u-%u\n", store->cl_range.low_idx, store->cl_range.high_idx);

    /* pH phase: OCP measurement */
    printf("Ref: pH OCP\n");
    ble_send_ref_frame(REF_MODE_PH, 0);
    PhConfig ph_cfg;
    ph_cfg.stabilize_s = 10.0f;
    ph_cfg.temp_c = temp_get();
    echem_ph_ocp(&ph_cfg, &store->ph_ref);
    store->ph_valid = 1;
    ble_send_ph_result(store->ph_ref.v_ocp_mv, store->ph_ref.ph, store->ph_ref.temp_c);

    store->has_refs = 1;
    ble_send_refs_done();
    printf("Ref collection complete\n");
}

/* ---- Send stored refs to client ---- */

void refs_send(const RefStore *store)
{
    if (!store->has_refs) {
        ble_send_ref_status(0);
        return;
    }
    ble_send_ref_status(1);

    for (int r = 0; r < REF_EIS_RTIA_COUNT; r++) {
        if (!store->eis[r].valid) continue;

        ble_send_ref_frame(REF_MODE_EIS, (uint8_t)r);
        uint32_t n = store->eis[r].n_points;
        ble_send_sweep_start(n, store->eis[r].pts[0].freq_hz,
                             store->eis[r].pts[n - 1].freq_hz);
        for (uint32_t i = 0; i < n; i++)
            ble_send_eis_point((uint16_t)i, &store->eis[r].pts[i]);
        ble_send_sweep_end();
    }

    if (store->lsv_range.valid)
        ble_send_ref_lp_range(REF_MODE_LSV, store->lsv_range.low_idx, store->lsv_range.high_idx);
    if (store->amp_range.valid)
        ble_send_ref_lp_range(REF_MODE_AMP, store->amp_range.low_idx, store->amp_range.high_idx);
    if (store->cl_range.valid)
        ble_send_ref_lp_range(REF_MODE_CL, store->cl_range.low_idx, store->cl_range.high_idx);

    if (store->ph_valid) {
        ble_send_ref_frame(REF_MODE_PH, 0);
        ble_send_ph_result(store->ph_ref.v_ocp_mv, store->ph_ref.ph, store->ph_ref.temp_c);
    }

    ble_send_refs_done();
}

void refs_clear(RefStore *store)
{
    memset(store, 0, sizeof(*store));
    ble_send_ref_status(0);
}
