// instrument_params.cpp — the VST3 adapter over core/instrument/param: the Parameter subclass
// whose toPlain/toNormalized ARE the taper, the construction of the unit and parameter lists,
// the model projection both directions, and both delivery channels (the controller's write and
// the audio thread's queue drain). It DECIDES nothing — the pure module owns the frozen table,
// the laws and the formatter.

#include "shell/instrument/reasampler_processor.h"

#include "base/source/fstring.h"
#include "pluginterfaces/base/ustring.h"
#include "pluginterfaces/vst/ivstparameterchanges.h"  // IParameterChanges / IParamValueQueue

#include "core/instrument/engine/master_gain.h"
#include "core/instrument/param/param_format.h"
#include "core/instrument/param/param_id.h"
#include "core/instrument/param/param_live.h"   // writeHostParam (the model side of a host write)
#include "core/instrument/param/param_units.h"
#include "core/instrument/ui/deck_values.h"

using namespace Steinberg;
using namespace Steinberg::Vst;

namespace reasampler::vst {

namespace param = instrument::param;
using instrument::ui::DeckParam;

namespace {

void assign128(String128 dst, const char* src) {
    UString(dst, str16BufferSize(String128)).fromAscii(src);
}

// One class for all of them: the law is per-control data inside the pure module, so a subclass
// per unit category would model nothing that a DeckParam does not already say.
class DeckParameter : public Parameter {
public:
    explicit DeckParameter(const param::ParamRow& row) : deck_(row.deck) {
        assign128(info.title, row.title);
        assign128(info.shortTitle, row.shortTitle);
        assign128(info.units, param::unitStringFor(row.deck));
        info.id = row.id;
        info.unitId = row.unit;
        // Continuous, every one of them — and structurally so rather than by luck: stepCount > 0
        // is only meaningful for a discrete control, and every discrete control is reload or
        // rebuild tier and therefore not exposed at all. The editor's shift-snap is a DRAG
        // interaction and must never be published here: stepCount quantizes the parameter
        // permanently, for the host's automation too, and freezes into the forever contract.
        info.stepCount = 0;
        // COMPUTED from the default, never a normalized literal — a hand-written normalized
        // default is a second source of truth for it and drifts from the taper silently.
        info.defaultNormalizedValue = param::defaultNormalized(row.deck);
        // No kIsBypass on anything: the plugin is an instrument and exposes no bypass.
        info.flags = ParameterInfo::kCanAutomate;
        valueNormalized = info.defaultNormalizedValue;
    }

    ParamValue toPlain(ParamValue normalized) const SMTG_OVERRIDE {
        return param::toPlain(deck_, normalized);
    }
    ParamValue toNormalized(ParamValue plain) const SMTG_OVERRIDE {
        return param::toNormalized(deck_, plain);
    }
    void toString(ParamValue normalized, String128 out) const SMTG_OVERRIDE {
        char digits[24];
        param::formatPlainFor(deck_, param::toPlain(deck_, normalized), digits, sizeof(digits));
        assign128(out, digits);
    }
    bool fromString(const TChar* text, ParamValue& normalized) const SMTG_OVERRIDE {
        String str(text);
        str.toMultiByte(kCP_Utf8);
        double plain = 0.0;
        if (!param::parsePlain(param::unitKindFor(deck_), str.text8(), plain)) return false;
        normalized = param::toNormalized(deck_, plain);
        return true;
    }

    OBJ_METHODS(DeckParameter, Parameter)

private:
    DeckParam deck_;
};

}  // namespace

void ReaSamplerProcessor::buildParameterList() {
    // One unit per deck group that carries an exposed parameter, so a host can present the list
    // under its group names rather than as one flat run.
    struct UnitDesc { UnitID id; const char* name; };
    static const UnitDesc kUnits[] = {
        {param::kUnitPitch,     "Pitch"},
        {param::kUnitPitchEnv,  "Pitch Env"},
        {param::kUnitFilter,    "Filter"},
        {param::kUnitFilterEnv, "Filter Env"},
        {param::kUnitAmp,       "Amp Env"},
        {param::kUnitMaster,    "Master"},
    };
    for (const UnitDesc& u : kUnits) {
        String128 name;
        assign128(name, u.name);
        addUnit(new Unit(name, u.id));
    }
    // Ascending id IS the presentation order, which is what makes identity order and
    // presentation order agree by construction rather than by maintenance.
    for (const param::ParamRow& row : param::exposedParams()) {
        parameters.addParameter(new DeckParameter(row));
    }
}

tresult PLUGIN_API ReaSamplerProcessor::setParamNormalized(ParamID tag, ParamValue value) {
    const param::ParamRow* row = param::exposedRowFor(tag);
    if (!row) return kResultFalse;
    // Notification is suppressed for the duration: this write CAME from the host, and echoing it
    // back through performEdit would let a lane in write mode re-record its own playback.
    const bool wasSuppressed = paramNotifySuppressed_;
    paramNotifySuppressed_ = true;
    // The host's write takes the control's EXISTING commit tier and no other. Nothing here can
    // reach reloadInstrument or rebuildVoiceEngine, and that is structural: every reload- and
    // rebuild-tier control is omitted from the list, so no id maps to one.
    if (row->deck == DeckParam::kMasterGain) {
        setMasterGainLinear(instrument::engine::masterGainLinearFromNorm(value));
    } else {
        InstrumentParams params = instrumentParams();
        writeDeckParamToModel(params, row->deck, value);
        setInstrumentParams(params);
        publishLiveParams();
    }
    paramNotifySuppressed_ = wasSuppressed;
    // The container caches what the MODEL took, not what the host sent — a control whose write
    // clamped would otherwise read back the out-of-range value the clamp rejected.
    return EditControllerEx1::setParamNormalized(
        tag, modelParamNormalized(instrumentParams(), row->deck));
}

void ReaSamplerProcessor::writeDeckParamToModel(InstrumentParams& params, DeckParam deck,
                                                double normalized) {
    // WHICH home a control's value has is param::valueHomeFor's answer, not a list repeated here
    // — the read peer below branches on the same predicate, the editor draws the same split at
    // applyParamControl, and the instance-scalar set's ARITY is pinned by param_live's own test,
    // so a third one cannot appear without a failure. The value map itself is param_live's, which
    // is what makes this write and the audio thread's patch of the same point agree.
    if (param::valueHomeFor(deck) == param::ValueHome::InstanceScalar) {
        // Master gain never arrives here — both callers route it to setMasterGainLinear, its own
        // funnel — so key-track is the whole of this arm.
        if (deck == DeckParam::kKeyTrack) {
            params.keyTrack = instrument::ui::keyTrackFromNorm(normalized);
        }
        return;
    }
    param::writeHostParam(deck, params.play, normalized);
}

double ReaSamplerProcessor::modelParamNormalized(const InstrumentParams& params,
                                                 DeckParam deck) const {
    if (param::valueHomeFor(deck) == param::ValueHome::InstanceScalar) {
        return deck == DeckParam::kMasterGain
            ? instrument::engine::masterGainNormFromLinear(masterGainLinear())
            : instrument::ui::keyTrackNormFrom(params.keyTrack);
    }
    return instrument::ui::deckParamNorm(deck, params.play);
}

void ReaSamplerProcessor::syncParamsFromModel() {
    const InstrumentParams params = instrumentParams();
    for (const param::ParamRow& row : param::exposedParams()) {
        // EditControllerEx1's own setter, NOT ours: this is the LOAD direction, and the SDK is
        // explicit that a controller must never pass a load back to the host through
        // IComponentHandler — it updates the GUI element only.
        EditControllerEx1::setParamNormalized(row.id, modelParamNormalized(params, row.deck));
    }
}

void ReaSamplerProcessor::notifyParamsFromModel(const double* beforeNorms,
                                                const InstrumentParams& after) {
    if (paramNotifySuppressed_) return;
    // The bake's reset moves ~40 values at once. Grouping them tells the host they are ONE act,
    // which is what an undo stack and an automation lane both want; the SDK provides exactly
    // this for exactly this case (ivsteditcontroller.h, IComponentHandler2).
    const bool group = componentHandler2 && !gestureLatching_;
    if (group) componentHandler2->startGroupEdit();
    for (const param::ParamRow& row : param::exposedParams()) {
        if (row.deck == DeckParam::kMasterGain) continue;  // its own funnel notifies it
        const double now = modelParamNormalized(after, row.deck);
        if (now == beforeNorms[static_cast<std::size_t>(row.deck)]) continue;
        notifyParamChanged(row.id, now);
    }
    if (group) componentHandler2->finishGroupEdit();
}

bool ReaSamplerProcessor::gestureIsOpen(param::ParamId id) const {
    for (std::size_t i = 0; i < openGestureCount_; ++i) {
        if (openGestureIds_[i] == id) return true;
    }
    return false;
}

void ReaSamplerProcessor::notifyParamChanged(param::ParamId id, double normalized) {
    EditControllerEx1::setParamNormalized(id, normalized);
    if (!componentHandler) return;
    if (gestureIsOpen(id)) {
        performEdit(id, normalized);
        return;
    }
    if (gestureLatching_ && openGestureCount_ < kMaxOpenGestures) {
        // First move this drag has made on this parameter: open its bracket and hold it, so the
        // whole drag is one edit rather than a run of one-point ones. An UNRELATED writer that
        // reaches here mid-drag (the sync tick's flushGainNotify) latches into the same bracket
        // set, so the host sees its touch end when the drag does rather than at once — bounded by
        // the drag and correctly closed, and the alternative (a second bracket state per writer)
        // buys a distinction no host acts on.
        openGestureIds_[openGestureCount_++] = id;
        beginEdit(id);
        performEdit(id, normalized);
        return;
    }
    // Every non-drag writer — a reset, the bake's reset — emits a degenerate one-point gesture,
    // which is what makes the host DISPLAY follow it instead of re-imposing the pre-write value
    // on the next touch.
    beginEdit(id);
    performEdit(id, normalized);
    endEdit(id);
}

void ReaSamplerProcessor::beginParamGestureLatch() {
    endParamGesture();  // a grab while one is open cannot leave the previous unclosed
    gestureLatching_ = true;
}

void ReaSamplerProcessor::beginParamGesture(DeckParam deck) {
    beginParamGestureLatch();
    const param::ParamId id = param::paramIdFor(deck);
    if (id == 0 || !param::isExposed(deck)) return;
    openGestureIds_[openGestureCount_++] = id;
    beginEdit(id);
}

bool ReaSamplerProcessor::drainInputParameterChanges(IParameterChanges* changes) {
    if (!changes) return false;
    // RT-SAFE, and the one non-obvious part of that: exposedRowFor walks exposedParams(), whose
    // backing vector is a function-local static built on FIRST CALL. buildParameterList() calls
    // it from initialize(), which the SDK guarantees precedes any process() — so the allocation
    // has already happened by the time the audio thread gets here.
    bool landed = false;
    const int32 queues = changes->getParameterCount();
    for (int32 q = 0; q < queues; ++q) {
        IParamValueQueue* queue = changes->getParameterData(q);
        if (!queue) continue;
        const int32 points = queue->getPointCount();
        if (points <= 0) continue;
        // The LAST point of the queue wins for the block. Applying every point at its sample
        // offset would put a "did anything change" question on the per-voice-per-sample path,
        // which the phase-wide guardrail forbids.
        int32 offset = 0;
        ParamValue value = 0.0;
        if (queue->getPoint(points - 1, offset, value) != kResultTrue) continue;
        const param::ParamRow* row = param::exposedRowFor(queue->getParameterId());
        if (!row) continue;
        // Master gain reaches the audio beside the block rather than through it, so its
        // automation write is the same one relaxed store the knob makes — and it takes no hold,
        // which is what keeps a lane on it alone from republishing the block every block.
        const bool ridesTheBlock = row->deck != DeckParam::kMasterGain;
        if (!ridesTheBlock) {
            masterGain_.store(
                static_cast<float>(instrument::engine::masterGainLinearFromNorm(value)),
                std::memory_order_relaxed);
        }
        // Also published to the UI thread, which folds it back into the model — the blob stays
        // authoritative, so a value that never came back would be lost on save.
        landed |= automation_.land(static_cast<std::size_t>(row->deck), value, ridesTheBlock);
    }
    return landed;
}

void ReaSamplerProcessor::drainAutomationToModel() {
    if (!automation_.takePending()) return;
    InstrumentParams params = instrumentParams();
    // What was folded, and at which sequence. Held back rather than released as we go: the
    // release below is a statement that the MODEL carries the point, which is only true once the
    // publish has happened.
    std::size_t foldedSlots[kDeckParamSlots];
    std::uint32_t foldedSeqs[kDeckParamSlots];
    std::size_t foldedCount = 0;
    bool moved = false;
    for (const param::ParamRow& row : param::exposedParams()) {
        const auto slot = static_cast<std::size_t>(row.deck);
        double value = 0.0;
        std::uint32_t seq = 0;
        if (!automation_.takeSlot(slot, value, seq)) continue;
        foldedSlots[foldedCount] = slot;
        foldedSeqs[foldedCount] = seq;
        ++foldedCount;
        // Master gain's model IS the atomic the audio thread already wrote; there is nothing to
        // fold, only the controller cache to refresh below.
        if (row.deck == DeckParam::kMasterGain) continue;
        // A present-but-static lane resends the SAME point every tick; writing it back would
        // republish liveParams_ and move paramsGeneration_ — a full model copy, a controller
        // cache write and an editor repaint, every tick, forever, for a value that never moved.
        // Compared on the STORED side, never the normalized one: toNormalized(toPlain(n)) == n
        // does NOT hold in general (param_taper.h — "no log map satisfies it in double"), so
        // comparing `value` to modelParamNormalized(params, ...) skipped almost nothing for the
        // 33 of 43 exposed controls whose taper is log/curved rather than identity — the churn
        // this comment describes ran unabated for those. hostStoredFromNorm is the exact map
        // writeDeckParamToModel below applies, so comparing its answer against the field it would
        // land in asks "would this write change anything", not "are two norms equal". The
        // release below still fires either way: the model already carries the point regardless
        // of whether this write actually runs.
        const double newStored = param::hostStoredFromNorm(row.deck, value);
        bool unchanged = false;
        if (row.deck == DeckParam::kKeyTrack) {
            unchanged = newStored == params.keyTrack;
        } else if (float* f = instrument::ui::deckFloatField(row.deck, params.play)) {
            // The filter's four store a float: compared at that width, since that is what a
            // re-write would actually round to, not the double newStored computes before it.
            unchanged = static_cast<float>(newStored) == *f;
        } else if (double* d = instrument::ui::deckDoubleField(row.deck, params.play)) {
            unchanged = newStored == *d;
        }
        if (unchanged) continue;
        writeDeckParamToModel(params, row.deck, value);
        moved = true;
    }
    // Suppressed for the whole fold: these values CAME from the host, and echoing them back
    // through performEdit would let a lane in write mode re-record its own playback. The
    // controller cache is still refreshed, so the host's display and the editor follow.
    const bool wasSuppressed = paramNotifySuppressed_;
    paramNotifySuppressed_ = true;
    if (moved) {
        setInstrumentParams(params);
    }
    // The hold outranks the model only until the model carries the point (this directory's
    // CLAUDE.md, THE AUTHORITY MODEL) — `releaseProof` is that ordering enforced structurally:
    // `automation_.release` below cannot compile without one, and the only ways to obtain one are
    // `publishLiveParams`'s return (the `moved` branch) or `noRepublishNeeded` (nothing to
    // publish because the fold left the model already matching the point). Built via an
    // immediately-invoked lambda rather than `moved ? publishLiveParams() : noRepublishNeeded()`
    // directly: ReleaseProof's copy constructor is deleted (automation_channel.h), and a ternary
    // between two same-type prvalue arms needs it to merge them into one value — each `return`
    // below is instead its OWN guaranteed-elided construction of the function's result object.
    const AutomationChannel::ReleaseProof releaseProof = [&]() -> AutomationChannel::ReleaseProof {
        if (moved) return publishLiveParams();
        return AutomationChannel::ReleaseProof::noRepublishNeeded();
    }();
    syncParamsFromModel();
    paramNotifySuppressed_ = wasSuppressed;
    for (std::size_t i = 0; i < foldedCount; ++i) {
        automation_.release(foldedSlots[i], foldedSeqs[i], releaseProof);
    }
}

void ReaSamplerProcessor::endParamGesture() {
    gestureLatching_ = false;
    if (openGestureCount_ == 0) return;
    // Latched into a local and the state cleared FIRST: endEdit can re-enter through a host's
    // own callback, and must not find a bracket this call is in the middle of closing.
    param::ParamId closing[kMaxOpenGestures];
    const std::size_t count = openGestureCount_;
    for (std::size_t i = 0; i < count; ++i) closing[i] = openGestureIds_[i];
    openGestureCount_ = 0;
    for (std::size_t i = 0; i < count; ++i) endEdit(closing[i]);
}

}  // namespace reasampler::vst
