// automation_channel.h — the host automation lane's per-instance state, and the ONE place its
// AUTHORITY LIFETIME is mechanised: a point outranks the model from the block it lands in until
// the UI thread has folded it back into the model AND republished. This directory's CLAUDE.md
// states the model; `core/instrument/param/param_merge` is the pure decision this feeds.
// The release itself is observed the NEXT BLOCK, not the instant it happens — see
// refreshReleases().

#pragma once

#include <atomic>
#include <cassert>
#include <cstddef>
#include <cstdint>

#include "core/instrument/param/param_merge.h"

namespace reasampler::vst {

class ReaSamplerProcessor;  // the one class that legitimately mints a ReleaseProof

// The DeckParam ordinal space — what the automation slots and the notification diff both index.
inline constexpr std::size_t kDeckParamSlots = instrument::param::kDeckParamSlots;

class AutomationChannel {
public:
    // Evidence release() below requires: the model has actually caught up with the
    // point being released. The two factories are named for the case they cover —
    // `fromPublish` for an observed model republish, `noRepublishNeeded` for the two
    // cases drainAutomationToModel finds nothing to publish (the fold left the model
    // unchanged, or no engine exists yet to read the live block). This is NOT a full
    // compile-time proof of ordering: `noRepublishNeeded` still lets `ReaSamplerProcessor`
    // claim the exemption with no publish at all. What it enforces structurally is that a
    // caller cannot spell release()'s argument without naming, by which factory it called,
    // WHICH exemption it is claiming — greppable by a reviewer, not silently inline — and
    // both factories are restricted to the one class that legitimately needs either.
    // The private, user-provided default constructor plus the deleted copy constructor
    // close the two ways `{}` and copy-reuse would otherwise fabricate one for free; see
    // the two definitions below for which C++17 rule each closes.
    class ReleaseProof {
    private:
        friend class ReaSamplerProcessor;

        static ReleaseProof fromPublish(std::uint32_t before, std::uint32_t after) {
            // publish() advances the generation BY CONSTRUCTION (gen + 2, skipping 0 on wrap —
            // live_params.h), so `after == before` is unreachable from this, its one call site,
            // short of ~2^31 publishes wrapping exactly onto `before`. It also cannot see the
            // actually-reachable failure mode, a caller falsely claiming noRepublishNeeded() —
            // that path never runs this function. DEBUG-ONLY on purpose: an unconditional abort
            // here would take down a musician's whole REAPER session, unsaved work on every other
            // track and plugin included, for a condition this call site cannot produce; a debug
            // build (where the test suite runs) is where a future regression in publish()'s
            // advance guarantee should be caught.
            assert(after != before && "publish() must advance the generation");
            return ReleaseProof{};
        }
        static ReleaseProof noRepublishNeeded() { return ReleaseProof{}; }

        // User-PROVIDED (a body, not `= default`): under C++17 a class with no data
        // members and only a user-DECLARED (not user-provided) default constructor is
        // still an aggregate, because C++17's aggregate rule excludes private data
        // members only, not private constructors — so `ReleaseProof{}` would perform
        // aggregate init and never call this. A user-provided constructor defeats that.
        // (C++20's P1008 closes the same hole at the language level; this project is
        // pinned to C++17 — CMakeLists.txt:28 — so the class must close it itself.)
        ReleaseProof() {}
        // Closes the other free-mint path: the implicit copy constructor is public by
        // default, so one legitimately-minted proof cached in a member could be replayed
        // by any later caller with no new publish behind it. A user-declared copy
        // constructor (deleted or not) also suppresses the implicit move constructor, so
        // no move-based replay path opens in its place — `release()` below takes this by
        // const reference for exactly that reason, rather than needing one back.
        ReleaseProof(const ReleaseProof&) = delete;
    };

    // --- AUDIO THREAD -------------------------------------------------------------------
    // A point landed for `slot`. `ridesTheBlock` is false for a control that reaches the audio
    // beside the live block (master gain, whose route is the processor's own atomic): such a
    // point takes no hold and does not make the block dirty, so a lane on it alone cannot drive
    // the per-voice fan-out every block for a value the block does not carry.
    //
    // Answers whether the block MOVED, which is what makes the merge conditional. A repeat of a
    // standing hold moves nothing and is dropped whole — no hold rewrite, no UI publish — because
    // it would only make the fold rewrite the model with the value already in it.
    bool land(std::size_t slot, double normalized, bool ridesTheBlock) {
        if (ridesTheBlock && !instrument::param::automationPointMoves(slots_[slot], normalized)) {
            return false;
        }
        if (ridesTheBlock) {
            slots_[slot].norm = normalized;
            slots_[slot].held = true;
        }
        published_[slot].store(normalized, std::memory_order_relaxed);
        // The sequence is stored LAST and with release: the fold reads it FIRST and only then
        // trusts the value beside it.
        seq_[slot].store(seq_[slot].load(std::memory_order_relaxed) + 1,
                         std::memory_order_release);
        any_.store(true, std::memory_order_release);
        return ridesTheBlock;
    }

    // Refreshes each held slot's release answer. Runs only inside process()'s merge branch, so a
    // release lands the NEXT BLOCK after the UI thread makes it, never the same instant — benign
    // on the `moved` path because publishLiveParams always bumps the generation that branch
    // checks, so the next block is guaranteed to run this. The `noRepublishNeeded()` path has no
    // publish and no generation bump to guarantee that — land() drops a static lane's repeats, so
    // `moved` is false there too — but it is equally benign: the model already carries the value
    // (that is why nothing published), so the release is simply observed whenever the generation
    // next moves under ANY writer, not specifically this one. Must run BEFORE the model block is
    // read: the acquire here synchronizes with the UI thread's release store, which it makes only
    // AFTER republishing the model — so a slot seen released is one whose value any block read
    // after this point is guaranteed to already carry.
    void refreshReleases() {
        for (std::size_t i = 0; i < kDeckParamSlots; ++i) {
            if (!slots_[i].held) continue;
            slots_[i].folded = folded_[i].load(std::memory_order_acquire) ==
                               seq_[i].load(std::memory_order_relaxed);
        }
    }

    instrument::param::AutomationSlot* slots() { return slots_; }

    // --- UI THREAD ----------------------------------------------------------------------
    // True when at least one point has landed since the last drain.
    bool takePending() { return any_.exchange(false, std::memory_order_acquire); }

    // The value and sequence of `slot`'s unfolded point, or false when there is nothing new.
    bool takeSlot(std::size_t slot, double& value, std::uint32_t& seq) const {
        seq = seq_[slot].load(std::memory_order_acquire);
        if (seq == folded_[slot].load(std::memory_order_relaxed)) return false;
        value = published_[slot].load(std::memory_order_relaxed);
        return true;
    }

    // Releases `slot`'s hold. The `ReleaseProof` argument is the enforcement: it can only be
    // constructed once the model carrying this point has been republished (or shown not to need
    // it), so a caller earlier in that ordering has no value to pass. By const reference, not
    // value: the copy constructor is deleted (see ReleaseProof), and the one caller releasing a
    // whole fold's worth of slots passes the same proof through this repeatedly.
    void release(std::size_t slot, std::uint32_t seq, const ReleaseProof&) {
        folded_[slot].store(seq, std::memory_order_release);
    }

private:
    instrument::param::AutomationSlot slots_[kDeckParamSlots] = {};  // audio thread only
    std::atomic<double> published_[kDeckParamSlots] = {};
    std::atomic<std::uint32_t> seq_[kDeckParamSlots] = {};     // written by the audio thread
    std::atomic<std::uint32_t> folded_[kDeckParamSlots] = {};  // written by the UI thread
    std::atomic<bool> any_{false};  // makes the UI thread's idle drain a single exchange
};

// The publication atomics above are read on the audio thread; a locked implementation would be a
// hidden mutex on it. Structural rather than assumed, for a class whose thesis is RT discipline.
static_assert(std::atomic<double>::is_always_lock_free,
              "the automation publication must be lock-free — the audio thread writes it");
static_assert(std::atomic<std::uint32_t>::is_always_lock_free,
              "the automation sequence must be lock-free — the audio thread writes it");

}  // namespace reasampler::vst
