// sound_puzzle.c — consumes MIC_P1_SOUND frames, estimates note, validates melody.
//
// Pitch estimation: zero-crossing rate -> fundamental -> MIDI note number.
// Known limit: ZCR conflates harmonics with the fundamental; accuracy is
// acceptable for well-separated notes in a quiet room. Future upgrade path:
// autocorrelation or esp-dsp FFT for more reliable fundamental detection.
#include "sound_puzzle.h"
#include "melody_validator.h"
#include "mic_broker.h"
#include "esp_log.h"
#include <math.h>
#include <stdint.h>
#include <stddef.h>

static const char *TAG = "sound_puzzle";

static melody_validator_t s_mel;
static sound_solved_cb_t  s_cb;
static int s_last_note = -1000;

// Crude pitch: zero-crossing rate -> fundamental -> MIDI. Replaced by an
// autocorrelation estimator if accuracy is insufficient (documented limit).
static int frame_to_midi(const int16_t *pcm, size_t n, int sr) {
    size_t zc = 0;
    for (size_t i = 1; i < n; i++) if ((pcm[i-1] < 0) != (pcm[i] < 0)) zc++;
    if (zc < 2) return -1000;                        // silence
    float freq = (float)zc * sr / (2.0f * n);
    if (freq < 80.0f || freq > 2000.0f) return -1000;
    return (int)lroundf(69.0f + 12.0f * log2f(freq / 440.0f));
}

static void on_frame(const int16_t *pcm, size_t n, void *ctx) {
    int note = frame_to_midi(pcm, n, 16000);
    if (note <= -1000) { s_last_note = -1000; return; }
    if (note == s_last_note) return;                 // debounce sustained note
    s_last_note = note;
    const size_t before = s_mel.pos;
    if (melody_validator_feed(&s_mel, note)) {
        ESP_LOGI(TAG, "note=%d -> melodie complete (%u notes)",
                 note, (unsigned) s_mel.count);
        mic_broker_set_mode(MIC_IDLE);   // self-stop: safe volatile mode write from broker task
        if (s_cb) s_cb();
    } else {
        // Trace bout-en-bout du test physique : note entendue + progression
        // (un retour a 0/N signale un reset du validateur sur fausse note).
        ESP_LOGI(TAG, "note=%d progression=%u/%u%s", note,
                 (unsigned) s_mel.pos, (unsigned) s_mel.count,
                 (s_mel.pos > before) ? "" : " (reset)");
    }
}

void sound_puzzle_start(const int *expected, size_t count, int tol, sound_solved_cb_t cb) {
    melody_validator_init(&s_mel, expected, count, tol);
    s_cb = cb; s_last_note = -1000;
    mic_broker_register(MIC_P1_SOUND, on_frame, NULL);
    mic_broker_set_mode(MIC_P1_SOUND);
}
void sound_puzzle_stop(void) { mic_broker_set_mode(MIC_IDLE); }

bool sound_puzzle_is_running(void) { return mic_broker_mode() == MIC_P1_SOUND; }
