// envelope_edit.h — PURE node hit-test + pixel-delta→clamped-param inverse map for the S-VIEW-3
// draggable envelope nodes. NO VST3, NO REAPER, NO SWELL/LICE types at the boundary. The mirror
// of card_drag / waveform_view: the drag arithmetic + clamp/monotonic constraints live here,
// unit-tested at the boundaries outside the DAW, while the editor shell (reasampler_editor.cpp)
// draws the handles, captures the grab on WM_LBUTTONDOWN, feeds each move's pixel delta back
// through here, and commits the resulting params to the zone through the same off-audio-thread
// path a slider edit uses.
//
// TWO SURFACES, ONE MODEL. envelope_overlay owns the params→polyline FORWARD map (draw); this
// module owns the pixel→params INVERSE map (edit) + node hit-test. Both read/write the SAME
// AmpEnvelope fields (the shell re-reads the zone every paint — no listener chain), so a node
// drag and a slider edit are two views on one source of truth and can never diverge.
//
// THE INVARIANT (S-VIEW-F2). A drag can NEVER produce a param a slider couldn't:
//   * MONOTONIC IN TIME — a node clamps between its time predecessor and successor, so attack-end
//     can't pass hold-end, decay can't pass release, etc. Each segment stays >= 0.
//   * RANGE-CLAMPED — times clamp to the SAME per-param [min,max] the slider enforces; levels
//     clamp to [0,1]. Because the concrete second/fraction maxima live SHELL-SIDE (param_slider
//     is deliberately engine-free — the shell owns the 0..1↔domain mapping), the clamp bounds are
//     CALLER-SUPPLIED here (EnvClampBounds): the shell passes the same maxima it feeds the slider,
//     so the two surfaces share one clamp by construction.
//
// WHICH AXES. Time-only nodes (attack-end, hold-end, release-end; fade-in-end, length-end,
// fade-out-end) drag on X only. The sustain node (DecayEnd) drags on BOTH axes — its X sets the
// decay time, its Y sets the sustain level (the standard ADSR-editor grammar). Origin and the
// drawing-only ReleaseStart vertex are NOT draggable.
//
// Reuses editor_geometry's Rect + the EnvNode / AmpEnvelope / EnvMode types from
// envelope_overlay (one shared node vocabulary across draw + edit), and the shared timeToX /
// levelToY maps so the handle the overlay drew and the grab region here agree pixel-for-pixel.

#pragma once

#include <cstdint>
#include <vector>

#include "editor_geometry.h"    // Rect
#include "envelope_overlay.h"   // EnvNode, EnvMode, AmpEnvelope, EnvVertex, timeToX/levelToY

namespace reasampler::vst {

// The pick radius (px) around a node's drawn point: a grab within this many pixels (in BOTH x and
// y) of a node handle grabs it. Mirrors waveform_view's kMarkerGrabWidth — wide enough to grab a
// small handle comfortably, narrow enough that adjacent nodes stay distinguishable.
inline constexpr int kNodeGrabRadius = 6;

// The per-param clamp bounds the shell supplies (the SAME maxima its sliders map 0..1 onto). All
// are upper bounds in the param's own domain; the lower bound is 0 (each stage >= 0), and the
// monotonic-in-time constraint tightens these further at edit time. Defaults are conservative
// placeholders; the shell OVERRIDES them with its live slider domain so the clamp matches exactly.
struct EnvClampBounds {
    double maxAttackSeconds  = 4.0;   // upper bound of the attack slider
    double maxHoldSeconds    = 4.0;
    double maxDecaySeconds   = 4.0;
    double maxReleaseSeconds = 4.0;
    // Trigger fades + length are fractions; their natural upper bound is 1.0. Exposed so a shell
    // that caps a fade below the full span (e.g. 0.5) shares that cap with its slider.
    double maxFadeInFraction  = 1.0;
    double maxFadeOutFraction = 1.0;
    double maxLengthFraction  = 1.0;
    // sustainLevel is always [0,1] — no shell knob needed, kept implicit.
};

// Which node a grab at (x, y) lands on, given the CURRENT envelope + overlay rect + sample
// duration (the same inputs buildEnvelopePolyline drew from, so the grab tests the drawn handles).
// Returns EnvNode::Origin's NON-membership as a miss via the bool return: `hit` is false for a
// point off every DRAGGABLE node. Origin and ReleaseStart are never returned (not draggable). On a
// tie (two handles within the radius) the earlier draw-order node wins (deterministic, mirroring
// waveform_view::markerAtPoint's first-match). Pure.
struct NodeHit {
    bool    hit  = false;
    EnvNode node = EnvNode::Origin;  // meaningful only when hit == true
};
NodeHit nodeAtPoint(const AmpEnvelope& env, const Rect& area, double totalSeconds, int x, int y);

// Resolve a drag of `node` to a new AmpEnvelope. Given the envelope AS OF GRAB TIME (`grabEnv` —
// the shell snapshots it on WM_LBUTTONDOWN so the delta is absolute, not accumulated), the overlay
// rect + sample duration (the pixel↔param maps), the caller's clamp bounds, and the pixel delta
// since grab (`dxPixels`, `dyPixels`), returns the envelope the node should now describe:
//   * X delta -> the node's TIME param, shifted proportionally (same linear map as timeToX),
//     clamped to [0, per-param max] AND to its monotonic-in-time neighbours (>= predecessor time,
//     <= successor time). For a cumulative-time node the shift lands on that node's OWN segment
//     duration (e.g. dragging HoldEnd changes holdSeconds, not attack).
//   * Y delta -> the LEVEL param, but ONLY for the sustain node (DecayEnd); clamped to [0,1].
//     dyPixels is IGNORED for every time-only node.
//   * Non-draggable node (Origin / ReleaseStart) or a zero-width/zero-height area or
//     totalSeconds <= 0 -> `grabEnv` returned unchanged (no motion).
// Only the dragged node's param(s) change; every other field carries through from `grabEnv`. Pure
// — rounding is to the param's continuous value (no snapping, matching the sliders' resolution).
AmpEnvelope resolveNodeDrag(const AmpEnvelope& grabEnv, EnvNode node, const Rect& area,
                            double totalSeconds, const EnvClampBounds& bounds,
                            int dxPixels, int dyPixels);

}  // namespace reasampler::vst
