#pragma once
// envelopes.h — the three per-frame envelope evaluators (AHDSR amplitude, Trigger fade
// shape, AD 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"

namespace reasampler {

// 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.
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;
        framesInStage_ = 0;
    }

    // Gate off: enter Release from the CURRENT level — release-before-sustain releases from
    // the partial attack/decay level, not from sustainLevel.
    void noteOff() {
        if (stage_ == Stage::Idle || stage_ == Stage::Finished || stage_ == Stage::Release) {
            return;  // already released / not sounding.
        }
        releaseFrom_ = level_;
        stage_ = Stage::Release;
        framesInStage_ = 0;
    }

    // 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.
    double tick() {
        switch (stage_) {
            case Stage::Idle:
            case Stage::Finished:
                level_ = 0.0;
                return 0.0;

            case Stage::Attack: {
                if (params_.attackFrames <= 0) {
                    level_ = 1.0;
                } else {
                    level_ = static_cast<double>(framesInStage_) /
                             static_cast<double>(params_.attackFrames);
                    if (level_ > 1.0) level_ = 1.0;
                }
                const double out = level_;
                ++framesInStage_;
                if (framesInStage_ >= params_.attackFrames) {
                    // holdFrames == 0 falls straight through Hold on the next tick to Decay.
                    stage_ = Stage::Hold;
                    framesInStage_ = 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;
                    framesInStage_ = 0;
                    level_ = 1.0;
                    // Single re-dispatch into Decay (bounded: Hold->Decay only, not general
                    // recursion).
                    return tick();
                }
                level_ = 1.0;
                const double out = level_;
                ++framesInStage_;
                if (framesInStage_ >= params_.holdFrames) {
                    stage_ = Stage::Decay;
                    framesInStage_ = 0;
                    level_ = 1.0;
                }
                return out;
            }

            case Stage::Decay: {
                if (params_.decayFrames <= 0) {
                    level_ = params_.sustainLevel;
                } else {
                    const double t = static_cast<double>(framesInStage_) /
                                     static_cast<double>(params_.decayFrames);
                    level_ = 1.0 + (params_.sustainLevel - 1.0) * t;
                }
                const double out = level_;
                ++framesInStage_;
                if (framesInStage_ >= params_.decayFrames) {
                    stage_ = Stage::Sustain;
                    framesInStage_ = 0;
                    level_ = params_.sustainLevel;
                }
                return out;
            }

            case Stage::Sustain:
                level_ = params_.sustainLevel;
                return level_;

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

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

private:
    AdsrParams params_;
    Stage stage_ = Stage::Idle;
    double level_ = 0.0;
    std::int64_t framesInStage_ = 0;
    double releaseFrom_ = 0.0;  // level at the moment noteOff() was called
};

// A stateless-shape amplitude function over the play span, evaluated at a source-frame
// offset into the span (not output frames): under Varispeed a transposed voice consumes
// source faster than output, so driving the fades off the read position keeps fade-in/out
// anchored to the same source frames regardless of engine. Distinct from AHDSR —
// time-boxed by the play length and note-off-immune.
class TriggerEnvelope {
public:
    // `playLengthFrames` is (playEnd - startFrame). Fades are clamped so
    // fadeIn + fadeOut <= playLength (fadeOut anchored to the end). A zero/negative play
    // length finishes immediately.
    void configure(std::int64_t playLengthFrames, std::int64_t fadeInFrames,
                   std::int64_t fadeOutFrames, FadeCurve curve = kDefaultFadeCurve) {
        playLength_ = playLengthFrames > 0 ? playLengthFrames : 0;
        curve_ = curve;
        finished_ = (playLength_ <= 0);

        // Clamp the fades so fadeIn + fadeOut <= playLength (fade-out anchored to the end).
        // A negative fade is treated as 0. When both fades together exceed the play length,
        // shrink the fade-out first (the head fade-in is the more perceptually load-bearing
        // onset ramp), then the fade-in — never letting either go negative or the sum exceed
        // the span.
        std::int64_t fi = fadeInFrames > 0 ? fadeInFrames : 0;
        std::int64_t fo = fadeOutFrames > 0 ? fadeOutFrames : 0;
        if (fi > playLength_) fi = playLength_;
        if (fi + fo > playLength_) fo = playLength_ - fi;  // fo >= 0 since fi <= playLength_
        fadeIn_ = fi;
        fadeOut_ = fo;
    }

    // Amplitude in [0,1] at `sourceOffset` = (readPos - startFrame). Latches finished() at
    // or past playLength. Pure over the offset so it composes with either pitch engine's
    // read rate.
    double amplitudeAt(double sourceOffset) {
        if (finished_ || sourceOffset < 0.0 ||
            sourceOffset >= static_cast<double>(playLength_)) {
            // At/past the play length the one-shot is done; the voice also frees on
            // readPos >= playEnd.
            if (sourceOffset >= static_cast<double>(playLength_)) finished_ = true;
            return 0.0;
        }

        // Fade-in: 0->1 over [0, fadeIn_). Fade-out: 1->0 over
        // [playLength_-fadeOut_, playLength_). Unity between. The two ramps never overlap
        // (configure clamps fadeIn_ + fadeOut_ <= length). The offset is fractional (the read
        // head is fractional under repitch), so the ramps are smooth rather than stepped.
        double amp = 1.0;
        const double foStart = static_cast<double>(playLength_ - fadeOut_);
        if (fadeIn_ > 0 && sourceOffset < static_cast<double>(fadeIn_)) {
            const double phase = sourceOffset / static_cast<double>(fadeIn_);  // 0..1
            amp = (curve_ == FadeCurve::EqualPower)
                      ? std::sin(phase * 1.5707963267948966)  // sin(phase*pi/2): constant power
                      : phase;
        } else if (fadeOut_ > 0 && sourceOffset >= foStart) {
            const double phase = (sourceOffset - foStart) / static_cast<double>(fadeOut_);
            amp = (curve_ == FadeCurve::EqualPower)
                      ? std::cos(phase * 1.5707963267948966)  // cos(phase*pi/2): constant power
                      : (1.0 - phase);
        }
        return amp;
    }

    bool finished() const { return finished_; }

private:
    std::int64_t playLength_ = 0;
    std::int64_t fadeIn_ = 0;
    std::int64_t fadeOut_ = 0;
    FadeCurve curve_ = kDefaultFadeCurve;
    bool finished_ = false;
};

// tick() returns the current pitch offset in semitones (0 when disabled or past
// attack+decay), advancing one frame. The voice converts it to a ratio multiply
// (Varispeed) or a shift-amount add (Preserve).
class PitchEnvelope {
public:
    void configure(const PitchEnvParams& params) { params_ = params; pos_ = 0; }
    void noteOn() { pos_ = 0; }

    double tick() {
        if (!params_.enabled) return 0.0;

        const std::int64_t a = params_.attackFrames > 0 ? params_.attackFrames : 0;
        const std::int64_t d = params_.decayFrames > 0 ? params_.decayFrames : 0;
        const double peak = params_.peakSemitones;

        double offset;
        if (pos_ < a) {
            // Attack: 0 -> peak over attackFrames (rise into the peak).
            offset = peak * (static_cast<double>(pos_) / static_cast<double>(a));
        } else if (pos_ < a + d) {
            // Decay: peak -> 0 over decayFrames (settle to base pitch).
            const double t = static_cast<double>(pos_ - a) / static_cast<double>(d);
            offset = peak * (1.0 - t);
        } else {
            offset = 0.0;  // past attack+decay: at base pitch forever.
        }
        ++pos_;
        return offset;
    }

private:
    PitchEnvParams params_;
    std::int64_t pos_ = 0;
};

}  // namespace reasampler
