/*
 * phone.c — Off-hook GPIO debounce + rotary pulse decoding + ring control.
 *
 * ISR sets a flag; the task reads the GPIO level after a short debounce
 * and reports the transition via hook_client_report() and
 * conversation_on_hook_change().
 *
 * Rotary pulse decoding (CONFIG_PLIP_DIAL_PULSE):
 *   While off-hook, a rotary dial produces brief open/close pulses on the
 *   SHK line (~60-100 ms open per pulse).  A real hangup holds the line
 *   open for >500 ms.  The task polls the GPIO at 5 ms intervals to catch
 *   pulses that would be missed by the 30 ms debounce.
 *
 *   Pulse train end: if the line stays closed for > PLIP_DIAL_PULSE_MAX_GAP_MS
 *   after at least one pulse, the count is emitted as a digit (10 pulses = 0).
 */

#include "phone.h"
#include "audio.h"
#include "hook_client.h"
#include "conversation.h"
#include "slic.h"

#if CONFIG_PLIP_DIAL_PULSE
#include "dialer.h"
#endif

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

#define TAG "phone"

#define DEBOUNCE_MS          30
#define HANGUP_THRESHOLD_MS  500   /* open > this → real hangup, not a pulse */
#define TASK_STACK           4096
#define TASK_PRIO            5

static volatile bool s_edge_pending = false;
static volatile bool s_ringing      = false;
static volatile bool s_offhook      = false;  /* true = handset picked up */

/* IRAM_ATTR: ISR must live in IRAM on original ESP32. */
static void IRAM_ATTR on_hook_isr(void *arg)
{
    (void)arg;
    s_edge_pending = true;
}

void phone_ring_start(void)
{
    if (s_ringing) return;
    s_ringing = true;
    ESP_LOGI(TAG, "ring start: SLIC RM/FR + audio tone");
    /* Physical bell via SLIC RM/FR (main ringer) */
    slic_ring_start();
    /* Optional in-earpiece audio tone (audible feedback when handset is up) */
    audio_ring_start();
}

void phone_ring_stop(void)
{
    if (!s_ringing) return;
    s_ringing = false;
    ESP_LOGI(TAG, "ring stop: SLIC RM/FR off + audio stop");
    slic_ring_stop();
    audio_stop();
}

static void report_offhook(bool offhook)
{
    conversation_on_hook_change(offhook);
    hook_client_report(offhook ? "off" : "on", offhook ? "pickup" : "hangup");
}

/*
 * Hook polarity:
 *   CONFIG_PLIP_HOOK_ACTIVE_HIGH=y  (default, SLIC SHK GPIO23): HIGH = off-hook
 *   CONFIG_PLIP_HOOK_ACTIVE_HIGH=n  (legacy BOOT button GPIO4): LOW  = off-hook
 *
 * Rotary pulse notes (SLIC SHK, active-HIGH):
 *   Off-hook base: SHK HIGH.  A rotary pulse briefly opens the loop → SHK drops LOW
 *   for ~60-100 ms, then returns HIGH.  Hangup: SHK stays LOW for >500 ms.
 *   So "open" (pulse event) = level drops LOW when active-HIGH polarity.
 */
#if CONFIG_PLIP_HOOK_ACTIVE_HIGH
#define HOOK_OFFHOOK_LEVEL  1   /* HIGH = off-hook */
#define HOOK_PULSE_OPEN     0   /* LOW  = rotary pulse open (loop broken) */
#define HOOK_PULSE_CLOSED   1   /* HIGH = rotary pulse closed (loop restored) */
#else
#define HOOK_OFFHOOK_LEVEL  0   /* LOW  = off-hook (legacy pull-up + BOOT button) */
#define HOOK_PULSE_OPEN     1   /* HIGH = rotary pulse open */
#define HOOK_PULSE_CLOSED   0   /* LOW  = rotary pulse closed */
#endif

static void phone_task(void *arg)
{
    (void)arg;
    const int hook_gpio = CONFIG_PLIP_HOOK_GPIO;

    gpio_config_t io_conf = {
        .pin_bit_mask = (1ULL << hook_gpio),
        .mode         = GPIO_MODE_INPUT,
        .pull_up_en   = GPIO_PULLUP_ENABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type    = GPIO_INTR_ANYEDGE,
    };
    ESP_ERROR_CHECK(gpio_config(&io_conf));
    ESP_ERROR_CHECK(gpio_install_isr_service(0));
    ESP_ERROR_CHECK(gpio_isr_handler_add(hook_gpio, on_hook_isr, NULL));

    /* Read and report initial level so master state machine is in sync. */
    int last_level = gpio_get_level(hook_gpio);
    s_offhook = (last_level == HOOK_OFFHOOK_LEVEL);
    ESP_LOGI(TAG, "phone task ready, hook GPIO=%d level=%d active_%s (%s)",
             hook_gpio, last_level,
             (HOOK_OFFHOOK_LEVEL == 1) ? "high" : "low",
             s_offhook ? "off-hook" : "on-hook");

    if (!s_offhook) {
        ESP_LOGI(TAG, "boot: on-hook -> PA off (mute)");
        audio_pa_set(false);
    } else {
        ESP_LOGI(TAG, "boot: off-hook -> PA on (unmute)");
        audio_pa_set(true);
    }

    hook_client_report(s_offhook ? "off" : "on", "boot");
    conversation_on_hook_change(s_offhook);

#if CONFIG_PLIP_DIAL_PULSE
    /* Rotary pulse state.
     * With SLIC SHK (active-HIGH): off-hook = HIGH, pulse = brief LOW dip. */
    int     pulse_count   = 0;
    bool    in_pulse      = false;   /* true while SHK is in pulse-open state */
    int64_t pulse_open_us = 0;       /* timestamp when pulse open started */
    int64_t last_close_us = 0;       /* timestamp when pulse closed (SHK back to OFFHOOK) */
#endif

    for (;;) {
        if (s_edge_pending) {
            s_edge_pending = false;

#if CONFIG_PLIP_DIAL_PULSE
            /* While off-hook, use fast polling instead of 30ms debounce
             * so rotary pulses (~60-100ms) are not missed. */
            if (s_offhook) {
                /* The ISR fired — re-read immediately to catch the edge. */
                vTaskDelay(pdMS_TO_TICKS(5));  /* minimal settle */
                int level = gpio_get_level(hook_gpio);

                if (level == HOOK_PULSE_OPEN && last_level == HOOK_PULSE_CLOSED) {
                    /* Pulse open: loop broken (SHK dropped from off-hook level) */
                    last_level    = HOOK_PULSE_OPEN;
                    in_pulse      = true;
                    pulse_open_us = esp_timer_get_time();
                    ESP_LOGD(TAG, "pulse: open");
                } else if (level == HOOK_PULSE_CLOSED && last_level == HOOK_PULSE_OPEN) {
                    /* Pulse closed: loop restored */
                    last_level    = HOOK_PULSE_CLOSED;
                    last_close_us = esp_timer_get_time();
                    int64_t open_dur_ms = (last_close_us - pulse_open_us) / 1000;

                    if (in_pulse && open_dur_ms < HANGUP_THRESHOLD_MS) {
                        /* Short open: count as a rotary pulse */
                        pulse_count++;
                        in_pulse = false;
                        ESP_LOGD(TAG, "pulse %d (open %"PRId64"ms)", pulse_count, open_dur_ms);
                    } else if (open_dur_ms >= HANGUP_THRESHOLD_MS) {
                        /* Was open too long: treat as hangup */
                        ESP_LOGI(TAG, "on-hook (hangup) after %"PRId64"ms open", open_dur_ms);
                        pulse_count   = 0;
                        in_pulse      = false;
                        s_offhook     = false;
                        audio_stop();
                        audio_pa_set(false);
                        report_offhook(false);
                    }
                }
                /* Don't fall through to the standard debounce below */
                goto poll_sleep;
            }
#endif
            /* Standard 30 ms debounce for on-hook/off-hook transitions */
            vTaskDelay(pdMS_TO_TICKS(DEBOUNCE_MS));
            int level = gpio_get_level(hook_gpio);
            if (level != last_level) {
                last_level = level;
                if (level == HOOK_OFFHOOK_LEVEL) {
                    /* Off-hook: handset picked up. */
                    s_offhook = true;
#if CONFIG_PLIP_DIAL_PULSE
                    pulse_count   = 0;
                    in_pulse      = false;
                    last_close_us = esp_timer_get_time();
#endif
                    ESP_LOGI(TAG, "off-hook (pickup) detected — PA on, SLIC offhook=%d",
                             slic_is_offhook());
                    audio_pa_set(true);
                    phone_ring_stop();
                    report_offhook(true);
                } else {
                    /* On-hook: handset hung up. */
                    s_offhook = false;
                    ESP_LOGI(TAG, "on-hook (hangup) detected — PA off, stopping audio");
                    audio_stop();
                    audio_pa_set(false);
                    report_offhook(false);
                }
            }
        }

#if CONFIG_PLIP_DIAL_PULSE
        /* Check for inter-digit gap: if we accumulated pulses and the line
         * has been at closed (off-hook) state for > MAX_GAP_MS, emit the digit. */
        if (s_offhook && pulse_count > 0 && !in_pulse && last_close_us > 0) {
            int64_t gap_ms = (esp_timer_get_time() - last_close_us) / 1000;
            if (gap_ms > CONFIG_PLIP_DIAL_PULSE_MAX_GAP_MS) {
                if (pulse_count > 10) {
                    ESP_LOGW(TAG, "bad pulse count %d, ignored", pulse_count);
                    pulse_count = 0;
                } else {
                    int digit = (pulse_count == 10) ? 0 : pulse_count;
                    ESP_LOGI(TAG, "rotary digit: %d pulses -> %d", pulse_count, digit);
                    dialer_push_digit(digit);
                    pulse_count = 0;
                }
            }
        }

        /* Also detect prolonged open (hangup) even if no more edges arrive.
         * Active-HIGH: hangup = level stays at HOOK_PULSE_OPEN (LOW) > 500 ms. */
        if (s_offhook && in_pulse && pulse_open_us > 0) {
            int64_t open_ms = (esp_timer_get_time() - pulse_open_us) / 1000;
            if (open_ms >= HANGUP_THRESHOLD_MS) {
                int level_now = gpio_get_level(hook_gpio);
                if (level_now == HOOK_PULSE_OPEN) {
                    ESP_LOGI(TAG, "on-hook (prolonged open %"PRId64"ms) detected", open_ms);
                    last_level  = HOOK_PULSE_OPEN;
                    pulse_count = 0;
                    in_pulse    = false;
                    s_offhook   = false;
                    audio_stop();
                    audio_pa_set(false);
                    report_offhook(false);
                } else {
                    /* GPIO returned to off-hook level but ISR missed it */
                    last_level    = HOOK_PULSE_CLOSED;
                    last_close_us = esp_timer_get_time();
                    if ((esp_timer_get_time() - pulse_open_us) / 1000 < HANGUP_THRESHOLD_MS) {
                        pulse_count++;
                    }
                    in_pulse = false;
                }
            }
        }

poll_sleep:
#endif
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

esp_err_t phone_init(void)
{
    BaseType_t ok = xTaskCreatePinnedToCore(phone_task, "phone",
                                             TASK_STACK, NULL,
                                             TASK_PRIO, NULL, 1);
    return (ok == pdPASS) ? ESP_OK : ESP_ERR_NO_MEM;
}

bool phone_is_offhook(void)
{
    return s_offhook;
}
