// param_slider.h — control-surface layout + hit-test + value<->pixel mapping for the
// editor parameter panel. Engine-free by design (no sampler_core/sample_map). Mirror of
// keyboard_strip/waveform_view/mode_switch; the shell draws each row and routes
// clicks/drags into these functions, owning the control-id -> engine-param binding and
// the value domain mapping.
//
// Controls are one of three shapes — a two-segment Toggle, a horizontal Slider, or a
// radial Knob with a needle and vertical-drag mapping — laid out as a vertical stack of
// fixed-height rows. This module maps a control's normalized value (0..1) to/from its
// handle pixel / needle angle; the shell converts each control's engine value (frames,
// seconds, a fraction, a signed semitone depth) to/from that 0..1.

#pragma once

#include <vector>

#include "core/instrument/ui/editor_geometry.h"  // Rect, contains
#include "core/instrument/ui/param_taper.h"      // DragModifiers (the shared interaction law)

namespace reasampler::instrument::ui {

// Fixed control-panel metrics, exposed so the shell and tests agree.
inline constexpr int kControlRowHeight = 22;
inline constexpr int kControlRowGap = 4;
inline constexpr int kControlLabelWidth = 92;
inline constexpr int kSliderHandleWidth = 8;
inline constexpr int kToggleSegments = 2;

// Toggle = two-segment selector (active segment highlights); Slider = horizontal track
// with a draggable handle over a 0..1 value; Knob = radial dial with a needle, dragged
// vertically (up = increase).
enum class ControlKind { Toggle, Slider, Knob };

// One control the shell places in the panel, in stack order. `id` is the shell's own
// control identifier, returned by the hit-test so the shell routes to the right engine
// param — this module never interprets it.
struct ControlDesc {
    int id = 0;
    ControlKind kind = ControlKind::Slider;
};

// Laid-out geometry of one control row: full row rect plus the interactive sub-rect (the
// track for a Slider, the whole control area for a Toggle — the shell splits a Toggle
// into segments via toggleSegmentRect).
struct ControlRow {
    int id = 0;
    ControlKind kind = ControlKind::Slider;
    Rect row;
    Rect label;
    Rect control;
};

// Lays out `controls` as a vertical stack of fixed-height rows inside `panel`, top-down.
// Label column takes the left kControlLabelWidth (clamped to the panel), control column
// the rest. A row past the panel bottom is still returned (the shell clips/suppresses it)
// so stack geometry is deterministic regardless of panel height.
std::vector<ControlRow> layoutControls(const Rect& panel,
                                       const std::vector<ControlDesc>& controls);

// Rect of segment `seg` within a toggle's `control` rect, splitting it into
// kToggleSegments equal segments left-to-right (last absorbs any width remainder).
Rect toggleSegmentRect(const Rect& control, int seg);

int toggleSegmentHitTest(const Rect& control, int x, int y);

// Slider track sub-rect inside `control`: inset so the handle stays fully within the
// control at value 0 and 1. The handle center ranges across [track.x, track.right()] as
// the value ranges [0,1].
Rect sliderTrackRect(const Rect& control);

// Handle rect for a slider at normalized `value` (clamped to [0,1]).
Rect sliderHandleRect(const Rect& control, double value);

// Maps a point x to a normalized slider value [0,1]: at/left of track start -> 0, at/right
// of end -> 1, linear between. Inverse of sliderHandleRect's position map.
double valueAtPoint(const Rect& control, int x);

// --- Radial knob ---------------------------------------------------------------------
//
// Angle convention: degrees clockwise from 12 o'clock (screen coords, y grows downward).
// The value arc sweeps clockwise from startDeg (value 0) to endDeg (value 1); an endDeg
// at-or-behind startDeg wraps +360.
//
// Default arc: 7 o'clock (210°) sweeping clockwise 300° to 5 o'clock (150°), leaving a
// symmetric 60° dead arc at the bottom; the 50% value lands at 12 o'clock. Angles are
// parameters, not hardcoded.
inline constexpr double kKnobArcStartDeg = 210.0;
inline constexpr double kKnobArcEndDeg = 150.0;

// Pixels of upward drag for one full 0->1 sweep.
inline constexpr int kKnobDragRangePixels = 128;

struct KnobArc {
    double startDeg = kKnobArcStartDeg;
    double endDeg = kKnobArcEndDeg;
};

// A knob's circle within its control cell: center + radius (doubles so the shell rounds
// once, at draw time). radius == 0 marks a degenerate cell.
struct KnobGeometry {
    double centerX = 0.0;
    double centerY = 0.0;
    double radius = 0.0;
};

struct KnobPoint {
    double x = 0.0;
    double y = 0.0;
};

// Knob circle inscribed in `cell`, centered, radius = half the smaller dimension. The
// shell must pass `row.control` both when drawing and hit-testing — controlAtPoint always
// uses `r.control` as the cell, so draw cell and hit cell must agree.
KnobGeometry computeKnob(const Rect& cell);

// True if (x, y) falls strictly inside the knob circle (boundary exclusive).
bool knobHitTest(const KnobGeometry& knob, int x, int y);

// Clockwise sweep of `arc` in degrees, in (0, 360]: normalized end - start, wrapping +360
// when the end is at-or-behind the start (default arc -> 300).
double knobSweepDeg(const KnobArc& arc);

// Needle angle for normalized `value` (clamped to [0,1]): startDeg at 0, endDeg at 1,
// linear between, normalized to [0, 360).
double knobValueAngleDeg(const KnobArc& arc, double value);

// Needle endpoint for normalized `value`: the point on the knob circle at the value's
// angle, from the center.
KnobPoint knobNeedlePoint(const KnobGeometry& knob, const KnobArc& arc, double value);

// Maps a vertical drag onto a knob value: `startValue` is the value at drag start,
// `dyPixels` the pointer's y displacement (down = positive). Up increases, down
// decreases; `dragRangePixels` pixels of travel covers the full 0..1 range. Ctrl in
// `mods` scales the rate (param_taper.h); Shift's snap is NOT applied here — the whole
// unit it snaps to is a property of the control's unit category, which this module does
// not know, so the caller applies it to the returned norm.
//
// The drag is grab-anchored ABSOLUTE, which is why the caller must re-anchor `startValue`
// and its own grab y on every modifier transition: rescaling an accumulated delta in place
// would jump the value by (1 - kFineDragScale) x whatever had accumulated.
double knobDragValue(double startValue, int dyPixels, const DragModifiers& mods = {},
                     int dragRangePixels = kKnobDragRangePixels);

// Control a point lands on, given laid-out `rows`. Returns the control id whose
// interactive area contains the point, or -1 for a miss. First matching row wins (rows
// never overlap).
int controlAtPoint(const std::vector<ControlRow>& rows, int x, int y);

}  // namespace reasampler::instrument::ui
