// stimulus.c — QR display + melody playback, driven over REST.
#include "stimulus.h"

#include <math.h>
#include <string.h>

#include "bsp/esp-box-3.h"
#include "lvgl.h"
#include "libs/qrcode/lv_qrcode.h"
#include "cJSON.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

static const char *TAG = "stimulus";
#define LOCK_MS 1000

static lv_obj_t *s_qr_screen;   // dedicated fullscreen QR screen
static lv_obj_t *s_qr;          // lv_qrcode widget
static lv_obj_t *s_qr_caption;

esp_err_t stimulus_init(void) {
    if (!bsp_display_lock(LOCK_MS)) return ESP_FAIL;

    s_qr_screen = lv_obj_create(NULL);
    lv_obj_set_style_bg_color(s_qr_screen, lv_color_white(), 0);

    // 160 px QR on the 320x240 LCD — leaves ~40 px white top/bottom and
    // ~80 px sides as the quiet zone quirc needs to lock the finder patterns
    // (220 px starved the vertical quiet zone and blocked detection).
    s_qr = lv_qrcode_create(s_qr_screen);
    lv_qrcode_set_size(s_qr, 160);
    lv_qrcode_set_dark_color(s_qr, lv_color_black());
    lv_qrcode_set_light_color(s_qr, lv_color_white());
    lv_qrcode_update(s_qr, "zacus", 5);
    lv_obj_align(s_qr, LV_ALIGN_CENTER, 0, -10);

    s_qr_caption = lv_label_create(s_qr_screen);
    lv_obj_set_style_text_color(s_qr_caption, lv_color_black(), 0);
    lv_label_set_text(s_qr_caption, "");
    lv_obj_align(s_qr_caption, LV_ALIGN_BOTTOM_MID, 0, -10);

    bsp_display_unlock();
    ESP_LOGI(TAG, "stimulus QR screen ready");
    return ESP_OK;
}

static void show_qr(const char *text) {
    if (!s_qr || !bsp_display_lock(LOCK_MS)) return;
    lv_qrcode_update(s_qr, text, strlen(text));
    lv_label_set_text(s_qr_caption, text);
    lv_screen_load(s_qr_screen);     // bring the QR to the front
    bsp_display_unlock();
    // Dim the backlight hard: an emissive LCD at full brightness blooms and
    // crushes QR contrast for the master's camera. ~35% keeps the modules
    // readable while killing the glare halo.
    bsp_display_brightness_set(35);
    ESP_LOGI(TAG, "QR shown (dimmed): %s", text);
}

// MIDI note -> frequency (A4=69=440 Hz).
static float midi_to_hz(int note) {
    return 440.0f * powf(2.0f, (float)(note - 69) / 12.0f);
}

// ─── REST ────────────────────────────────────────────────────────────────────

static esp_err_t read_body(httpd_req_t *req, char *buf, size_t cap) {
    if (req->content_len <= 0 || (size_t)req->content_len >= cap) return ESP_FAIL;
    int total = 0;
    while (total < (int)req->content_len) {
        int got = httpd_req_recv(req, buf + total, req->content_len - total);
        if (got <= 0) {
            if (got == HTTPD_SOCK_ERR_TIMEOUT) continue;
            return ESP_FAIL;
        }
        total += got;
    }
    buf[total] = '\0';
    return ESP_OK;
}

static esp_err_t reply(httpd_req_t *req, const char *status, const char *json) {
    httpd_resp_set_status(req, status);
    httpd_resp_set_type(req, "application/json");
    return httpd_resp_sendstr(req, json);
}

static esp_err_t handle_qr_post(httpd_req_t *req) {
    char body[256];
    if (read_body(req, body, sizeof(body)) != ESP_OK) {
        return reply(req, "400 Bad Request", "{\"error\":\"body\"}");
    }
    cJSON *root = cJSON_Parse(body);
    const cJSON *t = root ? cJSON_GetObjectItemCaseSensitive(root, "text") : NULL;
    if (!cJSON_IsString(t) || !t->valuestring[0]) {
        cJSON_Delete(root);
        return reply(req, "400 Bad Request", "{\"error\":\"text required\"}");
    }
    show_qr(t->valuestring);
    char resp[280];
    snprintf(resp, sizeof(resp), "{\"status\":\"ok\",\"text\":\"%s\"}",
             t->valuestring);
    cJSON_Delete(root);
    return reply(req, "200 OK", resp);
}

// Melody played on a worker task so the HTTP response returns immediately.
typedef struct { float hz[32]; int count; int note_ms; } melody_job_t;

static void melody_task(void *arg) {
    melody_job_t *job = (melody_job_t *)arg;
    for (int i = 0; i < job->count; i++) {
        audio_play_tone(job->hz[i], job->note_ms);
        vTaskDelay(pdMS_TO_TICKS(40));   // brief gap = note onset for the mic
    }
    free(job);
    vTaskDelete(NULL);
}

static esp_err_t handle_melody_post(httpd_req_t *req) {
    char body[512];
    if (read_body(req, body, sizeof(body)) != ESP_OK) {
        return reply(req, "400 Bad Request", "{\"error\":\"body\"}");
    }
    cJSON *root = cJSON_Parse(body);
    const cJSON *notes = root ? cJSON_GetObjectItemCaseSensitive(root, "notes") : NULL;
    if (!cJSON_IsArray(notes) || cJSON_GetArraySize(notes) == 0) {
        cJSON_Delete(root);
        return reply(req, "400 Bad Request", "{\"error\":\"notes required\"}");
    }
    const cJSON *ms = cJSON_GetObjectItemCaseSensitive(root, "ms");
    const int note_ms = cJSON_IsNumber(ms) ? ms->valueint : 400;

    melody_job_t *job = calloc(1, sizeof(*job));
    if (!job) { cJSON_Delete(root); return reply(req, "500 Internal Server Error", "{\"error\":\"oom\"}"); }
    job->note_ms = (note_ms > 0 && note_ms <= 4000) ? note_ms : 400;
    const cJSON *n;
    cJSON_ArrayForEach(n, notes) {
        if (job->count >= 32) break;
        if (cJSON_IsNumber(n) && n->valueint >= 0 && n->valueint <= 127) {
            job->hz[job->count++] = midi_to_hz(n->valueint);
        }
    }
    cJSON_Delete(root);
    if (job->count == 0) { free(job); return reply(req, "400 Bad Request", "{\"error\":\"no valid notes\"}"); }

    if (xTaskCreate(melody_task, "melody", 4096, job, 5, NULL) != pdPASS) {
        free(job);
        return reply(req, "503 Service Unavailable", "{\"error\":\"busy\"}");
    }
    char resp[64];
    snprintf(resp, sizeof(resp), "{\"status\":\"ok\",\"notes\":%d}", job->count);
    return reply(req, "200 OK", resp);
}

esp_err_t stimulus_register_routes(httpd_handle_t server) {
    static const httpd_uri_t uri_qr = {
        .uri = "/stim/qr", .method = HTTP_POST,
        .handler = handle_qr_post, .user_ctx = NULL,
    };
    static const httpd_uri_t uri_melody = {
        .uri = "/stim/melody", .method = HTTP_POST,
        .handler = handle_melody_post, .user_ctx = NULL,
    };
    esp_err_t e = httpd_register_uri_handler(server, &uri_qr);
    if (e == ESP_OK) e = httpd_register_uri_handler(server, &uri_melody);
    if (e == ESP_OK) ESP_LOGI(TAG, "routes up: POST /stim/qr, POST /stim/melody");
    return e;
}
