// audio.c - WAV loading (with ffmpeg fallback), bandpass filtering, playback
#include "audio.h"
#include "fft.h"
#include "platform.h"

#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <complex.h>
#include <stdio.h>

static bool ConvertToFFmpegWAV(const char* inputPath, char* outputPath, size_t outputSize)
{
    snprintf(outputPath, outputSize, "%s/rspektrum_temp_converted.wav",
             Platform_GetTempDir());

    /* Build argv array — no shell = no injection risk */
    const char* argv[] = {
        "ffmpeg", "-y", "-loglevel", "quiet",
        "-i", inputPath,
        "-ar", "48000", "-ac", "1", "-f", "wav",
        outputPath, NULL
    };

    PlatformSpawnHandle handle = { 0 };
    PlatformError rc = Platform_SpawnChild("ffmpeg", argv, &handle);
    if (rc != PLATFORM_OK) {
        TraceLog(LOG_WARNING, "Failed to spawn ffmpeg: %s", Platform_GetLastErrorMessage());
        return false;
    }

    Platform_WaitForChild(&handle);

    int exit_code = 0;
    SpawnStatus status = Platform_GetExitStatus(&handle, &exit_code);
    if (status == SPAWN_EXITED && exit_code == 0 && FileExists(outputPath)) {
        TraceLog(LOG_INFO, "FFmpeg conversion successful: %s", outputPath);
        return true;
    }

    TraceLog(LOG_WARNING, "FFmpeg conversion failed (exit code %d)", exit_code);
    return false;
}

bool LoadWavFile(const char* filepath, AudioSignal* signal)
{
    const char* ext = GetFileExtension(filepath);
    char convertedPath[512] = { 0 };
    bool isConverted = false;
    
    // Check if we need to convert via ffmpeg
    bool isWav = ext && (strcmp(ext, ".wav") == 0 || strcmp(ext, ".WAV") == 0);
    
    if (!isWav) {
        // Try ffmpeg conversion
        if (ConvertToFFmpegWAV(filepath, convertedPath, sizeof(convertedPath))) {
            filepath = convertedPath;
            isConverted = true;
        } else {
            TraceLog(LOG_ERROR, "Unsupported format and ffmpeg not available: %s", filepath);
            return false;
        }
    }
    
    Wave wave = LoadWave(filepath);
    if (wave.data == NULL) {
        TraceLog(LOG_ERROR, "Failed to open WAV file: %s", filepath);
        if (isConverted) FileRemove(convertedPath);
        return false;
    }
    
    signal->sampleRate = wave.sampleRate;
    signal->channels = wave.channels;
    signal->numSamples = wave.frameCount * wave.channels;
    signal->duration = (float)wave.frameCount / wave.sampleRate;
    if (signal->samples) free(signal->samples);  // free previous file's samples
    signal->samples = (float*)malloc(signal->numSamples * sizeof(float));
    
    if (wave.sampleSize == 16) {
        short* samples = (short*)wave.data;
        for (int i = 0; i < signal->numSamples; i++) signal->samples[i] = samples[i] / 32768.0f;
    } else if (wave.sampleSize == 32) {
        float* samples = (float*)wave.data;
        memcpy(signal->samples, samples, signal->numSamples * sizeof(float));
    } else {
        unsigned char* samples = (unsigned char*)wave.data;
        for (int i = 0; i < signal->numSamples; i++) signal->samples[i] = (samples[i] - 128) / 128.0f;
    }
    
    if (wave.channels > 1) {
        int monoSamples = wave.frameCount;
        for (int i = 0; i < monoSamples; i++) {
            float sum = 0.0f;
            for (int c = 0; c < wave.channels; c++) sum += signal->samples[i * wave.channels + c];
            signal->samples[i] = sum / wave.channels;
        }
        signal->numSamples = monoSamples;
    }
    UnloadWave(wave);
    
    // Clean up temp file if converted
    if (isConverted) {
        FileRemove(convertedPath);
        TraceLog(LOG_INFO, "Cleaned up temp file: %s", convertedPath);
    }
    
    TraceLog(LOG_INFO, "Loaded WAV: %d Hz, %.2f sec, %d samples", signal->sampleRate, signal->duration, signal->numSamples);
    return true;
}

void FreeSignal(AudioSignal* signal)
{
    if (signal->samples) { free(signal->samples); signal->samples = NULL; }
    signal->numSamples = 0; signal->sampleRate = 0; signal->duration = 0.0f;
}

// ===== Playback with FFT bandpass filter =====
static void ApplyBandpassFilterFFT(float* samples, int numSamples, int sampleRate, float freqLow, float freqHigh)
{
    if (freqLow <= 0 && freqHigh >= sampleRate / 2.0f) return;

    int fftSize = 1;
    while (fftSize < numSamples) fftSize *= 2;
    fftSize *= 2;

    float* paddedSamples = (float*)calloc(fftSize, sizeof(float));
    float complex *fftInput = (float complex*)malloc(fftSize * sizeof(float complex));
    float complex *fftOutput = (float complex*)malloc(fftSize * sizeof(float complex));

    // Copy samples directly without windowing (windowing causes fade in/out)
    for (int i = 0; i < numSamples; i++) {
        paddedSamples[i] = samples[i];
    }
    for (int i = 0; i < fftSize; i++) fftInput[i] = paddedSamples[i] + 0.0f * I;
    
    FFT(fftInput, fftOutput, fftSize, false);
    
    float freqPerBin = (float)sampleRate / fftSize;

    // Transition (skirt) width in Hz: ~20% of the passband, capped at 100 Hz,
    // with a floor of 3 FFT bins so it stays smooth at coarse resolution.
    // Previously the skirt was tied to bin width alone (freqPerBin * 10), so the
    // FFT size — and thus the filter's sharpness — changed with the selection
    // length: the same band sounded different depending on how much you boxed.
    float bandwidthHz = freqHigh - freqLow;
    float transitionHz = fmaxf(fminf(bandwidthHz * 0.2f, 100.0f), freqPerBin * 3.0f);

    for (int bin = 0; bin < fftSize / 2 + 1; bin++) {
        float frequency = bin * freqPerBin;
        float attenuation = 1.0f;
        if (frequency < freqLow) {
            float dist = (freqLow - frequency) / transitionHz;
            attenuation = 1.0f / (1.0f + dist * dist * dist);
        } else if (frequency > freqHigh) {
            float dist = (frequency - freqHigh) / transitionHz;
            attenuation = 1.0f / (1.0f + dist * dist * dist);
        }
        fftOutput[bin] *= attenuation;
        if (bin > 0 && bin < fftSize / 2) fftOutput[fftSize - bin] *= attenuation;
    }
    
    float complex* ifftOutput = (float complex*)malloc(fftSize * sizeof(float complex));
    FFT(fftOutput, ifftOutput, fftSize, true);
    
    float filteredPeak = 0.0f;
    for (int i = 0; i < numSamples; i++) {
        samples[i] = crealf(ifftOutput[i]);
        if (fabsf(samples[i]) > filteredPeak) filteredPeak = fabsf(samples[i]);
    }
    
    const float TARGET_PEAK = 0.9f;
    if (filteredPeak > 0.0001f) {
        float gain = TARGET_PEAK / filteredPeak;
        if (gain > 10.0f) gain = 10.0f;
        for (int i = 0; i < numSamples; i++) {
            samples[i] *= gain;
            if (samples[i] > 0.95f) samples[i] = 0.95f;
            if (samples[i] < -0.95f) samples[i] = -0.95f;
        }
    }
    
    free(paddedSamples); free(fftInput); free(fftOutput); free(ifftOutput);
}

static void ApplyBandpassFilter(float* samples, int numSamples, int sampleRate, float freqLow, float freqHigh)
{
    ApplyBandpassFilterFFT(samples, numSamples, sampleRate, freqLow, freqHigh);
}

// Build the current selection's audio: the samples in the selected time span,
// band-limited to the selected frequency box (same processing as playback, so
// "what you hear is what you save"). Returns a malloc'd mono float buffer the
// caller must free, with *outNumSamples set; NULL if there's no valid region.
static float* BuildSelectionAudio(int* outNumSamples)
{
    if (!app.loaded || !app.stftComputed) return NULL;

    // playFromT lets playback begin partway into the selection (the user
    // scrubbed the playhead) without disturbing the selection itself — the
    // whole point is to replay a subrange while keeping the region intact.
    float t0 = app.sel.timeStart;
    float t1 = app.sel.timeEnd;
    if (app.playFromT > 0.0f && app.playFromT < 1.0f)
        t0 = t0 + app.playFromT * (t1 - t0);

    int startSample = (int)(t0 * app.signal.numSamples);
    int endSample = (int)(t1 * app.signal.numSamples);
    int numSamples = endSample - startSample;
    if (numSamples <= 0 || startSample < 0 || endSample > app.signal.numSamples) return NULL;

    float* regionSamples = (float*)malloc(numSamples * sizeof(float));
    if (!regionSamples) return NULL;
    memcpy(regionSamples, app.signal.samples + startSample, numSamples * sizeof(float));

    // sel.freq* are fractions of the *displayed* axis (capped at
    // EffectiveMaxFreqHz), not of true Nyquist — same convention the PNG export
    // in ui.c uses. Convert through EffectiveMaxFreqHz so a display crop doesn't
    // scale the passband up by 1/DisplayFreqFraction(). The filter itself still
    // works in true-Nyquist terms, which is what nyquist is for.
    float nyquist = (float)app.signal.sampleRate / 2.0f;
    float maxFreq = EffectiveMaxFreqHz();
    float freqLow = app.sel.freqStart * maxFreq;
    float freqHigh = app.sel.freqEnd * maxFreq;
    if (freqLow > 10.0f || freqHigh < nyquist - 10.0f) {
        TraceLog(LOG_INFO, "Applying bandpass filter: %.0f - %.0f Hz", freqLow, freqHigh);
        ApplyBandpassFilter(regionSamples, numSamples, app.signal.sampleRate, freqLow, freqHigh);
    }

    *outNumSamples = numSamples;
    return regionSamples;
}

void EnsureAudioDevice(void)
{
    if (!IsAudioDeviceReady()) InitAudioDevice();
}

void ReleaseAudioDevice(void)
{
    if (!IsAudioDeviceReady()) return;
    if (AudioPlaybackSound.frameCount != 0) {
        UnloadSound(AudioPlaybackSound);
        AudioPlaybackSound = (Sound){ 0 };
    }
    CloseAudioDevice();
}

void PlaySelectedRegion(void)
{
    int numSamples = 0;
    float* regionSamples = BuildSelectionAudio(&numSamples);
    if (!regionSamples) return;

    // Snapshot what we're about to play so the playhead tracks THIS region even
    // if the user moves the selection mid-playback. Duration comes from the
    // buffer we actually built, not from app.signal.duration.
    // Snapshot the span actually handed to the device, which is the selection
    // narrowed by any scrub offset — the playhead is measured against this.
    app.playSelStart = app.sel.timeStart;
    app.playSelEnd   = app.sel.timeEnd;
    if (app.playFromT > 0.0f && app.playFromT < 1.0f)
        app.playSelStart += app.playFromT * (app.sel.timeEnd - app.sel.timeStart);
    app.playDuration = (app.signal.sampleRate > 0)
                     ? (float)numSamples / (float)app.signal.sampleRate : 0.0f;
    // Anchor the playhead to the wall clock (see playStartTime). Set here, at
    // the moment the buffer is handed to the device, so the marker can't drift
    // from the audio no matter how long a frame takes.
    app.playStartTime = GetTime();
    app.playheadSeekT = 0.0f;
    app.playheadT     = 0.0f;

    EnsureAudioDevice();  // opened on demand; released again once playback ends
    if (AudioPlaybackSound.frameCount != 0) UnloadSound(AudioPlaybackSound);

    Wave wave = { .data = regionSamples, .frameCount = (unsigned int)numSamples,
                  .sampleRate = (unsigned int)app.signal.sampleRate, .sampleSize = 32, .channels = 1 };
    AudioPlaybackSound = LoadSoundFromWave(wave);  // copies the samples
    PlaySound(AudioPlaybackSound);
    free(regionSamples);
}

void ExportSelectionWAV(const char* dirPath)
{
    int numSamples = 0;
    float* samples = BuildSelectionAudio(&numSamples);
    if (!samples) {
        snprintf(app.exportMessage, sizeof(app.exportMessage), "No region to export");
        app.exportMessageTimer = 3.0f;
        return;
    }

    float maxFreq = EffectiveMaxFreqHz();  // sel.freq* are display-axis fractions
    float t0 = app.sel.timeStart * app.signal.duration;
    float t1 = app.sel.timeEnd * app.signal.duration;
    float f0 = app.sel.freqStart * maxFreq;
    float f1 = app.sel.freqEnd * maxFreq;

    char path[4608];
    snprintf(path, sizeof(path), "%s/rspektrum_sel_%.0f-%.0fs_%.0f-%.0fHz.wav",
             dirPath, t0, t1, f0, f1);

    Wave wave = { .data = samples, .frameCount = (unsigned int)numSamples,
                  .sampleRate = (unsigned int)app.signal.sampleRate, .sampleSize = 32, .channels = 1 };
    if (ExportWave(wave, path)) {
        Platform_OfferFileToUser(path);  // desktop: no-op; web: browser download
        snprintf(app.exportMessage, sizeof(app.exportMessage), "Saved WAV: %.40s", GetFileName(path));
    } else
        snprintf(app.exportMessage, sizeof(app.exportMessage), "WAV export failed");
    app.exportMessageTimer = 3.0f;

    free(samples);
}

