// envelope_overlay.cpp — see envelope_overlay.h. Pure geometry; no host types.

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

#include "core/util/clamp01.h"

#include <algorithm>

namespace reasampler::instrument::ui {

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

int timeToX(const Rect& area, double totalSeconds, double t) {
    const int w = std::max(0, area.width);
    if (w <= 0 || totalSeconds <= 0.0) return area.x;
    if (t < 0.0) t = 0.0;
    // Clamp in double space before the int cast — a huge t would overflow a 32-bit long
    // (Windows) and wrap to the wrong edge.
    double px = (t / totalSeconds) * static_cast<double>(w);
    if (px > static_cast<double>(w - 1)) px = static_cast<double>(w - 1);
    return area.x + static_cast<int>(px + 0.5);
}

double gateStageSlotPx(const Rect& area) {
    const int w = std::max(0, area.width);
    if (w <= 0) return 0.0;
    // The four timed stages share the canvas minus their four separation bases and the last
    // in-bounds column; whatever they leave IS the sustain plateau, which is why a zero release
    // puts the plateau's end one separation short of the right edge rather than a fixed
    // fraction of the way across.
    const double usable = std::max(1.0, static_cast<double>(w - 1 - 4 * kGateNodeSepPx));
    return usable / 4.0;
}

int levelToY(const Rect& area, double level) {
    const int h = std::max(0, area.height);
    if (h <= 0) return area.y;
    if (level < 0.0) level = 0.0;
    if (level > 1.0) level = 1.0;
    // Level 1 -> top row, level 0 -> bottom row; spans (h-1) px so both endpoints land on a
    // drawable row.
    const int span = h - 1;
    const long dy = static_cast<long>((1.0 - level) * static_cast<double>(span) + 0.5);
    return area.y + static_cast<int>(dy);
}

double knotPhi(int x0, int x1) {
    if (x1 == x0) return 0.5;
    const int mid = (x0 + x1) / 2;
    return static_cast<double>(mid - x0) / static_cast<double>(x1 - x0);
}

AhdSplit splitAhdSeconds(const StageEnvelope& env) {
    AhdSplit out;
    const double span = std::max(0.0, env.spanSeconds);
    double a = std::max(0.0, env.attackSeconds);
    if (a > span) a = span;
    double d = std::max(0.0, env.decaySeconds);
    if (d > span - a) d = span - a;
    const double remaining = span - a - d;
    out.attack = a;
    out.decay = d;
    out.hold = remaining * clamp01(env.holdFraction);
    out.total = out.attack + out.hold + out.decay;
    return out;
}

namespace {

EnvVertex vtx(EnvNode node, const Rect& area, double totalSeconds, double t, double level,
              bool knot = false) {
    EnvVertex v;
    v.node = node;
    v.x = timeToX(area, totalSeconds, t);
    v.y = levelToY(area, level);
    v.level = level;
    v.knot = knot;
    return v;
}

// The AHDSR schematic works in px space rather than the plain timeToX map; clamps in double
// space before the int cast for the same overflow reason as timeToX.
EnvVertex gateVtx(EnvNode node, const Rect& area, double px, double level, bool knot = false) {
    const int w = std::max(1, area.width);
    if (px < 0.0) px = 0.0;
    if (px > static_cast<double>(w - 1)) px = static_cast<double>(w - 1);
    EnvVertex v;
    v.node = node;
    v.x = area.x + static_cast<int>(px + 0.5);
    v.y = levelToY(area, level);
    v.level = level;
    v.knot = knot;
    return v;
}

// The knot for a segment running from `startLevel` to `endLevel`, placed at the segment's
// pixel midpoint. Its level is read at the phi that midpoint's TRUNCATED x actually implies
// (knotPhi), not always phi = 0.5 — an odd-pixel span would otherwise draw the knot a half
// pixel off the curve its own vertices trace. curve_law.h owns the knot/dial pairing.
EnvVertex knotVtx(EnvNode node, const Rect& area, int x0, int x1, double startLevel,
                  double endLevel, double exponent) {
    EnvVertex v;
    v.node = node;
    v.x = (x0 + x1) / 2;
    const double u = curveLevelAt(knotPhi(x0, x1), exponent);
    const double level = startLevel + (endLevel - startLevel) * u;
    v.y = levelToY(area, level);
    v.level = level;
    v.knot = true;
    return v;
}

std::vector<EnvVertex> gatePolyline(const StageEnvelope& env, const Rect& area) {
    // Clamp defensively — a stored negative duration would be an upstream bug.
    const double a = std::max(0.0, env.attackSeconds);
    const double h = std::max(0.0, env.holdSeconds);
    const double d = std::max(0.0, env.decaySeconds);
    const double r = std::max(0.0, env.releaseSeconds);
    const double sus = clamp01(env.sustainLevel);

    const int W = std::max(1, area.width);
    const double sep = static_cast<double>(kGateNodeSepPx);
    const double slot = gateStageSlotPx(area);
    const double xMax = static_cast<double>(W - 1);

    // The release ANCHORS to the right edge: ReleaseEnd is the canvas edge and ReleaseStart —
    // the sustain->release join, and the node the user drags — sits a release-length to its
    // left. Everything the release does not take is the sustain plateau, so a zero release
    // leaves the plateau running to within one separation of the edge.
    double xAttack  = sep + slot * timeNormFromSeconds(a);
    double xHold    = xAttack + sep + slot * timeNormFromSeconds(h);
    double xDecay   = xHold + sep + slot * timeNormFromSeconds(d);
    double xPlateau = xMax - sep - slot * timeNormFromSeconds(r);
    const double xRelease = xMax;

    // Keep every node separated when the four stages together would overrun the canvas: the
    // plateau holds its minimum gap from the edge, then the A/H/D chain compresses from the
    // right and re-floors from the left. This only bites at the domain's extremes; gateVtx's
    // own clamp wins on a canvas too narrow to hold the gaps at all.
    if (xPlateau < xDecay + sep) {
        if (xPlateau < 4.0 * sep) xPlateau = 4.0 * sep;
        xDecay  = std::min(xDecay, xPlateau - sep);
        xHold   = std::min(xHold, xDecay - sep);
        xAttack = std::min(xAttack, xHold - sep);
        xAttack = std::max(xAttack, sep);
        xHold   = std::max(xHold, xAttack + sep);
        xDecay  = std::max(xDecay, xHold + sep);
        xPlateau = std::max(xPlateau, xDecay + sep);
    }

    std::vector<EnvVertex> pts;
    pts.reserve(9);
    pts.push_back(gateVtx(EnvNode::Origin,       area, 0.0,      0.0));
    pts.push_back(gateVtx(EnvNode::AttackEnd,    area, xAttack,  1.0));
    pts.push_back(gateVtx(EnvNode::HoldEnd,      area, xHold,    1.0));
    pts.push_back(gateVtx(EnvNode::DecayEnd,     area, xDecay,   sus));  // sustain node
    pts.push_back(gateVtx(EnvNode::ReleaseStart, area, xPlateau, sus));  // plateau end
    pts.push_back(gateVtx(EnvNode::ReleaseEnd,   area, xRelease, 0.0));  // anchored

    // Knots ride only SLOPED stages that actually have a duration — a zero-length stage has no
    // interior to place a handle in, and one there would collide with its own endpoints.
    if (a > 0.0) {
        pts.push_back(knotVtx(EnvNode::AttackCurve, area, pts[0].x, pts[1].x, 0.0, 1.0,
                              env.attackCurve));
    }
    if (d > 0.0) {
        pts.push_back(knotVtx(EnvNode::DecayCurve, area, pts[2].x, pts[3].x, 1.0, sus,
                              env.decayCurve));
    }
    if (r > 0.0) {
        pts.push_back(knotVtx(EnvNode::ReleaseCurve, area, pts[4].x, pts[5].x, sus, 0.0,
                              env.releaseCurve));
    }
    return pts;
}

std::vector<EnvVertex> ahdPolyline(const StageEnvelope& env, const Rect& area,
                                   double totalSeconds) {
    const AhdSplit s = splitAhdSeconds(env);
    const double t0 = std::max(0.0, env.originSeconds);

    std::vector<EnvVertex> pts;
    pts.reserve(6);
    pts.push_back(vtx(EnvNode::Origin,    area, totalSeconds, t0, 0.0));
    pts.push_back(vtx(EnvNode::AttackEnd, area, totalSeconds, t0 + s.attack, 1.0));
    pts.push_back(vtx(EnvNode::HoldEnd,   area, totalSeconds, t0 + s.attack + s.hold, 1.0));
    // DecayEnd is drawn at t0 + total, which coincides with HoldEnd exactly when decay ~ 0 —
    // independent of holdFraction (total = attack + hold + decay always). Left at its true
    // instant rather than nudged: the 1:1 axis this policy exists to keep honest must hold even
    // at a shared instant, including the Trigger default's abrupt (zero-decay) cutoff.
    // envelope_edit's nodeAtPoint handles the coincidence instead, by dropping DecayEnd from the
    // grabbable set when it also cannot move (holdFraction == 1.0).
    pts.push_back(vtx(EnvNode::DecayEnd,  area, totalSeconds, t0 + s.total, 0.0));
    if (s.attack > 0.0) {
        pts.push_back(knotVtx(EnvNode::AttackCurve, area, pts[0].x, pts[1].x, 0.0, 1.0,
                              env.attackCurve));
    }
    if (s.decay > 0.0) {
        pts.push_back(knotVtx(EnvNode::DecayCurve, area, pts[2].x, pts[3].x, 1.0, 0.0,
                              env.decayCurve));
    }
    return pts;
}

}  // namespace

std::vector<EnvVertex> buildEnvelopePolyline(const StageEnvelope& env, const OverlayArea& area,
                                             double totalSeconds) {
    const Rect& rect = area.rect;
    if (rect.width <= 0 || rect.height <= 0 || totalSeconds <= 0.0) {
        // Degenerate surface: flat two-point baseline so the shell always has a line.
        return {vtx(EnvNode::Origin, rect, 1.0, 0.0, 0.0),
                vtx(EnvNode::ReleaseEnd, rect, 1.0, 1.0, 0.0)};
    }
    return env.kind == EnvKind::Ahdsr ? gatePolyline(env, rect)
                                      : ahdPolyline(env, rect, totalSeconds);
}

}  // namespace reasampler::instrument::ui
