// 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

// Hard ceiling on the spectrogram image's width in pixels. GL implementations
// commonly cap textures at 16384 px per dimension, and a multi-hour capture
// produces far more STFT segments than that (~478k for 5.7 h at 12 kHz), so
// without a cap the texture upload fails and nothing draws at all. Segments
// beyond the cap are folded into columns; see ComputeSpectrogramReassignment.
// Kept below the common limit to leave headroom on weaker GL drivers.
#define MAX_SPECTRO_IMAGE_WIDTH 8192

// Contiguous time span containing one or more colliding annotations. Adjacent
// collisions are merged into a single region so the overlay draws one band per
// pile-up rather than one per event, which would smear into a solid wall when
// zoomed out on a long capture.
typedef struct {
    double t0, t1;
    double f_lo, f_hi;  // union of the colliding events' bands, in Hz
    int count;          // events involved in this region
} CollisionRegion;

// Vertical padding added to a collision band when drawing it, so a narrow
// overlap is still visible without covering the whole frequency axis.
#define COLLISION_BAND_PAD_HZ 100.0

// Cap on merged collision regions tracked per file. Beyond this the overlay
// still reports the total collision count, it just stops adding bands.
#define MAX_COLLISION_REGIONS 4096

// How long the auto-crop toast stays up, in seconds of *focused* time.
#define AUTOCROP_NOTICE_SECONDS 5.0f

// Sidebar sizing. It is an icon rail: the default width is exactly one icon
// plus its margins, so it costs the spectrogram as little width as possible.
// Settings that need more room open as popouts rather than widening the rail.
// Drag the right edge to resize, or drag past the minimum / double-click to
// collapse. RAIL_ICON is the button size the width is derived from.
#define RAIL_ICON_SIZE         34.0f
#define RAIL_ICON_MARGIN        6.0f
#define SIDEBAR_WIDTH_DEFAULT (RAIL_ICON_SIZE + RAIL_ICON_MARGIN * 2)  // 46
#define SIDEBAR_WIDTH_MIN     (RAIL_ICON_SIZE + RAIL_ICON_MARGIN * 2)

// Menubar across the top of the window.
#define MENUBAR_HEIGHT 22.0f

// Minimap thumbnail: a heavily downscaled view of the whole capture, rendered
// once into a texture and reused every frame. Small on purpose — it exists to
// show *where* you are, not to be readable, so a coarse blob is fine and keeps
// the one-time build cheap.
// Two sizes, large exactly double small, each with its own cached texture.
// The corner handle switches between them rather than free-resizing, so there
// are only ever two thumbnails to keep valid.
#define MINIMAP_TEX_W       256
#define MINIMAP_TEX_H        64
#define MINIMAP_TEX_W_LG    512
#define MINIMAP_TEX_H_LG    128
#define MINIMAP_DRAW_W      240.0f
#define MINIMAP_DRAW_H       60.0f
#define MINIMAP_MARGIN       10.0f
#define MINIMAP_HANDLE       12.0f   // corner grab square, bottom-left

// Neighbourhood the cursor's dB readout averages over, in STFT cells (not
// screen pixels — see DrawCursorReadout). Wider in frequency than in time:
// OFDM subcarriers are the noisy axis, whereas widening time would average
// across symbol boundaries. Sized to stay well inside one mLink channel.
#define CURSOR_AVG_SEGS 3   // +/- segments  (~300 ms at 12 kHz / 2048)
#define CURSOR_AVG_BINS 6   // +/- bins      (~76 Hz)

// How many overlapping annotation boxes the cursor-hit stack retains. Deeper
// piles than this are counted but not listed individually (the tooltip says
// "+N more"), which keeps a dense pile-up from covering the spectrogram.
#define MAX_HOVER_STACK 12

// 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 within the PLAYING region
    // Wall-clock instant (GetTime()) at which the current buffer started
    // playing. The playhead is derived from this rather than accumulated per
    // frame: summing GetFrameTime() drifts, because every frame spent on
    // rendering work is counted as playback time, and the error compounds over
    // a long region. Audio runs on its own clock, so the playhead has to as
    // well. Offset by playheadSeekT when the user scrubs.
    double playStartTime;
    float playheadSeekT;       // 0-1 offset into the buffer that playback began at
    // Where the next play should start within the selection (0-1). Set by
    // dragging the playhead while stopped; reset once the selection changes.
    float playFromT;
    bool playheadDragging;
    // Repeat the selection when it reaches the end, instead of stopping.
    bool loopPlayback;

    // Mouse cursor requested for this frame, applied once at the end of it.
    // Several handlers have an opinion about the cursor and they run in
    // sequence, each calling SetMouseCursor unconditionally — so whichever ran
    // last won, and two of them disagreeing produced a visible flicker between
    // shapes. Handlers now record a request and the highest-priority one wins.
    int cursorRequest;
    int cursorPriority;

    // Snapshot of the region actually handed to the audio device, captured at
    // PlaySelectedRegion time. The playhead must be measured against this, not
    // against the live app.sel — the user can move or resize the selection while
    // audio is still playing, and the marker has to keep tracking the sound
    // that's really coming out. playDuration comes from the buffer's own sample
    // count / sampleRate, so it can't drift from app.signal.duration (which is
    // derived pre-mono-downmix and disagrees for stereo files).
    float playSelStart, playSelEnd;  // sel.timeStart/End when playback began
    float playDuration;              // true length of the playing buffer, seconds

    // 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;
    // STFT segments folded into each image column (1 = one column per segment).
    // >1 once the visible span has more segments than MAX_SPECTRO_IMAGE_WIDTH,
    // and needed by anything converting between segment indices and image X.
    int reassignSegsPerCol;
    // Segment range the cached image covers, as [first, last). The image is
    // built for the visible span rather than the whole file, so zooming in
    // genuinely re-renders at higher resolution instead of magnifying pixels.
    // A rebuild is triggered when the view leaves this range.
    int reassignSegFirst, reassignSegLast;

    // 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];
    float autocropNoticeTimer;   // seconds left; counts down only while focused

    // Default LMB gesture on the spectrogram. Off (the default) = LMB pans and
    // Ctrl+LMB drags a selection box; on = the two swap, so LMB selects. The
    // modifier always means "the other one", so either mode can do both without
    // going back to the rail.
    bool selectMode;

    // Minimap: a cached thumbnail of the whole capture in the top-right corner,
    // with the current view drawn as a rectangle on it. Click or drag to
    // navigate. The texture is rendered once and only rebuilt when something
    // that changes its content does (new file, colormap, annotation/collision
    // overlays toggled) — never per frame.
    bool showMinimap;
    bool minimapLarge;          // corner handle toggles small <-> large
    // One cached texture per size. Both are built at the same source
    // resolution, so switching size never triggers a rebuild — only a content
    // change (new file, colormap, overlay toggles) does.
    Texture2D minimapTexture[2];
    bool minimapValid[2];
    bool minimapDragging;

    // Icon rail width (fixed — every button is the same size) and its collapse
    // state. Collapsing hands the whole window width to the spectrogram.
    float sidebarWidth;
    bool sidebarCollapsed;

    // Menubar: index of the open menu (-1 = none). Click to open, move across
    // to switch, click elsewhere or pick an item to close.
    int openMenu;

    // 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

    // Every annotation box the cursor is currently inside, not just the topmost.
    // Overlapping transmissions are the normal case (multiple stations on the
    // air at once), and a single hit index silently hid everything underneath —
    // so the stack is collected during the draw pass and the tooltip reports
    // all of it. Topmost-last, matching draw order; hoveredEvent is the last
    // entry.
    int hoverStack[MAX_HOVER_STACK];
    int hoverStackCount;

    // Collision analysis: which events share time AND frequency with another,
    // i.e. genuinely overlap on the air rather than merely looking stacked at
    // the current zoom. Computed once per annotation set (see ComputeCollisions)
    // because it depends only on the event data, not on the view.
    unsigned char* collisionFlags;   // one byte per event, 1 = collides
    int collisionCount;              // events involved in any collision
    CollisionRegion collisionRegions[MAX_COLLISION_REGIONS];
    int collisionRegionCount;        // merged contiguous spans of collisions
    bool showCollisions;             // overlay toggle
    int currentCollision;            // region index of the last jump (-1 = none)
    // Sidebar prev/next request, consumed by the main loop: -1 back, +1 forward,
    // 0 idle. The jump helpers are static to spectrogram.c, so the button can't
    // call them directly.
    int jumpCollisionRequest;
    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 showTimeline;       // lane visible at all (off => spectrogram gets the space)
    bool timelineExpanded;   // one row per kind instead of a single sparkline
    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)
{
    // The auto-crop notice is a toast, not a modal — it must not swallow keys
    // or block interaction with the spectrogram underneath it.
    return app.showFileBrowser || app.showAbout;
}

// Request a mouse cursor for this frame. Higher priority wins; ties go to the
// first caller. Applied once per frame (see the end of the main loop), so
// handlers can state their preference without fighting each other.
static inline void RequestCursor(int shape, int priority)
{
    if (priority > app.cursorPriority) {
        app.cursorPriority = priority;
        app.cursorRequest = shape;
    }
}

// Cursor priorities: an active drag outranks a hover hint, which outranks the
// default. Keeps "what am I doing" ahead of "what could I do".
#define CURSOR_PRI_DEFAULT  0
#define CURSOR_PRI_HOVER    10
#define CURSOR_PRI_ACTIVE   20

// 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;
    // A scrub offset is meaningless against a selection that no longer exists.
    app.playFromT = 0.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;
}

// Tightest allowed time window, as a fraction of the whole file.
//
// This MUST be derived from the file's duration rather than being a flat
// fraction: view.start/end are normalized to the file, so a constant floor
// makes the achievable time resolution scale with file length (a flat 2%
// capped a 30-minute recording at a 36-second window, while a 30-second one
// reached 0.6 s). The physical limit is the STFT hop — segments sit
// fftSize/HOP_RATIO samples apart, so once only a few segments span the
// viewport there is no further detail to reveal and zooming past that just
// interpolates. MIN_VISIBLE_SEGMENTS sets how many must stay in view.
#define MIN_VISIBLE_SEGMENTS 8
static inline float MinTimeViewWidth(void)
{
    if (app.signal.sampleRate <= 0 || app.signal.duration <= 0.0f) return 0.02f;
    int hopSamples = app.fftSize / HOP_RATIO;
    if (hopSamples < 1) hopSamples = 1;
    float hopSec = (float)hopSamples / (float)app.signal.sampleRate;
    float minSpanSec = hopSec * (float)MIN_VISIBLE_SEGMENTS;
    float w = minSpanSec / app.signal.duration;
    if (w > 1.0f) w = 1.0f;    // file shorter than the floor: whole file is the min
    if (w < 1e-7f) w = 1e-7f;  // guard float precision in the view math
    return w;
}

// ============================================================================
// 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);

// ============================================================================
// Menubar — built on top of the keymap so a menu item and its shortcut can
// never drift apart. Most items name a keymap entry by its raylib key code and
// reuse that entry's action, gate, and shortcut label. Items that toggle a
// simple flag with no binding point at the flag instead; a separator is an
// entry with neither.
// ============================================================================
typedef struct {
    const char* label;   // NULL = separator
    int key;             // keymap key to invoke, or 0
    bool* toggle;        // flag to flip when there's no keymap entry, or NULL
} MenuItem;

typedef struct {
    const char* title;
    const MenuItem* items;
    int itemCount;
} Menu;

// Returns the menubar definition and its menu count (defined in ui.c).
const Menu* GetMenus(int* count);

// Run a keymap entry's action by key code, honoring its gate. Used by the
// menubar so clicking an item goes through exactly the same path as the key.
void InvokeKeymapAction(int key);
bool KeymapActionEnabled(int key);   // false => draw the item greyed out
const char* KeymapLabelFor(int key); // shortcut text, "" if unbound

// ============================================================================
// 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
