// editor_models.cpp — the ReaSamplerEditor's model-selection half: which STORED struct each
// transient editor selection names. The overlay selection maps onto a staged envelope
// (pack/unpack) or a drawn contour; a curve-popup target maps onto one of the three velocity
// curves. One switch per family, so a moved field or a fourth curve is a one-place edit.
// Split from editor_controls, which owns the orthogonal half: the control-value domain maps.

#include "shell/instrument/reasampler_editor.h"

#include <cstdint>
#include <utility>  // std::as_const (the const/non-const accessor pairs)

#include "core/instrument/map/trigger_seam.h"    // triggerPlayLength (the Trigger play span)
#include "shell/instrument/reasampler_processor.h"

namespace reasampler::vst {

using namespace reasampler::instrument::map;  // PlaySeconds vocabulary + trigger_seam

namespace {
// The two directions of the AHDSR <-> StageEnvelope copy, so a field can only be forgotten in
// one place rather than two.
void packAhdsr(const AdsrSeconds& a, StageEnvelope& env) {
    env.kind = instrument::ui::EnvKind::Ahdsr;
    env.attackSeconds  = a.attackSeconds;
    env.holdSeconds    = a.holdSeconds;
    env.decaySeconds   = a.decaySeconds;
    env.sustainLevel   = a.sustainLevel;
    env.releaseSeconds = a.releaseSeconds;
    env.attackCurve    = a.attackCurve;
    env.decayCurve     = a.decayCurve;
    env.releaseCurve   = a.releaseCurve;
}
void unpackAhdsr(const StageEnvelope& env, AdsrSeconds& a) {
    a.attackSeconds  = env.attackSeconds;
    a.holdSeconds    = env.holdSeconds;
    a.decaySeconds   = env.decaySeconds;
    a.sustainLevel   = env.sustainLevel;
    a.releaseSeconds = env.releaseSeconds;
    a.attackCurve    = env.attackCurve;
    a.decayCurve     = env.decayCurve;
    a.releaseCurve   = env.releaseCurve;
}
void packAhd(const AhdSeconds& a, double originSeconds, double spanSeconds, StageEnvelope& env) {
    env.kind = instrument::ui::EnvKind::Ahd;
    env.attackSeconds = a.attackSeconds;
    env.decaySeconds  = a.decaySeconds;
    env.holdFraction  = a.holdFraction;
    env.attackCurve   = a.attackCurve;
    env.decayCurve    = a.decayCurve;
    env.originSeconds = originSeconds;
    env.spanSeconds   = spanSeconds;
}
void unpackAhd(const StageEnvelope& env, AhdSeconds& a) {
    a.attackSeconds = env.attackSeconds;
    a.decaySeconds  = env.decaySeconds;
    a.holdFraction  = env.holdFraction;
    a.attackCurve   = env.attackCurve;
    a.decayCurve    = env.decayCurve;
}
}  // namespace

StageEnvelope ReaSamplerEditor::packEnvelope(OverlayEnv which, const PlaySeconds& play,
                                             std::int64_t frames,
                                             std::int64_t startFrame) const {
    StageEnvelope env;
    const double rate = liveSampleRate();
    const double t0 = rate > 0.0 ? static_cast<double>(startFrame) / rate : 0.0;
    // The Trigger amp and filter AHDs live over the PLAY span; the pitch AHD over the whole
    // post-start span, since it keeps running after a Trigger one-shot's amplitude has ended.
    // effectiveLengthFraction, not the stored knob: the overlay's amp/filter AHD must read the
    // SAME span the engine plays, or its drawn shape and node drags cover only a fraction of
    // what the note actually does.
    const std::int64_t playLen =
        triggerPlayLength(effectiveLengthFraction(play), frames, startFrame);
    const double fullSpan =
        rate > 0.0 ? static_cast<double>((std::max)(std::int64_t{0}, frames - startFrame)) / rate
                   : 0.0;
    const double playSpan = rate > 0.0 ? static_cast<double>(playLen) / rate : 0.0;
    const bool trigger = (play.playMode == PlayMode::Trigger);
    switch (which) {
        case OverlayEnv::kPitch:
            packAhd(play.pitchEnv.shape, t0, fullSpan, env);
            break;
        case OverlayEnv::kFilter:
            if (trigger) packAhd(play.filter.trigEnv, t0, playSpan, env);
            else packAhdsr(play.filter.env, env);
            break;
        case OverlayEnv::kAmp:
            if (trigger) packAhd(play.trigAhd, t0, playSpan, env);
            else packAhdsr(play.adsr, env);
            break;
        case OverlayEnv::kNone:
            break;  // nothing is overlay-active; a default-constructed StageEnvelope, not amp
    }
    return env;
}

void ReaSamplerEditor::unpackEnvelope(OverlayEnv which, const StageEnvelope& env,
                                      PlaySeconds& play) const {
    const bool trigger = (play.playMode == PlayMode::Trigger);
    switch (which) {
        case OverlayEnv::kPitch:
            unpackAhd(env, play.pitchEnv.shape);
            break;
        case OverlayEnv::kFilter:
            if (trigger) unpackAhd(env, play.filter.trigEnv);
            else unpackAhdsr(env, play.filter.env);
            break;
        case OverlayEnv::kAmp:
            if (trigger) unpackAhd(env, play.trigAhd);
            else unpackAhdsr(env, play.adsr);
            break;
        case OverlayEnv::kNone:
            break;  // nothing is overlay-active, so there is nothing a drag could have edited
    }
}

const VelocityCurve& ReaSamplerEditor::splineFor(OverlayEnv which) const {
    switch (which) {
        case OverlayEnv::kPitch:  return params_.play.pitchSpline.contour;
        case OverlayEnv::kFilter: return params_.play.filterSpline.contour;
        case OverlayEnv::kAmp:
        case OverlayEnv::kNone:
            break;
    }
    return params_.play.ampSpline.contour;
}

VelocityCurve& ReaSamplerEditor::splineFor(OverlayEnv which) {
    return const_cast<VelocityCurve&>(std::as_const(*this).splineFor(which));
}

bool ReaSamplerEditor::overlayIsSpline() const {
    switch (overlayEnv_) {
        case OverlayEnv::kAmp:    return params_.play.ampSpline.mode == EnvMode::Spline;
        case OverlayEnv::kPitch:  return params_.play.pitchSpline.mode == EnvMode::Spline;
        case OverlayEnv::kFilter: return params_.play.filterSpline.mode == EnvMode::Spline;
        case OverlayEnv::kNone:   return false;
    }
    return false;
}

const VelocityCurve& ReaSamplerEditor::curveFor(CurveTarget target) const {
    switch (target) {
        case CurveTarget::kPitch:  return params_.play.pitchVelocityCurve;
        case CurveTarget::kFilter: return params_.play.filter.velocityCurve;
        case CurveTarget::kAmp:
        case CurveTarget::kNone:
            // kNone reads as amp — a valid, harmless choice for a target-agnostic caller (a
            // disabled deck cell still needs SOME curve to draw). editedCurve()'s mutable
            // overload below refuses kNone rather than relying on this fallback to protect amp.
            return params_.velocityCurve;
    }
    return params_.velocityCurve;
}

VelocityCurve& ReaSamplerEditor::curveFor(CurveTarget target) {
    return const_cast<VelocityCurve&>(std::as_const(*this).curveFor(target));
}

const VelocityCurve& ReaSamplerEditor::editedCurve() const { return curveFor(curvePopup_); }

VelocityCurve& ReaSamplerEditor::editedCurve() {
    // Refuses kNone rather than aliasing amp (see curvePopup_'s declaration for why); should be
    // unreachable now that every writer of kNone also cancels the drag, but this is the second,
    // independent line of defense.
    if (curvePopup_ == CurveTarget::kNone) {
        static VelocityCurve sink = VelocityCurve::flat();
        return sink;
    }
    return curveFor(curvePopup_);
}

}  // namespace reasampler::vst
