// 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;

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);
}

int gateTimedWidth(const Rect& area) {
    const int w = std::max(0, area.width);
    if (w <= 0) return 0;
    const int sustainPx =
        static_cast<int>(kGateSustainDisplayFraction * static_cast<double>(w) + 0.5);
    return std::max(1, w - sustainPx);
}

double gatePxPerSecond(const Rect& area) {
    const int timedW = gateTimedWidth(area);
    if (timedW <= 0) return 0.0;
    // Minus the four per-segment separation bases and the last in-bounds column, floored at 1.
    const double usable =
        std::max(1.0, static_cast<double>(timedW - 1 - 4 * kGateNodeSepPx));
    return usable / (4.0 * kGateStageMaxSeconds);
}

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);
}

namespace {

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

// Gate works in px space (timed px + the fixed sustain-plateau reserve) rather than the plain
// timeToX map; clamps in double space before the int cast for the same overflow reason as above.
EnvVertex gateVtx(EnvNode node, const Rect& area, double px, double level) {
    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;
    return v;
}

std::vector<EnvVertex> gatePolyline(const AmpEnvelope& 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);

    // A/H/D/R map onto the timed region at the param-domain scale, each segment getting a
    // kGateNodeSepPx base so nodes never coincide even at the tier-0 zero-hold/zero-decay
    // defaults. The sustain plateau is the fixed reserve between DecayEnd and ReleaseStart.
    const int W = std::max(1, area.width);
    const double sustainPx = static_cast<double>(W - gateTimedWidth(area));
    const double sep = static_cast<double>(kGateNodeSepPx);
    const double pps = gatePxPerSecond(area);

    double xAttack  = sep + a * pps;              // AttackEnd
    double xHold    = xAttack + sep + h * pps;    // HoldEnd
    double xDecay   = xHold + sep + d * pps;      // DecayEnd (sustain node)
    double xPlateau = xDecay + sustainPx;         // ReleaseStart (schematic note-off)
    double xRelease = xPlateau + sep + r * pps;   // ReleaseEnd

    // Overrun beyond the schematic domain compresses from the right, preserving minimum gaps so
    // trailing nodes stay separated instead of piling on the last column. This re-floor only
    // bites when the canvas is too narrow to hold the gaps at all — gateVtx's clamp wins then.
    const double xMax = static_cast<double>(W - 1);
    if (xRelease > xMax) {
        xRelease = xMax;
        xPlateau = std::min(xPlateau, xRelease - sep);
        xDecay   = std::min(xDecay, xPlateau - sustainPx);
        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 + sustainPx);
        xRelease = std::max(xRelease, xPlateau + sep);
    }

    std::vector<EnvVertex> pts;
    pts.reserve(6);
    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));
    return pts;
}

std::vector<EnvVertex> triggerPolyline(const AmpEnvelope& env, const Rect& area,
                                       double totalSeconds) {
    // Played span is lengthFraction of the whole sample; fades are fractions of that span.
    const double len   = clamp01(env.lengthFraction);
    double fadeIn      = clamp01(env.fadeInFraction);
    double fadeOut     = clamp01(env.fadeOutFraction);
    // Fades cannot overlap; trim fade-out first, matching the engine's TriggerParams clamp.
    if (fadeIn + fadeOut > 1.0) fadeOut = std::max(0.0, 1.0 - fadeIn);

    const double playSeconds  = len * totalSeconds;
    const double tFadeInEnd   = fadeIn * playSeconds;
    const double tFadeOutStart = playSeconds - fadeOut * playSeconds;  // where fade-out begins

    std::vector<EnvVertex> pts;
    pts.reserve(4);
    pts.push_back(vtx(EnvNode::Origin,       area, totalSeconds, 0.0,           0.0));
    pts.push_back(vtx(EnvNode::FadeInEnd,    area, totalSeconds, tFadeInEnd,    1.0));
    pts.push_back(vtx(EnvNode::FadeOutStart, area, totalSeconds, tFadeOutStart, 1.0));  // unity end
    pts.push_back(vtx(EnvNode::LengthEnd,    area, totalSeconds, playSeconds,   0.0));  // playEnd
    return pts;
}

}  // namespace

std::vector<EnvVertex> buildEnvelopePolyline(const AmpEnvelope& 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.mode == EnvMode::Gate ? gatePolyline(env, rect)
                                     : triggerPolyline(env, rect, totalSeconds);
}

}  // namespace reasampler::instrument::ui
