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

#include "envelope_overlay.h"

#include <algorithm>

namespace reasampler::vst {

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.left;
    if (t < 0.0) t = 0.0;
    // Linear map, NOT clamped on the high side: t past totalSeconds maps past area.right (the Gate
    // release tail, drawn after the sample end by design). Round to the nearest pixel.
    const double frac = t / totalSeconds;
    const long xi = static_cast<long>(frac * static_cast<double>(w) + 0.5);
    return area.left + static_cast<int>(xi);
}

int levelToY(const Rect& area, double level) {
    const int h = std::max(0, area.height());
    if (h <= 0) return area.top;
    if (level < 0.0) level = 0.0;
    if (level > 1.0) level = 1.0;
    // Level 1 -> top row, level 0 -> bottom row (bottom-1 under the half-open convention). The
    // range spans (h-1) pixels 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.top + static_cast<int>(dy);
}

namespace {

double clamp01(double v) {
    if (v < 0.0) return 0.0;
    if (v > 1.0) return 1.0;
    return v;
}

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

std::vector<EnvVertex> gatePolyline(const AmpEnvelope& env, const Rect& area, double totalSeconds) {
    // Non-negative segment durations (a stored negative would be an upstream bug; clamp defensively).
    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);

    // Cumulative wall-clock times of each breakpoint from t=0.
    const double tAttack = a;
    const double tHold   = tAttack + h;
    const double tDecay  = tHold + d;
    // The sustain plateau runs to the sample end; if the pre-sustain stages already overrun the
    // sample, the plateau collapses to zero width (its end clamps up to tDecay).
    const double tSustainEnd = std::max(tDecay, totalSeconds);
    const double tRelease    = tSustainEnd + r;  // release trails PAST the sample end, by design

    std::vector<EnvVertex> pts;
    pts.reserve(6);
    pts.push_back(vtx(EnvNode::Origin,       area, totalSeconds, 0.0,         0.0));
    pts.push_back(vtx(EnvNode::AttackEnd,    area, totalSeconds, tAttack,     1.0));
    pts.push_back(vtx(EnvNode::HoldEnd,      area, totalSeconds, tHold,       1.0));
    pts.push_back(vtx(EnvNode::DecayEnd,     area, totalSeconds, tDecay,      sus));  // sustain node
    pts.push_back(vtx(EnvNode::ReleaseStart, area, totalSeconds, tSustainEnd, sus));  // plateau end
    pts.push_back(vtx(EnvNode::ReleaseEnd,   area, totalSeconds, tRelease,    0.0));
    return pts;
}

std::vector<EnvVertex> triggerPolyline(const AmpEnvelope& env, const Rect& area,
                                       double totalSeconds) {
    // The 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: clamp so fadeIn + fadeOut <= 1 (of the played span), mirroring the
    // engine's TriggerParams clamp. Trim the LATER fade (fade-out) first, matching the engine.
    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 Rect& area,
                                             double totalSeconds) {
    if (area.width() <= 0 || area.height() <= 0 || totalSeconds <= 0.0) {
        // Degenerate surface: a two-point flat baseline at level 0 so the shell always has a line.
        return {vtx(EnvNode::Origin, area, 1.0, 0.0, 0.0),
                vtx(EnvNode::ReleaseEnd, area, 1.0, 1.0, 0.0)};
    }
    return env.mode == EnvMode::Gate ? gatePolyline(env, area, totalSeconds)
                                     : triggerPolyline(env, area, totalSeconds);
}

}  // namespace reasampler::vst
