// envelope_edit.cpp — see envelope_edit.h. Pure inverse map + hit-test; no host types.

#include "core/instrument/ui/envelope_edit.h"

#include "core/util/clamp01.h"

#include <algorithm>
#include <cmath>    // std::fabs
#include <cstdlib>  // std::abs

namespace reasampler::instrument::ui {

using util::clamp01;
using util::curveFromLevelAt;
using util::curveLevelAt;

namespace {

// Matches envelope_overlay::timeToX. Zero when the area is degenerate (no motion).
double secondsPerPixel(const Rect& area, double totalSeconds) {
    const int w = std::max(0, area.width);
    if (w <= 0 || totalSeconds <= 0.0) return 0.0;
    return totalSeconds / static_cast<double>(w);
}

// The exact inverse of gatePolyline's tapered stage placement: a stage's drawn offset inside its
// slot is slot * timeNormFromSeconds(t), so a pixel delta moves the NORM by dx/slot — never the
// seconds by a fixed rate. Reading the same taper the draw does is what makes a dragged handle
// track the cursor at both ends of the range instead of only near the ceiling.
double gateStageFromPixels(double grabSeconds, const Rect& area, double dxPixels) {
    const double slot = gateStageSlotPx(area);
    if (slot <= 0.0) return grabSeconds;
    return timeSecondsFromNorm(timeNormFromSeconds(grabSeconds) + dxPixels / slot);
}

// Shift's snaps, applied to the resolved param before its clamp so the domain edge always wins.
double snappedSeconds(double seconds, const DragModifiers& m) {
    return m.shift ? snapSecondsToWholeMs(seconds) : seconds;
}
double snappedFraction(double fraction, const DragModifiers& m) {
    return m.shift ? snapFractionToWholePercent(fraction) : fraction;
}
double snappedExponent(double exponent, const DragModifiers& m) {
    return m.shift ? snapExponentToWhole(exponent) : exponent;
}

// Matches envelope_overlay::levelToY (spans height-1 rows for [0,1]).
double levelPerPixel(const Rect& area) {
    const int h = std::max(0, area.height);
    if (h <= 1) return 0.0;
    return 1.0 / static_cast<double>(h - 1);
}

// Origin is a draw-only anchor; so is an AHDSR's ReleaseEnd, which is pinned to the right edge
// (release is dragged from ReleaseStart instead).
bool isDraggable(EnvNode n) {
    switch (n) {
        case EnvNode::Origin:
        case EnvNode::ReleaseEnd:
            return false;
        default:
            return true;
    }
}

// Guards the degenerate baseline's cross-kind vertices, and keeps the sustain-only nodes off an
// AHD. Applied by both the hit-test and the drag resolver.
bool nodeInKind(EnvNode n, EnvKind k) {
    switch (n) {
        case EnvNode::AttackEnd:
        case EnvNode::HoldEnd:
        case EnvNode::DecayEnd:
        case EnvNode::AttackCurve:
        case EnvNode::DecayCurve:
            return true;
        case EnvNode::ReleaseStart:
        case EnvNode::ReleaseCurve:
            return k == EnvKind::Ahdsr;
        case EnvNode::Origin:
        case EnvNode::ReleaseEnd:
            return false;
    }
    return false;
}

// The two endpoint levels of the segment a curve knot shapes. `ok` is false when the segment
// is level (nothing a curve could express), so the drag is a no-op rather than a division.
struct SegmentLevels {
    double start = 0.0;
    double end = 0.0;
    bool ok = false;
};
SegmentLevels segmentLevels(const StageEnvelope& env, EnvNode knot) {
    const double sus = clamp01(env.sustainLevel);
    SegmentLevels s;
    switch (knot) {
        case EnvNode::AttackCurve: s = {0.0, 1.0, true}; break;
        case EnvNode::DecayCurve:
            s = {1.0, env.kind == EnvKind::Ahdsr ? sus : 0.0, true};
            break;
        case EnvNode::ReleaseCurve: s = {sus, 0.0, true}; break;
        default: return s;
    }
    if (s.start == s.end) s.ok = false;
    return s;
}

// The pixel bounds of the segment a curve knot rides, by node — read off the SAME polyline the
// draw built (never re-derived), so the drag's phi can never disagree with knotVtx's.
struct SegmentPixels {
    int x0 = 0;
    int x1 = 0;
    bool ok = false;
};
SegmentPixels segmentPixels(const std::vector<EnvVertex>& poly, EnvNode knot) {
    EnvNode startNode, endNode;
    switch (knot) {
        case EnvNode::AttackCurve:  startNode = EnvNode::Origin;        endNode = EnvNode::AttackEnd; break;
        case EnvNode::DecayCurve:   startNode = EnvNode::HoldEnd;       endNode = EnvNode::DecayEnd;  break;
        case EnvNode::ReleaseCurve: startNode = EnvNode::ReleaseStart;  endNode = EnvNode::ReleaseEnd; break;
        default: return {};
    }
    SegmentPixels s;
    bool haveStart = false, haveEnd = false;
    for (const EnvVertex& v : poly) {
        if (v.node == startNode) { s.x0 = v.x; haveStart = true; }
        else if (v.node == endNode) { s.x1 = v.x; haveEnd = true; }
    }
    s.ok = haveStart && haveEnd;
    return s;
}

// A knot drag: the grab-time level at `phi` (the phi the knot's own drawn x implies — see
// knotPhi) shifted by the pixel delta, read back through curve_law's inverse at that same phi.
double curveFromKnotDrag(const StageEnvelope& grabEnv, EnvNode knot, double grabExponent,
                         double phi, const Rect& area, double dyPixels) {
    const SegmentLevels seg = segmentLevels(grabEnv, knot);
    if (!seg.ok) return grabExponent;
    const double span = seg.end - seg.start;
    // segmentLevels only rejects an EXACTLY level segment; a near-level one (e.g. sustain
    // 0.99) still passes with a tiny divisor here, so one pixel of drag can swing `u` by
    // ~1.0 and saturate the exponent. Floor the magnitude at a couple of pixels' worth of
    // level travel — a segment thinner than that is visually a no-op drag anyway.
    if (std::fabs(span) < 2.0 * levelPerPixel(area)) return grabExponent;
    const double grabLevel = seg.start + span * curveLevelAt(phi, grabExponent);
    const double newLevel = grabLevel - dyPixels * levelPerPixel(area);
    return curveFromLevelAt(phi, (newLevel - seg.start) / span);
}

// An AHD's DecayEnd moves decaySeconds via X, scaled by 1/(1 - holdFraction) — see
// resolveNodeDrag's DecayEnd case. At holdFraction == 1.0 that derivative is exactly 0, so a
// drag there can never change anything; when it ALSO coincides with HoldEnd (decay ~ 0) it is a
// dead handle sitting on top of a live one. Excluded from the grabbable set in that exact case
// only — a functional DecayEnd (holdFraction < 1) stays grabbable even when it coincides.
bool ahdDecayEndIsDead(const StageEnvelope& env, const std::vector<EnvVertex>& poly) {
    if (env.kind != EnvKind::Ahd) return false;
    if (1.0 - clamp01(env.holdFraction) > 1e-9) return false;
    EnvVertex hold, decay;
    bool haveHold = false, haveDecay = false;
    for (const EnvVertex& v : poly) {
        if (v.node == EnvNode::HoldEnd) { hold = v; haveHold = true; }
        else if (v.node == EnvNode::DecayEnd) { decay = v; haveDecay = true; }
    }
    return haveHold && haveDecay && hold.x == decay.x;
}

}  // namespace

NodeHit nodeAtPoint(const StageEnvelope& env, const OverlayArea& area, double totalSeconds,
                    int x, int y) {
    const std::vector<EnvVertex> poly = buildEnvelopePolyline(env, area, totalSeconds);
    const bool dropDeadDecayEnd = ahdDecayEndIsDead(env, poly);
    // Nearest draggable, kind-matching node within the pick radius wins (Chebyshev distance);
    // ties go to the earlier draw-order node. Knots are appended last, so a knot coincident
    // with an endpoint handle loses — a drag there stays a time edit.
    NodeHit best;
    int bestDist = kNodeGrabRadius + 1;
    for (const EnvVertex& v : poly) {
        if (!isDraggable(v.node) || !nodeInKind(v.node, env.kind)) continue;
        if (dropDeadDecayEnd && v.node == EnvNode::DecayEnd) continue;
        const int dist = std::max(std::abs(x - v.x), std::abs(y - v.y));
        if (dist < bestDist) {  // strict-less-than keeps ties at the earlier draw order
            bestDist = dist;
            best = NodeHit{true, v.node};
        }
    }
    return best;
}

StageEnvelope resolveNodeDrag(const StageEnvelope& grabEnv, EnvNode node, const OverlayArea& area,
                              double totalSeconds, const EnvClampBounds& bounds,
                              int dxPixels, int dyPixels, const DragModifiers& mods) {
    StageEnvelope out = grabEnv;
    if (!isDraggable(node) || !nodeInKind(node, grabEnv.kind)) return out;

    const Rect& rect = area.rect;
    const double secPerPx = secondsPerPixel(rect, totalSeconds);
    if (secPerPx <= 0.0) return out;  // degenerate area / duration — no motion
    // Fine drag scales the PIXEL delta, so it composes with every axis below (the tapered
    // schematic, the 1:1 wall clock, the level and the exponent) without a second rule.
    const double scale = fineDrag(mods) ? kFineDragScale : 1.0;
    const double dx = static_cast<double>(dxPixels) * scale;
    const double dy = static_cast<double>(dyPixels) * scale;
    const double dSec = dx * secPerPx;

    // A curve knot's phi is read off the same polyline knotVtx drew, so the drag inverts the
    // exact phi the knot is sitting at rather than assuming the segment midpoint.
    double curvePhi = 0.5;
    if (node == EnvNode::AttackCurve || node == EnvNode::DecayCurve ||
        node == EnvNode::ReleaseCurve) {
        const std::vector<EnvVertex> poly = buildEnvelopePolyline(grabEnv, area, totalSeconds);
        const SegmentPixels sp = segmentPixels(poly, node);
        if (sp.ok) curvePhi = knotPhi(sp.x0, sp.x1);
    }

    if (grabEnv.kind == EnvKind::Ahdsr) {
        switch (node) {
            // Each cumulative-time node edits its own segment duration. Non-negative durations
            // ARE the monotonic-in-time guarantee (a segment can never go negative, so a node
            // can never cross a neighbour) — the [0, max] clamp is the whole constraint.
            case EnvNode::AttackEnd:
                out.attackSeconds = std::clamp(
                    snappedSeconds(gateStageFromPixels(grabEnv.attackSeconds, rect, dx), mods), 0.0,
                    bounds.maxAttackSeconds);
                break;
            case EnvNode::HoldEnd:
                out.holdSeconds = std::clamp(
                    snappedSeconds(gateStageFromPixels(grabEnv.holdSeconds, rect, dx), mods), 0.0,
                    bounds.maxHoldSeconds);
                break;
            case EnvNode::DecayEnd: {
                // X sets decay time, Y sets sustain level (drag down = higher y = lower level).
                out.decaySeconds = std::clamp(
                    snappedSeconds(gateStageFromPixels(grabEnv.decaySeconds, rect, dx), mods), 0.0,
                    bounds.maxDecaySeconds);
                const double dLevel = -dy * levelPerPixel(rect);
                out.sustainLevel =
                    std::clamp(snappedFraction(grabEnv.sustainLevel + dLevel, mods), 0.0, 1.0);
                break;
            }
            case EnvNode::ReleaseStart:
                // The release runs from this node to the anchored right edge, so dragging LEFT
                // (negative dx) lengthens it — the delta enters with the opposite sign.
                out.releaseSeconds = std::clamp(
                    snappedSeconds(gateStageFromPixels(grabEnv.releaseSeconds, rect, -dx), mods), 0.0,
                    bounds.maxReleaseSeconds);
                break;
            case EnvNode::AttackCurve:
                out.attackCurve = snappedExponent(
                    curveFromKnotDrag(grabEnv, node, grabEnv.attackCurve, curvePhi, rect, dy), mods);
                break;
            case EnvNode::DecayCurve:
                out.decayCurve = snappedExponent(
                    curveFromKnotDrag(grabEnv, node, grabEnv.decayCurve, curvePhi, rect, dy), mods);
                break;
            case EnvNode::ReleaseCurve:
                out.releaseCurve = snappedExponent(
                    curveFromKnotDrag(grabEnv, node, grabEnv.releaseCurve, curvePhi, rect, dy), mods);
                break;
            default:
                break;
        }
        return out;
    }

    // AHD: the x-axis is the waveform's own, so a stage node moves at 1:1 wall-clock scale.
    const AhdSplit s = splitAhdSeconds(grabEnv);
    switch (node) {
        case EnvNode::AttackEnd:
            out.attackSeconds = std::clamp(snappedSeconds(grabEnv.attackSeconds + dSec, mods), 0.0,
                                           bounds.maxAttackSeconds);
            break;
        case EnvNode::HoldEnd: {
            // Hold is a fraction of what attack and decay left, so the node's pixel motion
            // converts through that remainder. A zero remainder leaves nothing to divide by and
            // nothing the drag could express.
            const double rem = std::max(0.0, grabEnv.spanSeconds) - s.attack - s.decay;
            if (rem <= 0.0) break;
            out.holdFraction = clamp01(snappedFraction((s.hold + dSec) / rem, mods));
            break;
        }
        case EnvNode::DecayEnd: {
            // DecayEnd is DRAWN at t0 + total, and total = attack + decay + (span-attack-decay)
            // * holdFraction, so d(total)/d(decay) = 1 - holdFraction: Hold eats a holdFraction
            // share of whatever decay gives up. Scaling by 1/(1-frac) makes the drawn endpoint
            // track the cursor 1:1, matching every other node. At frac == 1.0 (the Trigger
            // default) Hold consumes the WHOLE remainder regardless of decay's value, so the
            // derivative is exactly 0 — no scale recovers motion there, and decaySeconds is left
            // unchanged rather than divided by zero.
            const double denom = 1.0 - clamp01(grabEnv.holdFraction);
            if (denom > 1e-9) {
                out.decaySeconds = std::clamp(snappedSeconds(grabEnv.decaySeconds + dSec / denom, mods),
                                              0.0, bounds.maxDecaySeconds);
            }
            break;
        }
        case EnvNode::AttackCurve:
            out.attackCurve = snappedExponent(
                curveFromKnotDrag(grabEnv, node, grabEnv.attackCurve, curvePhi, rect, dy), mods);
            break;
        case EnvNode::DecayCurve:
            out.decayCurve = snappedExponent(
                curveFromKnotDrag(grabEnv, node, grabEnv.decayCurve, curvePhi, rect, dy), mods);
            break;
        default:
            break;
    }
    return out;
}

}  // namespace reasampler::instrument::ui
