#include <stdlib.h>

#include "driver/gpio.h"
#include "esp_adc/adc_cali_scheme.h"
#include "esp_adc/adc_oneshot.h"
#include "esp_log.h"
#include "esp_sleep.h"

#include "battery.h"

static const char *TAG = "battery";

#if CONFIG_FRAME_BATTERY_ADC_GPIO >= 0

#define BATTERY_ADC_GPIO CONFIG_FRAME_BATTERY_ADC_GPIO
#define BATTERY_SAMPLES 16
/* Trimmed mean: the extreme BATTERY_TRIM samples on each end (regulator/
 * RF transients, not the true resting voltage) are dropped before
 * averaging the rest -- a plain average lets even one or two of those
 * skew the result enough to read as a real percent change downstream
 * (see the recharge-jump handling in routers/device.py). */
#define BATTERY_TRIM 3
/* The external divider halves the battery voltage (2x200k, per the
 * Seeed-documented XIAO wiring) so a full 4.2V cell reads ~2.1V at the
 * pin, inside the 12dB-attenuation ADC range. */
#define BATTERY_DIVIDER_RATIO 2
/* Plausibility bounds after un-dividing, in mV. Below the floor means
 * no battery attached or a button held on the shared pin (~0V); above
 * the ceiling isn't a 1S LiPo. Either way: no valid reading. */
#define BATTERY_MV_MIN 2900
#define BATTERY_MV_MAX 4350

/* Piecewise-linear 1S LiPo discharge curve, resting voltage -> percent.
 * Coarse deliberately -- an e-ink frame needs "roughly how full", not
 * fuel-gauge precision. */
static const struct {
    int mv;
    int percent;
} LIPO_CURVE[] = {
    { 4200, 100 }, { 4060, 90 }, { 3980, 80 }, { 3920, 70 }, { 3870, 60 },
    { 3820, 50 },  { 3780, 40 }, { 3740, 30 }, { 3680, 20 }, { 3550, 10 },
    { 3300, 5 },   { 3000, 0 },
};

static int int_cmp(const void *a, const void *b)
{
    return *(const int *)a - *(const int *)b;
}

static int mv_to_percent(int mv)
{
    int n = sizeof(LIPO_CURVE) / sizeof(LIPO_CURVE[0]);
    if (mv >= LIPO_CURVE[0].mv) {
        return 100;
    }
    if (mv <= LIPO_CURVE[n - 1].mv) {
        return 0;
    }
    for (int i = 1; i < n; i++) {
        if (mv >= LIPO_CURVE[i].mv) {
            int span_mv = LIPO_CURVE[i - 1].mv - LIPO_CURVE[i].mv;
            int span_pct = LIPO_CURVE[i - 1].percent - LIPO_CURVE[i].percent;
            return LIPO_CURVE[i].percent + (mv - LIPO_CURVE[i].mv) * span_pct / span_mv;
        }
    }
    return 0;
}

static bool on_mains(void)
{
#if CONFIG_FRAME_VBUS_SENSE_GPIO >= 0
    /* The 5V pin only carries voltage when USB is plugged in (dead on
     * battery, per Seeed's docs); an external 2x100k divider halves it
     * to ~2.5V at this pin -- a clean logic high. On battery the
     * divider's bottom resistor holds the pin at GND. No internal pulls:
     * the divider drives the node either way. */
    gpio_config_t io_conf = {
        .pin_bit_mask = 1ULL << CONFIG_FRAME_VBUS_SENSE_GPIO,
        .mode = GPIO_MODE_INPUT,
    };
    gpio_config(&io_conf);
    return gpio_get_level(CONFIG_FRAME_VBUS_SENSE_GPIO) != 0;
#else
    return false; /* no sense pin configured -- can't tell, assume battery */
#endif
}

/* Puts the (shared, see battery.h) pin back on button duty: the same
 * input + pull-up + deep-sleep-wake-arm sequence every button _init()
 * runs. If the pin is NOT actually shared with a button, the extra
 * wake-arm is harmless -- the divider holds the node around 2.9V,
 * far above the wake-on-low threshold, so it can never fire. */
static void restore_button_pin(void)
{
    gpio_config_t io_conf = {
        .pin_bit_mask = 1ULL << BATTERY_ADC_GPIO,
        .mode = GPIO_MODE_INPUT,
        .pull_up_en = GPIO_PULLUP_ENABLE,
    };
    gpio_config(&io_conf);
    esp_sleep_enable_gpio_wakeup_on_hp_periph_powerdown(1ULL << BATTERY_ADC_GPIO, ESP_GPIO_WAKEUP_GPIO_LOW);
}

int battery_read_percent(void)
{
    if (on_mains()) {
        ESP_LOGI(TAG, "On mains power (VBUS present), no battery reading");
        return -1;
    }

    adc_unit_t unit;
    adc_channel_t channel;
    esp_err_t err = adc_oneshot_io_to_channel(BATTERY_ADC_GPIO, &unit, &channel);
    if (err != ESP_OK) {
        ESP_LOGW(TAG, "GPIO%d is not an ADC pin (%s)", BATTERY_ADC_GPIO, esp_err_to_name(err));
        return -1;
    }

    adc_oneshot_unit_init_cfg_t unit_cfg = { .unit_id = unit };
    adc_oneshot_unit_handle_t adc = NULL;
    err = adc_oneshot_new_unit(&unit_cfg, &adc);
    if (err != ESP_OK) {
        ESP_LOGW(TAG, "ADC init failed (%s)", esp_err_to_name(err));
        restore_button_pin();
        return -1;
    }

    adc_oneshot_chan_cfg_t chan_cfg = {
        .atten = ADC_ATTEN_DB_12,
        .bitwidth = ADC_BITWIDTH_DEFAULT,
    };
    err = adc_oneshot_config_channel(adc, channel, &chan_cfg);
    if (err != ESP_OK) {
        ESP_LOGW(TAG, "ADC channel config failed (%s)", esp_err_to_name(err));
        adc_oneshot_del_unit(adc);
        restore_button_pin();
        return -1;
    }

    /* Curve fitting is the ESP32-C6's calibration scheme. Without it,
     * fall back to raw readings scaled by the nominal full-scale range
     * -- coarser, but the percent curve is coarse anyway. */
    adc_cali_handle_t cali = NULL;
    adc_cali_curve_fitting_config_t cali_cfg = {
        .unit_id = unit,
        .chan = channel,
        .atten = ADC_ATTEN_DB_12,
        .bitwidth = ADC_BITWIDTH_DEFAULT,
    };
    bool calibrated = adc_cali_create_scheme_curve_fitting(&cali_cfg, &cali) == ESP_OK;
    if (!calibrated) {
        ESP_LOGW(TAG, "ADC calibration unavailable, using nominal scaling");
    }

    int mv_samples[BATTERY_SAMPLES];
    int samples = 0;
    for (int i = 0; i < BATTERY_SAMPLES; i++) {
        int value;
        if (calibrated) {
            if (adc_oneshot_get_calibrated_result(adc, cali, channel, &value) == ESP_OK) {
                mv_samples[samples++] = value;
            }
        } else {
            if (adc_oneshot_read(adc, channel, &value) == ESP_OK) {
                mv_samples[samples++] = value * 3300 / 4095; /* nominal 12-bit full scale at 12dB */
            }
        }
    }

    if (calibrated) {
        adc_cali_delete_scheme_curve_fitting(cali);
    }
    adc_oneshot_del_unit(adc);
    restore_button_pin();

    if (samples == 0) {
        ESP_LOGW(TAG, "All ADC reads failed");
        return -1;
    }

    /* Only trim if there's enough left afterward to still be a
     * meaningful average -- falls back to a plain average of whatever
     * came in on a wake where most reads failed. */
    qsort(mv_samples, samples, sizeof(int), int_cmp);
    int trim = (samples > 2 * BATTERY_TRIM) ? BATTERY_TRIM : 0;
    int mv_sum = 0;
    int kept = 0;
    for (int i = trim; i < samples - trim; i++) {
        mv_sum += mv_samples[i];
        kept++;
    }

    int battery_mv = (mv_sum / kept) * BATTERY_DIVIDER_RATIO;
    if (battery_mv < BATTERY_MV_MIN || battery_mv > BATTERY_MV_MAX) {
        ESP_LOGI(TAG, "Reading %dmV outside plausible battery range, ignoring", battery_mv);
        return -1;
    }

    int percent = mv_to_percent(battery_mv);
    ESP_LOGI(TAG, "Battery: %dmV -> %d%%", battery_mv, percent);
    return percent;
}

#else

int battery_read_percent(void) { return -1; }

#endif
