// master_meter.h — the interior of the spanning deck's readout column: the label gutter and
// bar field it divides into, the dB->y map its scale draws against, and the per-instance UI
// state the bars are drawn from. knob_deck hands over the column rect; this lays out inside
// it. Every timed, logged or latched quantity lives here, on the UI thread — the audio thread
// publishes raw block magnitudes and converts nothing.

#pragma once

#include "core/instrument/engine/meter_ballistics.h"
#include "core/instrument/ui/editor_geometry.h"
#include "core/instrument/ui/sample_bands.h"  // LaneSplit

namespace reasampler::instrument::ui {

inline constexpr int kMeterLabelW = 22;   // the numeral gutter, left of the bars
inline constexpr int kMeterLabelGap = 4;
inline constexpr int kMeterFieldW = 36;   // one 36px bar, or two 17px bars kMeterBarGap apart
inline constexpr int kMeterBarGap = 2;

// What the interior consumes, and therefore the width the deck must RESERVE for the column.
// knob_deck's MASTER descriptor reads this rather than restating 62 — §1.2 banks MASTER's
// growth to 236 as the meter's growth room, so this constant is expected to move.
inline constexpr int kMeterColumnW = kMeterLabelW + kMeterLabelGap + kMeterFieldW;

// A tick every 6 dB up the scale; every other one carries a numeral, and 0 dB draws heavier.
inline constexpr double kMeterTickStepDb = 6.0;

// Whether the tick at `db` carries a numeral. The numeral SET is spec-pinned (0, −12, −24,
// −36, −48, −60), so it lives beside the step it is derived from rather than in the painter.
bool meterTickNumeralled(int db);

struct MeterRects {
    Rect labels;  // the numeral gutter
    Rect field;   // the whole bar field
    Rect barA;    // Single: the one wide bar. Stereo: L.
    Rect barB;    // empty() unless Stereo
};

// `split` is the RESOLVED lane decision resolveLaneSplit already folds (channel mode AND the
// source's channel count), not "is the instrument in stereo mode": a mono source under stereo
// mode is dual-mono, and two identical bars would be a lie. One source, two views, one rule.
// A column narrower than kMeterColumnW yields nothing rather than an overrunning field.
MeterRects meterRects(const Rect& column, LaneSplit split);

// Whether `dirty` (a WM_PAINT update rect, already client-clipped) lies wholly inside the bar
// field — the editor's meter-frame fast-path test. Geometric, not a flag: the caller decides
// per paint from the rect Windows actually handed it, never from a remembered "was this a
// meter tick" bit, because Windows unions a meter invalidate with any other pending one into a
// single rcPaint and a flag would then paint a meter frame over a face that had really changed.
bool meterFastPathEligible(const MeterRects& m, const Rect& dirty);

// The structural invariant drawMeterField's self-containment rests on: every bar it paints
// stays inside the field it fills first. The ticks, the held-peak tick and the clip cap are all
// drawn directly off `field`'s own coordinates and so are bounded by construction; the bars are
// the one independently-computed rect that isn't.
bool meterBarsWithinField(const MeterRects& m);

// y of `db` inside the bar field — kMeterTopDb at the top edge, kMeterFloorDb at the bottom,
// linear in dB between, clamped outside.
int meterDbToY(const Rect& field, double db);

// The numeral's label rect for the tick at `y`, kept inside the gutter: the floor tick sits ON
// the field's bottom edge, and an unclamped y±5 box would hang below the column.
Rect meterNumeralRect(const Rect& labels, int y);

// The per-instance UI state behind the column. One clip latch per channel (the cap is drawn
// once, over whichever of them tripped).
struct MasterMeterUi {
    engine::MeterState left;
    engine::MeterState right;
    double reductionDb = 0.0;  // how far the limiter is pulling gain down; 0 = not working
    // The lamp's hold, on the SAME principle (and the same window) as the peak tick's: without
    // it a catch smaller than kMeterFallDbPerSecond x the UI period is fully decayed by the
    // next frame, so the lamp is dark again after the single repaint the catch landed on.
    double reductionHoldSeconds = 0.0;
};

// What the audio thread published SINCE THE LAST READ, in this module's own vocabulary — the
// peaks are a max over every block in that window and minGain a min, so no block is discarded
// unseen between two UI frames.
struct MasterMeterBlock {
    double peakL = 0.0;
    double peakR = 0.0;
    double minGain = 1.0;  // the limiter's smallest gain over the window; 1 = no reduction
    bool clip = false;     // the AUDIO thread's own latch
};

MasterMeterUi advanceMasterMeter(MasterMeterUi prev, const MasterMeterBlock& block,
                                 double elapsedSeconds);

bool meterClipped(const MasterMeterUi& m);
MasterMeterUi clearMasterMeterClip(MasterMeterUi prev);

// What the ONE bar shows on a single-lane column: the two channels folded per FIELD, not the
// louder channel's whole state. Picking a channel by level would draw a hold tick and a clip
// belonging to whichever won on level — inert while L ≡ R on every path that reaches Single,
// and wrong the moment they diverge.
engine::MeterState meterSingleLaneState(const MasterMeterUi& m);

// Whether two states would DRAW the same, so the UI tick can repaint only on a change and an
// idle editor costs nothing. Compares what the column shows — the bar, the held tick, the cap
// and the lamp — not every stored double.
bool meterDrawEqual(const MasterMeterUi& a, const MasterMeterUi& b);

// The lamp reports "the limiter is working", not "a sample grazed the threshold", so it needs
// a floor rather than a bare non-zero test. 0.5 dB is a CHOSEN floor, not a measurement — the
// spec asks only for "a small floor". It is bounded on BOTH sides: raising it hides genuine
// catches, since the limiter's ceiling is only −0.3 dBTP; lowering it turns the lamp into a
// "some sample crossed the ceiling" light, because the gain law is ceiling/peak and so reports
// an arbitrarily small reduction for a peak arbitrarily close to the ceiling.
inline constexpr double kGrLampFloorDb = 0.5;
bool grLampLit(const MasterMeterUi& m);

}  // namespace reasampler::instrument::ui
