// p5_morse.c — P5 Morse / telegraph-key puzzle driver.
//
// All real implementation is inside #if CONFIG_ZACUS_P5_MORSE_ENABLE so the
// object compiles to pure stubs when the flag is off (default). No #ifdef
// leaks into callers — they include p5_morse.h and call unconditionally.
//
// Logic adapted from puzzles/p5_morse/main/main.c (standalone ESP-NOW
// firmware), repackaged as a library with an arm/disarm API and a solved
// callback instead of the espnow_slave_notify_solved path.

#include "p5_morse.h"

#include "sdkconfig.h"
#include "esp_log.h"
#include "esp_err.h"

static const char *TAG = "p5_morse";

#if CONFIG_ZACUS_P5_MORSE_ENABLE

#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <string.h>
#include <stdbool.h>

// ─── Morse table A-Z ────────────────────────────────────────────────────────

static const char *kMorseTable[26] = {
    ".-",    "-...",  "-.-.",  "-..",   ".",     "..-.",  "--.",
    "....",  "..",    ".---",  "-.-",   ".-..",  "--",    "-.",
    "---",   ".--.",  "--.-",  ".-.",   "...",   "-",     "..-",
    "...-",  ".--",   "-..-",  "-.--",  "--.."
};

// ─── State ──────────────────────────────────────────────────────────────────

// Expected word (decoded as letters, uppercase, e.g. "ZACUS").
static char s_expected[32]       = {0};
// Accumulator for letters decoded so far.
static char s_received[32]       = {0};
static uint8_t s_recv_pos        = 0;
// Current symbol dot/dash buffer.
static char s_symbol[16]         = {0};
static uint8_t s_sym_pos         = 0;

static volatile bool s_armed     = false;
static volatile bool s_solved    = false;

static p5_morse_solved_cb_t s_solved_cb = NULL;

static bool s_initialised = false;

static TaskHandle_t s_task_handle = NULL;

// ─── Helpers ────────────────────────────────────────────────────────────────

static uint32_t now_ms(void)
{
    return (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
}

static bool key_pressed(void)
{
#if CONFIG_ZACUS_P5_ACTIVE_LOW
    return (gpio_get_level(CONFIG_ZACUS_P5_GPIO) == 0);
#else
    return (gpio_get_level(CONFIG_ZACUS_P5_GPIO) != 0);
#endif
}

// Decode accumulated symbol (dot/dash string) to a letter and append it.
static void decode_symbol(void)
{
    if (s_sym_pos == 0) return;
    s_symbol[s_sym_pos] = '\0';

    for (int i = 0; i < 26; i++) {
        if (strcmp(kMorseTable[i], s_symbol) == 0) {
            char letter = (char)('A' + i);
            if (s_recv_pos < (uint8_t)(sizeof(s_received) - 1)) {
                s_received[s_recv_pos++] = letter;
                s_received[s_recv_pos]   = '\0';
            }
            ESP_LOGD(TAG, "decoded: %s → %c (so far: %s)",
                     s_symbol, letter, s_received);
            break;
        }
    }
    s_sym_pos = 0;
    memset(s_symbol, 0, sizeof(s_symbol));
}

// Check whether the received word matches; invoke callback on match.
static void check_word(void)
{
    if (strcmp(s_received, s_expected) == 0) {
        s_solved = true;
        ESP_LOGI(TAG, "SOLVED: \"%s\" decoded correctly", s_expected);
        if (s_solved_cb) {
            s_solved_cb();
        }
    } else {
        ESP_LOGW(TAG, "wrong word: got \"%s\", expected \"%s\" — resetting",
                 s_received, s_expected);
        s_recv_pos = 0;
        memset(s_received, 0, sizeof(s_received));
    }
}

// ─── Morse decoding task ─────────────────────────────────────────────────────

static void morse_task(void *arg)
{
    (void)arg;

    uint32_t press_start  = 0;
    uint32_t last_release = 0;
    bool     key_down     = false;

    for (;;) {
        if (!s_armed || s_solved) {
            // Idle: yield and wait for the next arm cycle.
            vTaskDelay(pdMS_TO_TICKS(50));
            continue;
        }

        uint32_t t       = now_ms();
        bool     pressed = key_pressed();

        if (pressed && !key_down) {
            // Key press start.
            key_down    = true;
            press_start = t;

        } else if (!pressed && key_down) {
            // Key release — classify as dot or dash.
            key_down = false;
            uint32_t duration = t - press_start;
            last_release = t;

            if (duration <= (uint32_t)CONFIG_ZACUS_P5_DOT_MS) {
                if (s_sym_pos < (uint8_t)(sizeof(s_symbol) - 1))
                    s_symbol[s_sym_pos++] = '.';
                ESP_LOGD(TAG, "dot (%lu ms)", (unsigned long)duration);
            } else if (duration >= (uint32_t)CONFIG_ZACUS_P5_DASH_MS) {
                if (s_sym_pos < (uint8_t)(sizeof(s_symbol) - 1))
                    s_symbol[s_sym_pos++] = '-';
                ESP_LOGD(TAG, "dash (%lu ms)", (unsigned long)duration);
            } else {
                ESP_LOGD(TAG, "ignored press %lu ms (between dot and dash thresholds)",
                         (unsigned long)duration);
            }

        } else if (!key_down && last_release > 0) {
            uint32_t silence = t - last_release;

            // Letter gap: decode accumulated symbol.
            if (silence > (uint32_t)CONFIG_ZACUS_P5_GAP_MS && s_sym_pos > 0) {
                decode_symbol();
                last_release = t;
            }
            // Word gap (2x letter gap): check assembled word.
            if (silence > (uint32_t)(CONFIG_ZACUS_P5_GAP_MS * 2u) && s_recv_pos > 0) {
                check_word();
                last_release = 0;
            }
        }

        vTaskDelay(pdMS_TO_TICKS(10));   // 100 Hz sampling
    }
}

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

esp_err_t p5_morse_init(void)
{
    if (s_initialised) return ESP_OK;

    gpio_config_t cfg = {
        .pin_bit_mask = (1ULL << CONFIG_ZACUS_P5_GPIO),
        .mode         = GPIO_MODE_INPUT,
        .pull_up_en   = GPIO_PULLUP_ENABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type    = GPIO_INTR_DISABLE,
    };
    esp_err_t err = gpio_config(&cfg);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "gpio_config(GPIO %d): %s",
                 CONFIG_ZACUS_P5_GPIO, esp_err_to_name(err));
        return err;
    }

    BaseType_t rc = xTaskCreate(morse_task, "p5_morse", 3072, NULL, 5,
                                &s_task_handle);
    if (rc != pdPASS) {
        ESP_LOGE(TAG, "xTaskCreate(p5_morse) failed");
        return ESP_ERR_NO_MEM;
    }

    s_initialised = true;
    ESP_LOGI(TAG, "init OK (GPIO %d, dot<=%d ms, dash>=%d ms, gap>%d ms)",
             CONFIG_ZACUS_P5_GPIO,
             CONFIG_ZACUS_P5_DOT_MS,
             CONFIG_ZACUS_P5_DASH_MS,
             CONFIG_ZACUS_P5_GAP_MS);
    return ESP_OK;
}

void p5_morse_arm(const char *expected, p5_morse_solved_cb_t solved_cb)
{
    if (!s_initialised) {
        ESP_LOGW(TAG, "arm called before init — ignored");
        return;
    }
    // Reset state.
    s_armed    = false;   // pause task briefly while we reset
    s_solved   = false;
    s_recv_pos = 0;
    s_sym_pos  = 0;
    memset(s_received, 0, sizeof(s_received));
    memset(s_symbol,   0, sizeof(s_symbol));

    if (expected && expected[0]) {
        strncpy(s_expected, expected, sizeof(s_expected) - 1);
        s_expected[sizeof(s_expected) - 1] = '\0';
    } else {
        s_expected[0] = '\0';
    }
    s_solved_cb = solved_cb;
    s_armed     = true;
    ESP_LOGI(TAG, "armed, expected \"%s\"", s_expected);
}

void p5_morse_disarm(void)
{
    s_armed  = false;
    s_solved = false;
    s_recv_pos = 0;
    s_sym_pos  = 0;
    memset(s_received, 0, sizeof(s_received));
    memset(s_symbol,   0, sizeof(s_symbol));
    ESP_LOGI(TAG, "disarmed");
}

#else // CONFIG_ZACUS_P5_MORSE_ENABLE not set — emit stubs only

esp_err_t p5_morse_init(void)
{
    return ESP_OK;
}

void p5_morse_arm(const char *expected, p5_morse_solved_cb_t solved_cb)
{
    (void)expected;
    (void)solved_cb;
    /* stub — CONFIG_ZACUS_P5_MORSE_ENABLE=n */
}

void p5_morse_disarm(void)
{
    /* stub — CONFIG_ZACUS_P5_MORSE_ENABLE=n */
}

#endif // CONFIG_ZACUS_P5_MORSE_ENABLE
