/*
 * slic.c — K50835F / AG1171-class SLIC control (ESP-IDF port).
 *
 * Ported from hardware/projects/slic-phone/src/slic/Ks0835SlicController.cpp
 * (Arduino) to bare ESP-IDF 5.x GPIO driver.
 *
 * Key differences from Arduino original:
 *  - Open-drain on PD achieved via GPIO_MODE_OUTPUT_OD + gpio_set_level(PD, 1)
 *  - Ring FR toggle runs as a FreeRTOS task instead of a cooperative tick()
 *  - hook_active_high is hard-coded TRUE (matches A252ConfigStore default)
 */

#include "slic.h"
#include "board_config.h"

#include "driver/gpio.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#define TAG "slic"

/* Empirically on THIS A1S+SLIC unit, SHK is ACTIVE-HIGH: off-hook (loop closed)
 * drives SHK HIGH, on-hook drives it LOW.  Confirmed at the bench via
 * /debug/hookmon: on-hook→0, pickup-during-ring→0→1 transition, off-hook→1.
 * (The A252 reference was active-low; the polarity is inverted here, so the
 * raw straight-wired GPIO reads off-hook = HIGH.) */
#define SLIC_SHK_OFFHOOK_LEVEL  1

static volatile bool s_ringing = false;
static TaskHandle_t  s_ring_task = NULL;

/* France Télécom ring cadence: 1.5 s burst ON, 3.5 s silent pause, repeating.
 * (Single-ring cadence — distinct from the UK double-ring or US 2 s/4 s.)
 * The bell is driven during the burst by toggling FR at ~25 Hz with RM HIGH;
 * during the pause RM and FR are held low so the bell is silent. */
#define RING_FR_TOGGLE_MS  20     /* 20 ms half-period → ~25 Hz bell drive */
#define RING_BURST_MS      1500   /* FT: 1.5 s of ringing */
#define RING_PAUSE_MS      3500   /* FT: 3.5 s of silence */

/* ── FR toggle + cadence task ─────────────────────────────────────────────── */

static void slic_ring_task(void *arg)
{
    (void)arg;
    bool fr_state = false;
    bool in_burst = true;     /* start each ring with a burst */
    int  phase_ms = 0;        /* elapsed ms in the current burst/pause phase */

    for (;;) {
        if (s_ringing) {
            if (in_burst) {
                /* Ring burst: RM HIGH, FR toggling at ~25 Hz to swing the bell */
                gpio_set_level(PLIP_SLIC_RM, 1);
                fr_state = !fr_state;
                gpio_set_level(PLIP_SLIC_FR, fr_state ? 1 : 0);
                phase_ms += RING_FR_TOGGLE_MS;
                if (phase_ms >= RING_BURST_MS) {
                    /* End of burst → enter silent pause */
                    in_burst = false;
                    phase_ms = 0;
                    fr_state = false;
                    gpio_set_level(PLIP_SLIC_RM, 0);
                    gpio_set_level(PLIP_SLIC_FR, 0);
                    ESP_LOGV(TAG, "ring: burst end -> pause");
                }
            } else {
                /* Silent pause: RM/FR stay low */
                phase_ms += RING_FR_TOGGLE_MS;
                if (phase_ms >= RING_PAUSE_MS) {
                    in_burst = true;
                    phase_ms = 0;
                    ESP_LOGV(TAG, "ring: pause end -> burst");
                }
            }
        } else {
            /* Idle — keep RM/FR low and reset the cadence for the next ring */
            gpio_set_level(PLIP_SLIC_RM, 0);
            gpio_set_level(PLIP_SLIC_FR, 0);
            fr_state = false;
            in_burst = true;
            phase_ms = 0;
        }
        vTaskDelay(pdMS_TO_TICKS(RING_FR_TOGGLE_MS));
    }
}

/* ── Public API ──────────────────────────────────────────────────────────── */

esp_err_t slic_init(void)
{
    /* RM: Ring Mode output, init LOW.
     * INPUT_OUTPUT (not plain OUTPUT) so gpio_get_level() reads back the real
     * pad level — plain OUTPUT disables the input buffer and always reads 0. */
    gpio_config_t rm_cfg = {
        .pin_bit_mask = (1ULL << PLIP_SLIC_RM),
        .mode         = GPIO_MODE_INPUT_OUTPUT,
        .pull_up_en   = GPIO_PULLUP_DISABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type    = GPIO_INTR_DISABLE,
    };
    esp_err_t ret = gpio_config(&rm_cfg);
    if (ret != ESP_OK) return ret;
    gpio_set_level(PLIP_SLIC_RM, 0);

    /* FR: Forward/Reverse output, init LOW (INPUT_OUTPUT for readback, see RM). */
    gpio_config_t fr_cfg = {
        .pin_bit_mask = (1ULL << PLIP_SLIC_FR),
        .mode         = GPIO_MODE_INPUT_OUTPUT,
        .pull_up_en   = GPIO_PULLUP_DISABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type    = GPIO_INTR_DISABLE,
    };
    ret = gpio_config(&fr_cfg);
    if (ret != ESP_OK) return ret;
    gpio_set_level(PLIP_SLIC_FR, 0);

    /* SHK: Switch Hook input with pull-up (physical line has no external pull) */
    gpio_config_t shk_cfg = {
        .pin_bit_mask = (1ULL << PLIP_SLIC_SHK),
        .mode         = GPIO_MODE_INPUT,
        .pull_up_en   = GPIO_PULLUP_ENABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type    = GPIO_INTR_DISABLE,
    };
    ret = gpio_config(&shk_cfg);
    if (ret != ESP_OK) return ret;

    /* PD: Power Down — EXACT A252-proven sequence: open-drain output, HIGH = released
     * = SLIC active (setPowerDown(false) in Ks0835SlicController). This is the config
     * the working Arduino slic-phone project uses on the same chip. */
    /* INPUT_OUTPUT_OD (not plain OUTPUT_OD) so gpio_get_level() reads the real
     * pad level — OUTPUT_OD also disables the input buffer and would read 0. */
    ret = gpio_set_direction(PLIP_SLIC_PD, GPIO_MODE_INPUT_OUTPUT_OD);
    if (ret != ESP_OK) return ret;
    ret = gpio_set_level(PLIP_SLIC_PD, 1);   /* open-drain released HIGH = active (A252-proven) */
    if (ret != ESP_OK) return ret;

    ESP_LOGI(TAG, "slic: PD GPIO%d open-drain HIGH (A252-proven active) — testing with fixed mic", PLIP_SLIC_PD);
    ESP_LOGI(TAG, "slic: pins RM=%d FR=%d SHK=%d PD=%d hook_active_high=1",
             PLIP_SLIC_RM, PLIP_SLIC_FR, PLIP_SLIC_SHK, PLIP_SLIC_PD);

    /* Spawn the FR-toggle task (runs indefinitely, toggles only when s_ringing=true) */
    BaseType_t ok = xTaskCreatePinnedToCore(slic_ring_task, "slic_ring",
                                             2048, NULL, 4, &s_ring_task, 1);
    if (ok != pdPASS) {
        ESP_LOGE(TAG, "Failed to create slic_ring task");
        return ESP_ERR_NO_MEM;
    }

    int shk = gpio_get_level(PLIP_SLIC_SHK);
    ESP_LOGI(TAG, "SLIC init OK — SHK level=%d (%s)",
             shk, (shk == SLIC_SHK_OFFHOOK_LEVEL) ? "off-hook" : "on-hook");
    return ESP_OK;
}

bool slic_is_offhook(void)
{
    return gpio_get_level(PLIP_SLIC_SHK) == SLIC_SHK_OFFHOOK_LEVEL;
}

void slic_ring_start(void)
{
    if (s_ringing) return;
    ESP_LOGI(TAG, "ring start: France Télécom cadence %d ms ON / %d ms OFF",
             RING_BURST_MS, RING_PAUSE_MS);
    /* The cadence task drives RM/FR; it starts on a burst (in_burst reset in
     * the idle branch). Just arm it here. */
    s_ringing = true;
}

void slic_ring_stop(void)
{
    if (!s_ringing) return;
    ESP_LOGI(TAG, "ring stop: RM=LOW, FR=LOW");
    s_ringing = false;
    gpio_set_level(PLIP_SLIC_RM, 0);
    gpio_set_level(PLIP_SLIC_FR, 0);
}

bool slic_is_ringing(void)
{
    return s_ringing;
}
