#pragma once
// envelopes.h — the three per-frame envelope evaluators (AHDSR amplitude, sustain-less AHD,
// AHD pitch offset). Concrete classes, every body defined in-class: these are called
// per-voice-per-sample from Voice::advanceFrame, so they must inline into the render loop.
// NEVER give them a common base or a virtual tick() — that vtable lands on the hottest
// inner loop in the program (root CLAUDE.md, structural heuristic 3).

#include <cmath>
#include <cstdint>

#include "core/instrument/engine/play_params.h"
#include "core/util/curve_law.h"

namespace reasampler {

using util::curveMap;

// The A/H/D split of a bounded span, in frames.
struct AhdSpan {
    std::int64_t attack = 0;
    std::int64_t hold   = 0;
    std::int64_t decay  = 0;
    std::int64_t total  = 0;  // attack + hold + decay; <= span by construction
};

// THE span split, shared by every sustain-less envelope so they cannot disagree about where a
// stage boundary is. Attack takes at most the whole span and Decay at most what Attack left,
// so `remaining` is non-negative without a clamp; Hold then takes its FRACTION of that
// remainder, which is why total <= span holds for every (attack, decay, fraction) triple and
// there is no sum to clamp. The two per-stage mins reproduce the retired Trigger fade clamp
// exactly (head first, tail into what is left), so a migrated instance keeps its stage lengths.
inline AhdSpan fitAhd(std::int64_t spanFrames, const AhdParams& p) {
    AhdSpan out;
    const std::int64_t span = spanFrames > 0 ? spanFrames : 0;
    std::int64_t a = p.attackFrames > 0 ? p.attackFrames : 0;
    if (a > span) a = span;
    std::int64_t d = p.decayFrames > 0 ? p.decayFrames : 0;
    if (d > span - a) d = span - a;
    const std::int64_t remaining = span - a - d;
    double frac = p.holdFraction;
    if (!(frac > 0.0)) frac = 0.0;  // also catches NaN
    if (frac > 1.0) frac = 1.0;
    out.attack = a;
    out.decay = d;
    out.hold = static_cast<std::int64_t>(static_cast<double>(remaining) * frac + 0.5);
    out.total = out.attack + out.hold + out.decay;
    return out;
}

// The AHD's normalized level at `offset` frames into the span: 0 -> 1 over attack, flat 1
// across hold, 1 -> 0 over decay, 0 outside. Pure over the offset so both the ticking pitch
// envelope and the positional amplitude one read one shape.
inline double ahdLevelAt(double offset, const AhdSpan& s, double attackCurve,
                         double decayCurve) {
    if (offset < 0.0 || offset >= static_cast<double>(s.total)) return 0.0;
    if (s.attack > 0 && offset < static_cast<double>(s.attack)) {
        return curveMap(offset / static_cast<double>(s.attack), attackCurve);
    }
    const double decayStart = static_cast<double>(s.total - s.decay);
    if (s.decay > 0 && offset >= decayStart) {
        double t = (offset - decayStart) / static_cast<double>(s.decay);
        if (t > 1.0) t = 1.0;
        return 1.0 - curveMap(t, decayCurve);
    }
    return 1.0;
}

// Absorbs a step a live parameter move would otherwise put straight into an evaluator's
// output, as an offset that decays to EXACTLY zero — so the at-rest path carries no residue
// and the smoother's own branch stays predictably false. Per-frame decay rather than a
// wall-clock one, matching the voice's takeover declick; the floor is far below both domains
// this is used in (amplitude, and semitones of pitch offset).
class StepSmoother {
public:
    // `step` is (level before the change - level after it): adding it back reproduces the
    // pre-change output exactly on the first frame.
    void absorb(double step) {
        offset_ += step;
        active_ = (offset_ > kFloor || offset_ < -kFloor);
        if (!active_) offset_ = 0.0;
    }
    void clear() { offset_ = 0.0; active_ = false; }
    bool active() const { return active_; }

    // This frame's offset; decays afterwards, latching inactive at the floor.
    double advance() {
        const double out = offset_;
        offset_ *= kDecay;
        if (offset_ < kFloor && offset_ > -kFloor) {
            offset_ = 0.0;
            active_ = false;
        }
        return out;
    }

private:
    static constexpr double kDecay = 0.95;
    static constexpr double kFloor = 1e-5;
    double offset_ = 0.0;
    bool active_ = false;
};

// AHDSR amplitude envelope, sample-based (times in frames), linear segments. A gate:
// noteOn() enters Attack; noteOff() enters Release from wherever it is.
//
// Segment math:
//   Attack:  0 -> 1 over attackFrames
//   Hold:    hold 1 over holdFrames
//   Decay:   1 -> sustainLevel over decayFrames
//   Sustain: hold sustainLevel until noteOff
//   Release: currentLevel -> 0 over releaseFrames
// A zero-length attack jumps straight to 1 on the first frame; holdFrames == 0 skips Hold
// entirely (the pre-hold-stage ADSR, back-compat); zero decay jumps to sustain; a noteOff
// during attack/hold/decay releases from the current partial level, not from sustainLevel.
//
// stagePos_ is the elapsed position within the current stage. It is a double rather than a
// frame count only so applyLive can hold a fractional normalized position; every value it
// takes on the un-edited path is integral, so the segment math is bit-identical to the
// integer-counter engine.
class AdsrEnvelope {
public:
    enum class Stage { Idle, Attack, Hold, Decay, Sustain, Release, Finished };

    void configure(const AdsrParams& params) { params_ = params; }

    // Gate on: (re)start from Attack.
    void noteOn() {
        stage_ = Stage::Attack;
        level_ = 0.0;
        stagePos_ = 0.0;
        smooth_.clear();
    }

    // Gate off: enter Release from the CURRENT level — release-before-sustain releases from
    // the partial attack/decay level, not from sustainLevel. A running smoother deliberately
    // survives: it is mid-glide, and cutting it here would reintroduce the step it absorbed.
    void noteOff() {
        if (stage_ == Stage::Idle || stage_ == Stage::Finished || stage_ == Stage::Release) {
            return;  // already released / not sounding.
        }
        releaseFrom_ = level_;
        stage_ = Stage::Release;
        stagePos_ = 0.0;
    }

    // Live parameter delivery to a fresh voice — one that has NOT yet rendered a frame, whose
    // latched copy may predate the newest edit. It takes the params outright: there is no
    // phase to hold and nothing to be continuous with. applyLive cannot serve here in either
    // direction — with a stale duration of 0 its phi rule reads stagePos_ == 0 as a COMPLETED
    // stage and discards the newly-dialled time, and with a stale duration > 0 against a new 0
    // it absorbs a full-scale step into a voice that has emitted nothing, fading the onset in.
    void snapLive(const AdsrParams& params) {
        params_ = params;
        smooth_.clear();
    }

    // Live parameter delivery to a SOUNDING voice. The mid-stage rule is HOLD NORMALIZED
    // STAGE POSITION: phi = elapsed/duration is kept fixed across the change, so this frame's
    // level is unchanged by construction and the remainder of the stage takes its share of the
    // newly-dialled duration. The rule is expressed over normalized position, never over
    // output level, so a per-segment curve exponent composes with it as a pure map of phi.
    //
    // Two cases phi cannot cover, both absorbed by the smoother rather than allowed to step:
    // a sustain level moved while the voice holds it (sustain is a level, not a timed stage),
    // and a stage duration dialled to exactly zero mid-stage (the stage ceases to exist and
    // completes at its terminal level).
    void applyLive(const AdsrParams& params) {
        const double before = stageLevel(params_);
        const double oldDuration = stageDuration(params_);
        const double newDuration = stageDuration(params);
        if (newDuration > 0.0) {
            stagePos_ = (oldDuration > 0.0) ? stagePos_ * (newDuration / oldDuration)
                                            : newDuration;  // a collapsed stage was complete
        }
        params_ = params;
        const double after = stageLevel(params_);
        if (after != before) smooth_.absorb(before - after);
    }

    // Advances one frame and returns the amplitude for THIS frame (before advancing).
    // Once Release completes the envelope latches Finished and returns 0.0 forever (until
    // the next noteOn). A single, monotonic per-frame step — the caller pulls one value per
    // output frame.
    //
    // While the smoother runs the return may sit OUTSIDE [0,1] by the offset it is decaying
    // (bounded by the step it absorbed). finished() ignores that residue, so a Release that
    // completes with an offset still decaying is hard-cut when the voice frees — the audible
    // remainder of a step the smoother had already taken most of.
    double tick() {
        const double out = tickStage();
        return smooth_.active() ? out + smooth_.advance() : out;
    }

    Stage stage() const { return stage_; }
    bool finished() const { return stage_ == Stage::Finished; }
    double level() const { return level_; }

private:
    // The level tick() would emit right now under `params` without advancing anything. THE one
    // home for every segment's shape: tickStage owns only the advance and the stage
    // transitions and reads its output from here, so a per-segment curve added later lands in
    // one place and the smoother can never size a step against a different curve than the
    // output takes.
    double stageLevel(const AdsrParams& params) const {
        switch (stage_) {
            case Stage::Attack: {
                if (params.attackFrames <= 0) return 1.0;
                double l = stagePos_ / static_cast<double>(params.attackFrames);
                if (l > 1.0) l = 1.0;
                return curveMap(l, params.attackCurve);
            }
            case Stage::Hold:
                // A zero-length hold falls straight through to Decay on the next tick, whose
                // level at position 0 is 1.0 — unless decay is zero too, which lands on sustain.
                if (params.holdFrames > 0) return 1.0;
                return (params.decayFrames <= 0) ? params.sustainLevel : 1.0;
            case Stage::Decay: {
                if (params.decayFrames <= 0) return params.sustainLevel;
                double t = stagePos_ / static_cast<double>(params.decayFrames);
                if (t > 1.0) t = 1.0;  // never bites on the un-edited path (transitions at >=)
                return 1.0 + (params.sustainLevel - 1.0) * curveMap(t, params.decayCurve);
            }
            case Stage::Sustain:
                return params.sustainLevel;
            case Stage::Release: {
                if (params.releaseFrames <= 0) return 0.0;
                double t = stagePos_ / static_cast<double>(params.releaseFrames);
                if (t > 1.0) t = 1.0;
                return releaseFrom_ * (1.0 - curveMap(t, params.releaseCurve));
            }
            default:
                return 0.0;
        }
    }

    // The current stage's dialled duration under `params`; 0 for the untimed stages.
    double stageDuration(const AdsrParams& params) const {
        switch (stage_) {
            case Stage::Attack:  return static_cast<double>(params.attackFrames);
            case Stage::Hold:    return static_cast<double>(params.holdFrames);
            case Stage::Decay:   return static_cast<double>(params.decayFrames);
            case Stage::Release: return static_cast<double>(params.releaseFrames);
            default:             return 0.0;
        }
    }

    double tickStage() {
        switch (stage_) {
            case Stage::Idle:
            case Stage::Finished:
                level_ = 0.0;
                return 0.0;

            case Stage::Attack: {
                level_ = stageLevel(params_);
                const double out = level_;
                stagePos_ += 1.0;
                if (stagePos_ >= static_cast<double>(params_.attackFrames)) {
                    // holdFrames == 0 falls straight through Hold on the next tick to Decay.
                    stage_ = Stage::Hold;
                    stagePos_ = 0.0;
                    level_ = 1.0;
                }
                return out;
            }

            case Stage::Hold: {
                // holdFrames <= 0 leaves the stage on this same tick (no frame consumed at
                // 1.0 beyond what Attack already emitted) so a zero-length hold emits no
                // extra sample.
                if (params_.holdFrames <= 0) {
                    stage_ = Stage::Decay;
                    stagePos_ = 0.0;
                    level_ = 1.0;
                    // Single re-dispatch into Decay (bounded: Hold->Decay only, not general
                    // recursion). Re-enters the STAGE evaluator, never tick(), so a running
                    // smoother is applied exactly once per frame.
                    return tickStage();
                }
                level_ = stageLevel(params_);
                const double out = level_;
                stagePos_ += 1.0;
                if (stagePos_ >= static_cast<double>(params_.holdFrames)) {
                    stage_ = Stage::Decay;
                    stagePos_ = 0.0;
                    level_ = 1.0;
                }
                return out;
            }

            case Stage::Decay: {
                level_ = stageLevel(params_);
                const double out = level_;
                stagePos_ += 1.0;
                if (stagePos_ >= static_cast<double>(params_.decayFrames)) {
                    stage_ = Stage::Sustain;
                    stagePos_ = 0.0;
                    level_ = params_.sustainLevel;
                }
                return out;
            }

            case Stage::Sustain:
                level_ = stageLevel(params_);
                return level_;

            case Stage::Release: {
                if (params_.releaseFrames <= 0) {
                    level_ = 0.0;
                    stage_ = Stage::Finished;
                    return 0.0;
                }
                level_ = stageLevel(params_);
                const double out = level_;
                stagePos_ += 1.0;
                if (stagePos_ >= static_cast<double>(params_.releaseFrames)) {
                    stage_ = Stage::Finished;
                    level_ = 0.0;
                }
                return out;
            }
        }
        return 0.0;  // unreachable; silences a warning.
    }

    AdsrParams params_;
    Stage stage_ = Stage::Idle;
    double level_ = 0.0;
    double stagePos_ = 0.0;
    double releaseFrom_ = 0.0;  // level at the moment noteOff() was called
    StepSmoother smooth_;
};


// The sustain-less AHD amplitude shape, evaluated at a source-frame offset into the span
// rather than by ticking output frames: under Varispeed a transposed voice consumes source
// faster than output, so driving the shape off the read position keeps every stage boundary on
// the same source frames regardless of engine. Note-off-immune and time-boxed by the span.
//
// Positional means there is no phase counter to hold across a live edit, so the phi rule
// AdsrEnvelope applies has nothing to act on here; a live reshape is a level step, absorbed by
// the same bounded smoother.
class AhdEnvelope {
public:
    // `spanFrames` is the bound the stages are fitted into — (playEnd - startFrame) for the
    // Trigger amp and filter envelopes. A zero/negative span finishes immediately.
    void configure(std::int64_t spanFrames, const AhdParams& params) {
        span_ = spanFrames > 0 ? spanFrames : 0;
        fit(params, /*latchFinished=*/false);  // a fresh note starts from a clean read
        smooth_.clear();
    }

    // Peer of AdsrEnvelope::snapLive: a voice that has rendered nothing takes the new shape
    // outright, with no step to absorb.
    void snapLive(const AhdParams& params) {
        fit(params, /*latchFinished=*/false);
        smooth_.clear();
    }

    // Live delivery to a sounding voice at its current `sourceOffset`. See the class note for
    // why this smooths rather than holding a normalized position.
    void applyLive(double sourceOffset, const AhdParams& params) {
        const double before = ahdLevelAt(sourceOffset, fit_, attackCurve_, decayCurve_);
        // LATCHED: a voice already read past its fitted total must never resurge because a
        // later live move reopened the total. Reachable on any active() voice, including one
        // ringing out past its own end (voice.h) where tickAmplitude() still runs.
        fit(params, /*latchFinished=*/true);
        const double after = ahdLevelAt(sourceOffset, fit_, attackCurve_, decayCurve_);
        if (after != before) smooth_.absorb(before - after);
    }

    // Amplitude at `sourceOffset` = (readPos - startFrame). Latches finished() at or past the
    // fitted total, which is what frees the voice.
    double amplitudeAt(double sourceOffset) {
        if (finished_ || sourceOffset >= static_cast<double>(fit_.total)) {
            if (sourceOffset >= static_cast<double>(fit_.total)) finished_ = true;
            return 0.0;
        }
        const double out = ahdLevelAt(sourceOffset, fit_, attackCurve_, decayCurve_);
        return smooth_.active() ? out + smooth_.advance() : out;
    }

    bool finished() const { return finished_; }
    const AhdSpan& stages() const { return fit_; }

private:
    // `latchFinished`: once true, a re-fit can only ever KEEP finished_ true, never clear it —
    // see applyLive above for why. configure()/snapLive() pass false: those are a fresh read
    // (new note or a not-yet-rendered voice), which must compute finished_ from scratch.
    void fit(const AhdParams& p, bool latchFinished) {
        fit_ = fitAhd(span_, p);
        attackCurve_ = p.attackCurve;
        decayCurve_ = p.decayCurve;
        const bool empty = (fit_.total <= 0);
        finished_ = latchFinished ? (finished_ || empty) : empty;
    }

    std::int64_t span_ = 0;
    AhdSpan fit_;
    double attackCurve_ = util::kCurveNeutral;
    double decayCurve_ = util::kCurveNeutral;
    bool finished_ = true;
    StepSmoother smooth_;
};

// tick() returns the current pitch offset in semitones (0 when disabled or past the AHD),
// advancing one frame. The voice converts it to a ratio multiply (Varispeed) or a shift-amount
// add (Preserve). Unlike the amplitude AHD this owns its own position counter — pitch-envelope
// time is wall-clock output frames — so the mid-stage rule applies in full.
class PitchEnvelope {
public:
    // `spanFrames` is the playable span the Hold fraction is taken against.
    void configure(std::int64_t spanFrames, const PitchEnvParams& params) {
        span_ = spanFrames > 0 ? spanFrames : 0;
        params_ = params;
        fit_ = fitAhd(span_, params.shape);
        pos_ = 0.0;
    }
    void noteOn() { pos_ = 0.0; smooth_.clear(); }

    // Peer of AdsrEnvelope::snapLive (see it for why the two paths cannot share code): a voice
    // that has rendered nothing takes the new shape and depth outright. `enabled` is a discrete
    // toggle travelling by reload, so the caller's copy of it is deliberately ignored.
    //
    // Both live entry points re-take `spanFrames` rather than keeping configure()'s: the span is
    // an OUTPUT-frame duration the caller converts from the read rate, and that rate carries a
    // live control (voice.h's pitchEnvSpanFrames). Passing the span back unchanged is exact.
    void snapLive(std::int64_t spanFrames, const PitchEnvParams& params) {
        span_ = spanFrames > 0 ? spanFrames : 0;
        params_.peakSemitones = params.peakSemitones;
        params_.shape = params.shape;
        fit_ = fitAhd(span_, params_.shape);
        smooth_.clear();
    }

    // Live parameter delivery, same rule as AdsrEnvelope::applyLive: hold the normalized
    // position within whichever leg the envelope is in, and absorb the depth step (peak is a
    // level, not a duration). A moved span re-fits under the same rule, so a live Pitch move
    // reshapes this envelope continuously instead of leaving it on the note-on read rate.
    void applyLive(std::int64_t spanFrames, const PitchEnvParams& params) {
        const double before = offsetAt();
        span_ = spanFrames > 0 ? spanFrames : 0;
        const AhdSpan next = fitAhd(span_, params.shape);
        pos_ = holdPhase(fit_, next);
        params_.peakSemitones = params.peakSemitones;
        params_.shape = params.shape;
        fit_ = next;
        const double after = offsetAt();
        if (after != before) smooth_.absorb(before - after);
    }

    double tick() {
        if (!params_.enabled) return 0.0;
        const double offset = offsetAt();
        pos_ += 1.0;
        return smooth_.active() ? offset + smooth_.advance() : offset;
    }

private:
    // The semitone offset at the current position — the shared evaluator for both tick() and
    // applyLive's before/after comparison.
    double offsetAt() const {
        if (!params_.enabled) return 0.0;
        return params_.peakSemitones *
               ahdLevelAt(pos_, fit_, params_.shape.attackCurve, params_.shape.decayCurve);
    }

    // The position under `next` holding the normalized position within whichever leg pos_ is
    // in. A leg dialled to zero completes: the position lands on that leg's new end.
    double holdPhase(const AhdSpan& old, const AhdSpan& next) const {
        const double oa = static_cast<double>(old.attack);
        const double oh = static_cast<double>(old.hold);
        const double od = static_cast<double>(old.decay);
        const double na = static_cast<double>(next.attack);
        const double nh = static_cast<double>(next.hold);
        const double nd = static_cast<double>(next.decay);
        if (pos_ < oa) return (na > 0.0) ? pos_ * (na / oa) : na;
        if (pos_ < oa + oh) return (nh > 0.0) ? na + (pos_ - oa) * (nh / oh) : na + nh;
        if (pos_ < oa + oh + od) {
            return (nd > 0.0) ? na + nh + (pos_ - oa - oh) * (nd / od) : na + nh + nd;
        }
        return na + nh + nd;  // already past the envelope: stay past it under the new shape
    }

    PitchEnvParams params_;
    std::int64_t span_ = 0;
    AhdSpan fit_;
    double pos_ = 0.0;
    StepSmoother smooth_;
};

}  // namespace reasampler
