// qr_puzzle.c — camera capture + quirc decode; validates QR order via seq_validator.
// Pin map: board_pins_mediakit.h (frozen source of truth for CAM_PIN_* macros).
#include "qr_puzzle.h"
#include "seq_validator.h"
#include "board_pins_mediakit.h"
#include "esp_camera.h"
#include "quirc.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <string.h>

#define TAG "qr_puzzle"

// QVGA dimensions — must match frame_size = FRAMESIZE_QVGA in cam_cfg().
#define QR_WIDTH  320
#define QR_HEIGHT 240

static seq_validator_t s_seq;
static qr_solved_cb_t  s_cb;
static TaskHandle_t    s_task;
static volatile bool   s_run;
static qr_preview_cb_t s_preview_cb;   // optional viewfinder mirror

void qr_puzzle_set_preview_cb(qr_preview_cb_t cb) { s_preview_cb = cb; }

static camera_config_t cam_cfg(void) {
    camera_config_t c = {
        .pin_pwdn       = CAM_PIN_PWDN,
        .pin_reset      = CAM_PIN_RESET,
        .pin_xclk       = CAM_PIN_XCLK,
        .pin_sccb_sda   = CAM_PIN_SIOD,
        .pin_sccb_scl   = CAM_PIN_SIOC,
        .pin_d7         = CAM_PIN_D7,
        .pin_d6         = CAM_PIN_D6,
        .pin_d5         = CAM_PIN_D5,
        .pin_d4         = CAM_PIN_D4,
        .pin_d3         = CAM_PIN_D3,
        .pin_d2         = CAM_PIN_D2,
        .pin_d1         = CAM_PIN_D1,
        .pin_d0         = CAM_PIN_D0,
        .pin_vsync      = CAM_PIN_VSYNC,
        .pin_href       = CAM_PIN_HREF,
        .pin_pclk       = CAM_PIN_PCLK,
        .xclk_freq_hz   = 20000000,
        .ledc_timer     = LEDC_TIMER_0,
        .ledc_channel   = LEDC_CHANNEL_0,
        .pixel_format   = PIXFORMAT_GRAYSCALE,
        // QVGA: matches the on-device viewfinder canvas (320x240) so the live
        // preview keeps working. VGA was tried for QR decode but quirc still
        // found zero finder patterns (the LCD-emissive contrast is the real
        // blocker, not resolution), and it broke the viewfinder — so revert.
        .frame_size     = FRAMESIZE_QVGA,   // 320x240
        .fb_count       = 1,
        .fb_location    = CAMERA_FB_IN_PSRAM,
        .grab_mode      = CAMERA_GRAB_LATEST,
    };
    return c;
}

static void scan_task(void *arg) {
    // CONFIG_SPIRAM_USE_MALLOC=y: quirc_new's ~76 KB internal buffer lands in PSRAM via malloc.
    struct quirc *q = quirc_new();
    if (!q) {
        ESP_LOGE(TAG, "quirc_new failed (out of memory)");
        esp_camera_deinit();
        s_task = NULL;
        vTaskDelete(NULL);
        return;
    }
    // quirc is (re)sized to the camera's ACTUAL frame geometry. The OV3660
    // on this board does not always deliver exactly QVGA — adapt at runtime
    // instead of dropping every off-size frame (the old fixed 320x240 guard
    // silently starved quirc when the sensor returned another size).
    int q_w = 0, q_h = 0;

    while (s_run) {
        camera_fb_t *fb = esp_camera_fb_get();
        if (!fb) { vTaskDelay(pdMS_TO_TICKS(50)); continue; }

        if (fb->width != q_w || fb->height != q_h) {
            if (quirc_resize(q, fb->width, fb->height) < 0) {
                ESP_LOGE(TAG, "quirc_resize(%d,%d) failed", fb->width, fb->height);
                esp_camera_fb_return(fb);
                vTaskDelay(pdMS_TO_TICKS(50));
                continue;
            }
            q_w = fb->width;
            q_h = fb->height;
            ESP_LOGI(TAG, "scanning at sensor geometry %dx%d", q_w, q_h);
        }

        if (s_preview_cb) s_preview_cb(fb->buf, q_w, q_h);

        uint8_t *img = quirc_begin(q, NULL, NULL);
        memcpy(img, fb->buf, (size_t)q_w * q_h);
        quirc_end(q);

        int n = quirc_count(q);
        // Diagnostic: a code IDENTIFIED but not decoded points at quiet-zone /
        // image-quality issues rather than framing. Throttled to ~1/sec.
        static int s_diag_ticks;
        if (n > 0 && (s_diag_ticks++ % 33) == 0) {
            ESP_LOGI(TAG, "quirc identified %d candidate(s)", n);
        }
        for (int i = 0; i < n; i++) {
            // static: ~12.5 KB total — too large for the task stack; single-instance task.
            static struct quirc_code code;
            static struct quirc_data data;
            quirc_extract(q, i, &code);
            if (quirc_decode(&code, &data) == 0) {
                // data.payload is a NUL-terminated uint8_t string for text QRs.
                ESP_LOGI(TAG, "QR decoded: %s", (const char *)data.payload);
                if (seq_validator_feed(&s_seq, (const char *)data.payload)) {
                    if (s_cb) s_cb();
                    s_run = false;
                }
            }
        }

        esp_camera_fb_return(fb); // always return the frame
        vTaskDelay(pdMS_TO_TICKS(30));
    }

    quirc_destroy(q);
    esp_camera_deinit();   // free PSRAM + power down sensor
    s_task = NULL;
    vTaskDelete(NULL);
}

esp_err_t qr_puzzle_start(const char *const *expected, size_t count, qr_solved_cb_t cb) {
    if (s_task) {
        ESP_LOGW(TAG, "busy (running or still stopping)");
        return ESP_ERR_INVALID_STATE;
    }

    camera_config_t cfg = cam_cfg();
    esp_err_t e = esp_camera_init(&cfg);
    if (e != ESP_OK) {
        ESP_LOGE(TAG, "camera init failed: 0x%x (%s)", e, esp_err_to_name(e));
        return e;
    }

    // Tune the OV3660 for QR decoding off a backlit LCD: max contrast +
    // sharpness, and cap the gain/exposure so the bright screen does not
    // bloom and wash out the QR modules.
    sensor_t *s = esp_camera_sensor_get();
    if (s) {
        if (s->set_contrast)   s->set_contrast(s, 2);
        if (s->set_sharpness)  s->set_sharpness(s, 2);
        if (s->set_gainceiling) s->set_gainceiling(s, GAINCEILING_2X);
        if (s->set_whitebal)   s->set_whitebal(s, 1);
        if (s->set_aec2)       s->set_aec2(s, 1);
        // The OV3660 mounts mirrored on this board: un-mirror so the captured
        // image matches reality (the viewfinder showed a mirror image, and a
        // flipped frame defeats QR finder-pattern detection).
        if (s->set_hmirror)    s->set_hmirror(s, 1);
        if (s->set_vflip)      s->set_vflip(s, 1);
    }

    seq_validator_init(&s_seq, expected, count);
    s_cb  = cb;
    s_run = true;

    if (xTaskCreate(scan_task, "qr_scan", 8192, NULL, 5, &s_task) != pdPASS) {
        ESP_LOGE(TAG, "xTaskCreate failed");
        esp_camera_deinit();
        s_task = NULL;
        s_run  = false;
        return ESP_ERR_NO_MEM;
    }

    return ESP_OK;
}

void qr_puzzle_stop(void) {
    if (!s_task) return; // no-op when idle
    s_run = false;
}

bool qr_puzzle_is_running(void) { return s_task != NULL; }
