// waveform_view.h — the WAVEFORM band's interior: the drawn lane/overlay surface, plus
// frame<->pixel mapping, marker hit-test and zero-crossing snap. Unit-tested outside the
// DAW; the shell draws and marshals mouse events into it.
//
// The band maps a sample's full frame span [0, frameCount] linearly across its width.
// Markers are a generic N-named-marker set (not hardcoded specials), so a different mode
// (e.g. start + %-length end + fades) can repurpose the same machinery.

#pragma once

#include <cstdint>

#include "core/instrument/ui/editor_geometry.h"  // Rect, OverlayArea, contains
#include "core/instrument/ui/sample_bands.h"     // LaneSplit (resolveLaneSplit's answer)
#include "core/audio/peaks.h"            // AudioSample (float), Envelope

namespace reasampler::instrument::ui {

using audio::AudioSample;

// What the waveform band actually draws: the channel lane(s), and THE rect every overlay
// riding the waveform occupies.
//
// OVERLAY CONTRACT — `overlay` is the whole band in BOTH modes, never a lane. The amp
// envelope trace and its node handles, the start/loop markers, and the loop region draw
// ONCE into `overlay`, spanning both stacked lanes in stereo. Hit-testing reads the same
// rect, so a grab in the lower lane resolves to the same overlay item as one in the upper.
// Anything that draws per lane is a duplicate and a defect.
struct WaveformSurface {
    Rect upper;          // lane 0 -> channel 0 (LEFT); the whole band when single-lane
    Rect lower;          // lane 1 -> channel 1 (RIGHT); empty() when single-lane
    OverlayArea overlay; // the full band, both modes
    int laneCount = 0;  // 0 on a degenerate band, else 1 or 2 — matches `lower`'s emptiness
                        // (2 iff lower non-empty). For a non-empty band <= 2px tall, `upper`
                        // can be empty too while this still reports 1 — unreachable through
                        // the band-stack allocator's kWaveformMinHeight floor.
};

// THE lane-split decision, free of any pixel geometry: two lanes need BOTH stereo mode and a
// source that has a second channel to show, since a mono source under stereo mode is dual-mono
// and a second lane would be the redundant duplicate single-lane mode exists to avoid. The one
// home of that rule — the meter's bar count is the SAME question and reads it here, rather than
// inferring it from a band rect it has no business knowing about.
LaneSplit resolveLaneSplit(bool stereoMode, int sourceChannels);

// Resolves the surface for a waveform band, folding the split above and then measuring it
// against the band: WaveformSurface::laneCount can still report 1 for a Stereo split on a band
// too thin to divide, which is a geometry fact and not a second rule.
WaveformSurface waveformSurface(const Rect& band, bool stereoMode, int sourceChannels);

// THE overlay area, standalone — same value as WaveformSurface::overlay, for the hit-test
// paths that have no channel count to hand. An overlay's rect never depends on the lane
// split, which is exactly the contract.
OverlayArea waveformOverlayArea(const Rect& band);

// The single-channel envelope lane `lane` draws, taken from a multi-channel envelope
// computed in ONE computeEnvelope pass (it already envelopes channels independently, so a
// second lane costs no second scan of the PCM). Lane 0 is the upper lane and takes channel
// 0, lane 1 the lower and channel 1 — the L-above-R order. An out-of-range lane yields an
// empty envelope, which draws as a bare midline.
audio::Envelope laneEnvelope(const audio::Envelope& env, int lane);

// Pixel width of a marker's grab region either side of its x line. Mirrors keyboard_strip's
// edge-grab idiom.
inline constexpr int kMarkerGrabWidth = 5;

// x pixel of `frame` under the linear map: frame 0 -> area.x, frame frameCount -> area.right().
// Frame is clamped to [0, frameCount] before mapping. frameCount <= 0 or a zero-width area pins
// every frame to area.x. Takes the overlay (not a lane) — see the OVERLAY CONTRACT above.
int frameToX(const OverlayArea& area, std::int64_t frameCount, std::int64_t frame);

// Inverse of frameToX: the frame a point x maps to, clamped to [0, frameCount]. A point left of
// area.x yields 0; right of area.right() yields frameCount.
std::int64_t xToFrame(const OverlayArea& area, std::int64_t frameCount, int x);

// A marker's grab HANDLE — THE CAP, in the mark grammar's vocabulary: a tab riding the top of
// the overlay, centred on the marker's x and clipped into the area. Distinct from the
// full-height grab COLUMN markerAtPoint answers, so two markers that share a frame stay
// independently grabbable — the handle owns the top strip, the column owns everything below
// it. Without that split, first-in-draw-order wins every coincident tie and the loser can never
// be dragged apart again. Every mark's cap is this ONE rect shape; only the glyph drawn inside
// it differs, which is what lets the claim arbitration see a single nominal cap area.
inline constexpr int kMarkerHandleHeight = 10;
// Same half-width as the column's own grab band on purpose: the handle is that same grab
// tolerance, just confined to the top strip, not an independent tuning.
inline constexpr int kMarkerHandleHalfWidth = kMarkerGrabWidth;
Rect markerHandleRect(const OverlayArea& area, std::int64_t frameCount, std::int64_t frame);

// --- The overlay's four marks -------------------------------------------------------------

// The marks the overlay carries, in DRAW and COLUMN-hit order. The shell's WaveMarker aliases
// this, so the drag router and the geometry below cannot disagree about an ordinal.
enum class WaveMark { kStart = 0, kLoopStart = 1, kLoopEnd = 2, kCrossfade = 3, kCount = 4 };
inline constexpr int kWaveMarkCount = static_cast<int>(WaveMark::kCount);

// Which marks are on screen and where. A mark that is not `present` is excluded from every
// answer below — with no loop set there is no crossfade mark to reach.
struct WaveMarks {
    std::int64_t frame[kWaveMarkCount] = {0, 0, 0, 0};
    bool present[kWaveMarkCount] = {false, false, false, false};
};

// Which mark's cap a point lands on, or -1. Caps resolve in the REVERSE of the column order
// markerAtPoint uses — crossfade, end, loop start, start — and that reversal is the whole
// separability argument: whichever mark of a coincident PAIR loses the cap still answers its
// own full-height column, and the crossfade, the one mark with no column at all, is first so
// nothing can shadow it. A coincident TRIPLE still strands its middle mark, exactly as the
// pre-cap tab/column split did.
int capAtPoint(const OverlayArea& area, std::int64_t frameCount, const WaveMarks& marks,
               int x, int y);

// Labels sit in the row directly below the caps, beside the mark's line: START and LOOP to the
// right of it, END and XFADE to the left, so a label never crosses into the span it bounds.
inline constexpr int kMarkLabelHeight = 10;
inline constexpr int kMarkLabelGap = 3;  // between the mark's line and its text

bool markLabelLeftOfLine(WaveMark m);

// The box `textWidth` px of label occupies for a mark at `frame`. Nudged inside the area rather
// than clipped — half a label reads as a different mark's — and empty when it cannot fit.
Rect markLabelRect(const OverlayArea& area, std::int64_t frameCount, std::int64_t frame,
                   bool leftOfLine, int textWidth);

// The placed labels; an empty box is a label that is not drawn. `textWidth` is per mark, in the
// caller's own font (this module measures no text). `promoted` — a WaveMark ordinal, or -1 —
// is placed FIRST and so can never be the one suppressed: it is the mark the user is grabbing
// or hovering, i.e. the one they are asking about.
struct WaveMarkLabels {
    Rect box[kWaveMarkCount];
};
WaveMarkLabels layoutMarkLabels(const OverlayArea& area, std::int64_t frameCount,
                                const WaveMarks& marks, const int* textWidth, int promoted);

// The crossfade region's peak edge-wedge height. The region draws as a wedge at the overlay's
// top and bottom edges and NEVER as a second fill: it now sits INSIDE the loop span, where a
// translucent fill would stack on the loop fill over an already-accepted under-floor contrast
// pair (see editor_paint_waveform.cpp).
inline constexpr int kCrossfadeWedgePx = 10;

// Wedge height at pixel column `x` over [x0, x1): zero at x0, kCrossfadeWedgePx at x1 - 1. The
// audible region and the ingredient ghost are the SAME ramp over the two spans the fade mixes,
// so one function draws both.
int crossfadeWedgeHeight(int x0, int x1, int x);

// Which marker (index into the caller's parallel `frames` array, in draw order) a grab at
// (x, y) lands on, or -1 for a miss. A marker is grabbed when x is within kMarkerGrabWidth of
// its drawn x and y is inside `area`. First marker in draw order wins an overlapping tie.
int markerAtPoint(const OverlayArea& area, std::int64_t frameCount, const std::int64_t* frames,
                  int count, int x, int y);

// Resolves a drag to a new frame: `startFrame` shifted by round(dxPixels * frameCount /
// areaWidth), clamped to [0, frameCount]. The shell applies between-marker clamps (e.g.
// start <= loopEnd) after this per-marker resolve.
std::int64_t resolveDragFrame(const OverlayArea& area, std::int64_t frameCount,
                              std::int64_t startFrame, int dxPixels);

// Nearest zero-crossing frame to `target` in the mono PCM, for loop/start snap. A crossing is a
// frame i (1 <= i < frames) where pcm[i-1] and pcm[i] differ in sign (pcm[i] == 0 snaps to i).
// Search fans out symmetrically from the clamped target; an equidistant tie resolves to the
// lower frame. No sign change anywhere (or fewer than 2 frames) returns the clamped target
// unchanged.
std::int64_t nearestZeroCrossing(const AudioSample* pcm, std::int64_t frames,
                                 std::int64_t target);

}  // namespace reasampler::instrument::ui
