#include "primitives.h"
#include "raylib.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>

// Bucket width for the envelope summary. 1024 samples keeps the table ~0.8%
// of the signal's own footprint (1.9 MB for a 5.7-hour capture) while still
// giving a pixel column several buckets to reduce over at typical zooms.
#define WAVE_BUCKET_SIZE 1024

void FreeWaveEnvelope(WaveEnvelope* env)
{
    free(env->buckets);
    env->buckets = NULL;
    env->bucketCount = 0;
    env->samples = NULL;
    env->numSamples = 0;
}

void BuildWaveEnvelope(WaveEnvelope* env, const float* samples, int numSamples)
{
    // Already summarises this exact buffer — nothing to do. Comparing the
    // pointer AND the length catches both a new file and a re-decode that
    // happened to land on the same address.
    if (env->buckets && env->samples == samples && env->numSamples == numSamples)
        return;

    FreeWaveEnvelope(env);
    if (!samples || numSamples <= 0) return;

    int bucketSize = WAVE_BUCKET_SIZE;
    int bucketCount = (numSamples + bucketSize - 1) / bucketSize;
    env->buckets = (WaveMinMax*)malloc((size_t)bucketCount * sizeof(WaveMinMax));
    if (!env->buckets) return;  // fall back to the raw-sample path

    for (int b = 0; b < bucketCount; b++) {
        int a = b * bucketSize;
        int e = a + bucketSize;
        if (e > numSamples) e = numSamples;
        float mn = samples[a], mx = samples[a];
        for (int k = a + 1; k < e; k++) {
            float v = samples[k];
            if (v < mn) mn = v;
            if (v > mx) mx = v;
        }
        env->buckets[b].mn = mn;
        env->buckets[b].mx = mx;
    }
    env->bucketCount = bucketCount;
    env->bucketSize = bucketSize;
    env->samples = samples;
    env->numSamples = numSamples;
}

void InitScopeView(ScopeView* view, WaveformData data, int x, int y, int width, int height)
{
    view->data = data;
    view->envelope = (WaveEnvelope){ 0 };
    view->x = x;
    view->y = y;
    view->width = width;
    view->height = height;
    view->viewStart = 0.0f;
    view->viewEnd = 1.0f;
    view->ampMin = -1.0f;
    view->ampMax = 1.0f;
    view->cursorT = -1.0f;  // -1 means no cursor
    view->showGrid = true;
    view->gridAlpha = 0.3f;
    view->bgColorR = 20;
    view->bgColorG = 20;
    view->bgColorB = 25;
    view->bgColorA = 255;
    view->gridR = 80;
    view->gridG = 80;
    view->gridB = 100;
    view->gridA = 180;
}

static int AmplitudeToY(ScopeView* view, float amp)
{
    return view->y + view->height - (int)((amp - view->ampMin) / (view->ampMax - view->ampMin) * view->height);
}

// Map a signal-space time (0-1 over the WHOLE signal) to a screen X, honoring
// the visible window. The waveform envelope below already draws only
// viewStart..viewEnd, so grid lines and the cursor have to use the same mapping
// or they drift out of register with the trace (and with the spectrogram above)
// as soon as the user zooms or pans.
static int TimeToX(ScopeView* view, float t)
{
    float span = view->viewEnd - view->viewStart;
    if (span <= 0.0f) span = 1.0f;
    return view->x + (int)((t - view->viewStart) / span * view->width);
}

void DrawScopeView(ScopeView* view, float cursorT)
{
    if (!view || !view->data.samples) return;

    int totalSamples = view->data.numSamples;
    if (totalSamples <= 0) return;

    // Draw background
    DrawRectangle(view->x, view->y, view->width, view->height,
                  (Color){ view->bgColorR, view->bgColorG, view->bgColorB, view->bgColorA });

    DrawRectangleLines(view->x, view->y, view->width, view->height,
                       Fade((Color){ view->gridR, view->gridG, view->gridB, view->gridA }, 0.5f));

    // Grid lines
    if (view->showGrid) {
        Color gridColor = (Color){ view->gridR, view->gridG, view->gridB, (int)(view->gridAlpha * 255) };

        // Vertical time divisions — ten evenly spaced lines across the VISIBLE
        // window, so the grid stays put under zoom instead of sliding off.
        for (int i = 0; i <= 10; i++) {
            float t = view->viewStart + (float)i / 10.0f * (view->viewEnd - view->viewStart);
            int x = TimeToX(view, t);
            DrawLineV((Vector2){ x, view->y }, (Vector2){ x, view->y + view->height }, gridColor);
        }

        // Horizontal amplitude divisions
        for (int i = 0; i <= 10; i++) {
            float amp = view->ampMin + (float)i / 10.0f * (view->ampMax - view->ampMin);
            int y = AmplitudeToY(view, amp);
            DrawLine(view->x, y, view->x + view->width, y, gridColor);
        }

        // Zero line
        int zeroY = AmplitudeToY(view, 0.0f);
        DrawLine(view->x, zeroY, view->x + view->width, zeroY,
                 Fade((Color){ 255, 255, 255, 150 }, 0.7f));

        // Amplitude labels
        char label[32];
        for (int i = 0; i <= 10; i++) {
            float amp = view->ampMin + (float)i / 10.0f * (view->ampMax - view->ampMin);
            int y = AmplitudeToY(view, amp);
            sprintf(label, "%.1f", amp);
            DrawText(label, view->x - 35, y - 5, 8,
                     Fade((Color){ view->gridR, view->gridG, view->gridB, view->gridA }, 0.7f));
        }
    }

    // Visible sample range
    int startSample = (int)(view->viewStart * totalSamples);
    int endSample = (int)(view->viewEnd * totalSamples);
    if (startSample < 0) startSample = 0;
    if (endSample > totalSamples) endSample = totalSamples;

    int visibleSamples = endSample - startSample;
    if (visibleSamples <= 0) return;

    // Compute samples-per-pixel so every pixel gets a column
    int spp = (visibleSamples + view->width - 1) / view->width;
    if (spp < 1) spp = 1;

    // Keep the summary current; no-op unless the sample buffer changed.
    BuildWaveEnvelope(&view->envelope, view->data.samples, totalSamples);

    // Reduce over whole buckets only when a column covers at least one, so the
    // summary is never used to answer a question finer than it can. Zoomed in
    // past a bucket the raw path runs, and is cheap there by definition.
    const WaveEnvelope* env = &view->envelope;
    bool useEnvelope = env->buckets != NULL && spp >= env->bucketSize;

    // Draw envelope: per-pixel min/max (Audacity-style)
    Color waveColor = (Color){ 200, 220, 255, 255 };
    for (int px = 0; px < view->width; px++) {
        int s0 = startSample + px * spp;
        int s1 = startSample + (px + 1) * spp;
        if (s0 >= endSample) s0 = endSample - 1;
        if (s1 > endSample) s1 = endSample;

        float minAmp, maxAmp;

        if (useEnvelope) {
            // Bucket range covering [s0, s1). Rounding inward would leave the
            // column's edges unsampled, so the span is widened to whole buckets
            // — at this zoom a bucket is at most one pixel wide anyway.
            int b0 = s0 / env->bucketSize;
            int b1 = (s1 + env->bucketSize - 1) / env->bucketSize;
            if (b0 < 0) b0 = 0;
            if (b1 > env->bucketCount) b1 = env->bucketCount;
            if (b1 <= b0) b1 = b0 + 1;
            if (b0 >= env->bucketCount) b0 = env->bucketCount - 1;

            minAmp = env->buckets[b0].mn;
            maxAmp = env->buckets[b0].mx;
            for (int b = b0 + 1; b < b1; b++) {
                if (env->buckets[b].mn < minAmp) minAmp = env->buckets[b].mn;
                if (env->buckets[b].mx > maxAmp) maxAmp = env->buckets[b].mx;
            }
        } else {
            minAmp = view->data.samples[s0];
            maxAmp = view->data.samples[s0];
            for (int s = s0 + 1; s < s1; s++) {
                float v = view->data.samples[s];
                if (v < minAmp) minAmp = v;
                if (v > maxAmp) maxAmp = v;
            }
        }

        int yTop = AmplitudeToY(view, maxAmp);
        int yBot = AmplitudeToY(view, minAmp);
        DrawLine(px + view->x, yTop, px + view->x, yBot, waveColor);
    }

    // Cursor. cursorT is signal-space; TimeToX maps it into the visible window,
    // and the bounds check below drops it when it falls outside the current view.
    if (cursorT >= 0.0f && cursorT <= 1.0f) {
        int cursorX = TimeToX(view, cursorT);
        if (cursorX >= view->x && cursorX <= view->x + view->width) {
            Color cc = (Color){ 255, 80, 80, 255 };
            DrawLine(cursorX, view->y, cursorX, view->y + view->height, cc);
            Vector2 p1 = (Vector2){ cursorX, view->y };
            Vector2 p2 = (Vector2){ cursorX - 6, view->y + 10 };
            Vector2 p3 = (Vector2){ cursorX + 6, view->y + 10 };
            DrawTriangle(p1, p2, p3, cc);
        }
    }
}
