// spectrogram_types.h - Shared types, constants, globals, and small math helpers.
// This is the "spine" header included by every module.
#ifndef SPECTROGRAM_TYPES_H
#define SPECTROGRAM_TYPES_H

#include "raylib.h"
#include "utils.h"        // AudioSignal, SignalStats
#include "primitives.h"   // ScopeView, WaveformData
#include "mlnl.h"         // MlnlAnnotations

#include <stdbool.h>
#include <math.h>

#ifndef M_PI
    #define M_PI 3.14159265358979323846
#endif

#ifndef CYAN
    #define CYAN (Color){ 0, 255, 255, 255 }
#endif

// ============================================================================
// Configuration
// ============================================================================

#define FFT_SIZE_DEFAULT 2048
#define FFT_SIZE_MAX 2048
#define FFT_SIZE_MIN 128
#define HOP_RATIO 4  // FFT_SIZE / HOP_SIZE = 4 means 75% overlap
#define MAX_SAMPLE_RATE 48000
#define LOUDNESS_FLOOR_DB -80.0f

// Base resolution for proportional UI scaling (see GetUIScale in render.c)
#define BASE_WIDTH 1280
#define BASE_HEIGHT 800

#define FFT_CACHE_SIZE 4

// ============================================================================
// Data Structures
// ============================================================================

typedef enum {
    COLORMAP_GRAYS = 0,
    COLORMAP_INFERNO,
    COLORMAP_VIRIDIS,
    COLORMAP_PLASMA,
    COLORMAP_HOT,
    COLORMAP_COOL,
    COLORMAP_COUNT
} ColormapType;

// How the colorizer maps amplitude to brightness:
//  - RELATIVE: ceiling tracks the signal peak; floor sits dynRangeDb below it.
//  - ABSOLUTE: fixed dBFS scale (0 dBFS ceiling = full scale, absolute floor).
typedef enum {
    SCALE_RELATIVE = 0,
    SCALE_ABSOLUTE
} AmplitudeScaleMode;

typedef struct {
    float frequency;
    float amplitude;
    float phase;
} FrequencyData;

typedef struct {
    FrequencyData* spectrum;
    FrequencyData* derivativeSpectrum;  // STFT with derivative window (for synchrosqueezing)
    int numBins;
    int sampleOffset;
    int sampleCount;
} StftSegment;

typedef struct {
    StftSegment* segments;
    int numSegments;
    int sampleRate;
    int totalSamples;
    bool useHannWindow;
} StftResult;

typedef struct {
    int fftSize;
    StftResult result;
    int accessOrder;  // lower = more recently accessed
} FFTCacheEntry;

typedef struct {
    FFTCacheEntry entries[FFT_CACHE_SIZE];
    int count;
    int nextOrder;
} FFTSizeCache;

// The time+frequency box selection and its drag/move interaction state.
// All coordinates are 0-1 normalized. A "box-select" drags out a new box;
// a "move" drags an existing box around.
typedef struct {
    float timeStart, timeEnd;   // selected time span
    float freqStart, freqEnd;   // selected frequency span
    bool isTimeSelecting;       // dragging out a new time span
    bool isFreqSelecting;       // dragging out a new frequency span
    Vector2 selectStartPos;     // mouse pos when a box-select began (min-drag check)
    bool isDragging;            // moving an existing selection box
    Vector2 dragStartPos;       // mouse pos when the move began
    float dragTimeStart;        // selection start time when the move began
    float dragFreqStart;        // selection freq start when the move began
} Selection;

// Two-point ruler for measuring deltas on the spectrogram (Δt, Δf, baud, drift).
// Both points are 0-1 normalized: t over the whole signal, f as a fraction of
// Nyquist. A press-drag-release drops A at press and B at release; the readout
// stays on screen until cleared, so you can re-drag to re-measure.
typedef struct {
    bool active;    // a measurement exists (drawn + read out)
    bool dragging;  // mid-drag, placing point B
    float t0, f0;   // point A
    float t1, f1;   // point B
} MarkerTool;

// The visible window into the spectrogram (time + frequency), all 0-1
// normalized, plus the range captured at the start of a pan drag.
typedef struct {
    float start, end;                 // visible time range
    float freqStart, freqEnd;         // visible freq range (0 = 0 Hz, 1 = Nyquist)
    bool isPanning;
    float panStart, panEnd;           // time range captured when the pan began
    float panFreqStart, panFreqEnd;   // freq range captured when the pan began
    Vector2 panStartPos;              // mouse pos when the pan began
} Viewport;

typedef struct {
    AudioSignal signal;
    StftResult stft;
    Image spectrogramImage;
    Texture2D spectrogramTexture;
    bool loaded;
    bool stftComputed;

    // Playback state
    float playheadT;           // 0-1 normalized position in selection
    float playheadElapsed;     // Elapsed seconds since play started

    // Time + frequency box selection and its drag/move interaction state.
    Selection sel;

    // Two-point ruler tool. markerMode swaps the LMB-drag gesture from
    // box-select to dropping markers; showSpectrum toggles the PSD slice panel.
    MarkerTool marker;
    bool markerMode;
    bool showSpectrum;

    // Export settings
    float exportScale;
    char exportDir[4096];
    char exportMessage[256];
    float exportMessageTimer;  // seconds the export message stays on screen

    // Visible viewport (time + frequency) and in-progress pan state.
    Viewport view;

    // Cached visible texture
    Texture2D visibleTexture;
    int cachedVisibleStart;
    int cachedVisibleEnd;
    int cachedVisibleStartY;
    int cachedVisibleEndY;
    bool visibleTextureValid;

    // Display settings. amplitudeFloorDb/CeilingDb are the values the colorizer
    // actually uses; they're derived from the mode + the controls below.
    float amplitudeFloorDb;
    float amplitudeCeilingDb;
    AmplitudeScaleMode amplitudeMode;
    float dynRangeDb;       // RELATIVE mode: dB of range shown below the peak
    float absoluteFloorDb;  // ABSOLUTE mode: floor in dBFS (ceiling pinned at 0)
    ColormapType colormap;
    bool showGrid;
    int fftSize;  // Current FFT size (128-2048)

    // Cached synchrosqueezed energy (the expensive reassignment result).
    // Reused across dB-floor / colormap changes — only re-colorized, not recomputed.
    float* reassignBuffer;
    int reassignWidth;
    int reassignHeight;

    // Overlays
    bool showAbout;  // About / help dialog

    // Sidebar scroll offset (px), for when controls overflow a short window
    float sidebarScroll;

    // File browser state
    bool showFileBrowser;
    char browserPath[512];
    char** browserFiles;
    bool* browserIsDir;
    int browserFileCount;
    int browserScroll;
    int browserSelected;
    bool isBrowsing;

    // Playback state
    bool isPlaying;
    bool playbackFinished;  // Track if playback completed naturally

    // Loading/processing state
    int loadingPhase;         // 0 = computing STFT, 1 = generating texture
    float loadingProgress;    // 0.0 to 1.0 overall progress
    int currentSTFTSegment;   // Which segment we're on for incremental processing

    // Adaptive resolution: skipFactor=1 means compute all segments, skipFactor=N
    // means compute every Nth segment (faster initial load, overview-only).
    // highResFinished tracks whether full-res segments have been computed for
    // the current view range.
    int skipFactor;
    bool highResFinished;

    // Background high-res computation state.
    // After the overview (skipFactor-strided) loads, missing segments are
    // filled in at full resolution in the background while the user is idle.
    int bgHighResSeg;       // next segment index to compute at high-res
    bool bgFinished;        // true when all segments are computed at high-res
    bool isBgProcessing;    // true while background task is actively computing

    // FFT size cache — LRU cache of previously computed STFT results.
    // When user switches FFT sizes, we check the cache first to avoid
    // recomputing. When cache is full, we evict the least-recently-used entry.
    FFTSizeCache fftCache;

    // Waveform scope view (underneath spectrogram viewport)
    ScopeView scopeView;
    bool showScope;       // Toggle to show/hide scope view

    // Scope view divider
    float dividerY;       // Y position of divider between spectrogram and scope (0-1 normalized)
    bool isDividing;      // True while user is dragging the divider
    Vector2 dividerStartPos;  // Mouse position when started dividing
    float dividerStartY;      // Spectro height when started dividing

    // Display-side frequency crop. Caps the displayed frequency axis at this
    // Hz value — purely a visualization concern (signal data, STFT, audio
    // playback are unaffected). 0 = no crop, use full Nyquist. The crop is
    // automatically clamped to the current signal's Nyquist by the helper
    // below, so a 3 kHz crop is harmless when loading a 2 kHz-sample file.
    // Persists across loads so a user analyzing mLink (≤3 kHz) doesn't have
    // to re-set it after every file open.
    float displayMaxFreqHz;
    // True when a fresh signal has loaded and is waiting for ApplyAutoCrop to
    // run (after the STFT exists, so the energy fallback can compute). Set by
    // ResetForNewSignal; cleared by the loadingPhase=2 hook once autocrop has
    // run, so an FFT-size change (same loadingPhase path) won't retrigger.
    bool autocropPending;

    // Notice splash shown when ApplyAutoCrop actually shrank the view. Modal:
    // user dismisses with "OK" (keep crop) or "Uncrop" (restore full view).
    bool autocropNoticeActive;
    char autocropNoticeMsg[256];

    // Optional mLnL annotations parsed from the loaded WAV (empty if the file
    // doesn't carry the chunk). The annotations overlay has two surfaces:
    //   1. A faint always-on draw on the spectrogram (alpha = opacityBase).
    //   2. A "timeline lane" above the spectrogram for browsing events;
    //      hover/click in the lane bumps the matching spectrogram overlay to
    //      opacityHover so the user can find/inspect specific events without
    //      the overlay drowning the underlying signal.
    MlnlAnnotations annotations;
    int hoveredEvent;            // spectrogram-cursor hit (-1 = none); used for tooltip
    bool showAnnotations;        // master on/off
    bool annotationsExpanded;    // sidebar dropdown open (per-kind checkboxes etc.)
    bool annotationKindEnabled[MLNL_KIND_MAX];  // per-kind visibility (filters both surfaces)
    float annotationOpacityBase;     // 0..1 — quiet always-on alpha for spectrogram overlay
    float annotationOpacityHover;    // 0..1 — alpha for hovered/selected events

    // Timeline lane state. The lane is rendered between the freq-range banner
    // and the spectrogram pixels. Collapsed = single-row sparkline; expanded =
    // one row per kind currently enabled in the file.
    bool timelineExpanded;
    int hoveredTimelineEvent;    // -1 = none; event index hovered in the lane
    int selectedAnnotation;      // -1 = none; persistent selection from a lane click
} SpectrogramApp;

// ============================================================================
// Global State (defined in spectrogram.c)
// ============================================================================

extern SpectrogramApp app;
extern Sound AudioPlaybackSound;
extern Texture2D colormapTexture;
extern Font mainFont;

// Reset all per-signal state after a new signal is loaded into app.signal
// (defined in spectrogram.c; used by every load path).
void ResetForNewSignal(void);

// Auto-crop the display freq axis and time view to "frequencies/times of
// interest" using whichever source has high confidence:
//   1) mLnL annotations — max(f_hi)+headroom for freq, span(t_start..t_end)+pad for time
//   2) STFT energy heuristic — cumulative-energy threshold for freq, activity envelope for time
// A no-op when neither source meets the confidence test (e.g. signal genuinely
// uses most of the band/timeline). Modifies displayMaxFreqHz, view.start/end,
// and invalidates the texture cache. Called automatically after STFT init on
// every file load; can also be re-run from the sidebar button.
void ApplyAutoCrop(void);

// True when a modal overlay owns input; normal spectrogram/keyboard interaction
// is gated off while this is the case. Add new overlays here in one place.
static inline bool UiModalOpen(void)
{
    return app.showFileBrowser || app.showAbout || app.autocropNoticeActive;
}

// Reset the box selection to the full signal (the "no selection" state).
static inline void ClearSelection(void)
{
    app.sel.timeStart = 0.0f; app.sel.timeEnd = 1.0f;
    app.sel.freqStart = 0.0f; app.sel.freqEnd = 1.0f;
}

// Effective top of the displayed frequency axis (Hz). Capped at the actual
// signal Nyquist so the crop never tries to show frequencies that aren't in
// the data. All DISPLAY-side code paths (labels, banner, annotation freq
// mapping, texture sampling fraction) should reach the frequency axis through
// this helper instead of computing sampleRate*0.5 directly. Data-side math
// (STFT bin spacing, PSD, audio filtering) keeps using the true Nyquist.
static inline float EffectiveMaxFreqHz(void)
{
    if (app.signal.sampleRate <= 0) return 1.0f;
    float nyq = app.signal.sampleRate * 0.5f;
    if (app.displayMaxFreqHz > 0.0f && app.displayMaxFreqHz < nyq) return app.displayMaxFreqHz;
    return nyq;
}

// Fraction of the texture's full frequency axis that should be visible (0..1).
// Used by the spectrogram's texture sub-image extraction: a value of 0.5 means
// "the visible window's freqEnd=1.0 corresponds to the texture's mid-row".
static inline float DisplayFreqFraction(void)
{
    if (app.signal.sampleRate <= 0) return 1.0f;
    float nyq = app.signal.sampleRate * 0.5f;
    if (nyq <= 0.0f) return 1.0f;
    return EffectiveMaxFreqHz() / nyq;
}

// ============================================================================
// Keymap — single source of truth for global key bindings.
// The dispatcher (DispatchKeymap in spectrogram.c) runs every entry whose
// `action` is non-NULL and whose gate passes; the About/Help overlay renders
// the whole table so the on-screen key list can never drift from the bindings.
// Order-sensitive keys (Space, Esc) carry action==NULL and are handled inline
// where their frame ordering matters; they appear here for documentation only.
// ============================================================================
typedef void (*KeyActionFn)(void);

#define KEYGATE_NONE   0u
#define KEYGATE_MODAL  1u   // skip while a modal overlay is open (!UiModalOpen())
#define KEYGATE_LOADED 2u   // require a loaded signal
#define KEYGATE_STFT   4u   // require a computed STFT

typedef struct {
    int key;             // raylib key code
    unsigned gate;       // KEYGATE_* bitmask
    KeyActionFn action;  // NULL = handled inline / documentation-only
    const char* label;   // shown in the help overlay, e.g. "O", "Home"
    const char* help;    // description for the help overlay
} KeyBinding;

// Returns the keymap table and its entry count (defined in spectrogram.c).
const KeyBinding* GetKeymap(int* count);

// ============================================================================
// Small math helpers (header-inline so every module can use them)
// ============================================================================

static inline float AmplitudeToDecibels(float amplitude)
{
    if (amplitude < 0.0001f) amplitude = 0.0001f;
    return 20.0f * log10f(amplitude);
}

static inline float Clamp(float value, float min, float max)
{
    if (value < min) return min;
    if (value > max) return max;
    return value;
}

#endif // SPECTROGRAM_TYPES_H
