// Slopsmith plugin-sandbox IPC protocol.
//
// One pair of files defines the wire format used by both the host (Slopsmith
// Desktop, inside the slopsmith_audio.node addon) and the sandbox subprocess
// (slopsmith-vst-host.exe).
//
// Two channels:
//   * Control: a bidirectional named pipe carrying length-prefixed JSON
//     messages. Used for everything that isn't per-block audio.
//   * Audio: shared-memory ring buffers plus a pair of auto-reset OS events.
//     Used for the audio fast-path; sized for one block at the configured
//     sample rate.
//
// See SANDBOX-DESIGN.md (in the PR description) for the rationale.

#pragma once

#include <cstdint>
#include <juce_core/juce_core.h>

namespace slopsmith::sandbox {

// Bumped whenever the wire format changes incompatibly. Host and sandbox MUST
// agree on this number during the `ready` handshake; mismatched versions abort
// the spawn and fall back to in-process loading.
//
// v2 changes vs v1:
//   * AudioShmHeader splits writeIdx/readIdx into per-direction
//     in*/out* pairs so input and output rings no longer share state.
//   * MIDI is bundled into the audio shm per input slot (MidiQueue) instead
//     of riding the control pipe per event; op::kMidiEvent is removed (the
//     sandbox keeps a warn-and-drop handler as a paranoid v1 fallback).
//   * setBlockSize is no longer "planned" — it's a real op gated by the
//     audio-thread pause/drain/resume protocol described in vst-host/main.cpp.
//
// v3 (this PR, mid-review): SHM ABI cleanup — overflow accounting moved from
//   a per-slot field on MidiQueue to a single AudioShmHeader.midiOverflows
//   counter (per-slot was confusing under round-robin slot reuse). Bumped
//   because mixed v2/v3 binaries would interpret different offsets for the
//   MIDI queue + trailing header counters and silently corrupt ring data.
//   v2 was never released externally; this is the first version that ships.
inline constexpr uint32_t kProtocolVersion = 3;

// Magic number stamped at the head of the audio shared memory so a stale
// mapping from a crashed sandbox can be detected.
inline constexpr uint32_t kAudioShmMagic = 0x534C5341u; // 'SLSA'

// Frame format on the control pipe:
//   [u32 length-LE][utf8 json body of `length` bytes]
inline constexpr uint32_t kMaxControlMessageBytes = 8 * 1024 * 1024; // 8 MiB
inline constexpr uint32_t kControlPipeBufferBytes = 64 * 1024;

// Tightest reasonable budget: 4 blocks at 1024 samples / 8 channels / float32.
inline constexpr uint32_t kAudioMaxBlocks = 4;
inline constexpr uint32_t kAudioMaxBlockSamples = 1024;
inline constexpr uint32_t kAudioMaxChannels = 8;

// Wall-clock cap on host-side waits for sandbox replies. Plugins doing slow
// state-restore can legitimately take a while, so this is generous.
inline constexpr int kDefaultReplyTimeoutMs = 10000;

// Named-object suffixes used by the audio shm + event pair. Names are
// generated host-side and passed to the sandbox via command-line args, so
// only the host's AudioChannel::createHostSide actually uses these — but
// centralising them prevents future drift if the sandbox ever needs to
// reconstruct a name (e.g. for diagnostic logging that surfaces the names
// in stable form rather than via argv echo).
inline constexpr const char* kShmNameSuffix       = "audio";
inline constexpr const char* kEvtToHostSuffix     = "evt-out";
inline constexpr const char* kEvtToSandboxSuffix  = "evt-in";

// Editor size default applied when the plugin reports an invalid (< 16 in
// either axis) editor size. Used by both the sandbox host (kOpenEditor
// reply) and the host-side SandboxedEditor fallback so the two sides
// don't drift.
inline constexpr int kDefaultEditorWidth  = 1000;
inline constexpr int kDefaultEditorHeight = 600;

// Watchdog: a sandbox that sends neither `ready` nor a `loading` heartbeat
// within this window is presumed broken. The host measures the deadline
// against the last signal of either kind (see event::kLoading), so a plugin
// that keeps heart-beating through a long first-run init is not fast-failed —
// only a genuinely silent (hung or crashed) sandbox trips it. Still generous
// on its own because some plugins (NI Guitar Rig 6 in particular) spin up an
// embedded Qt5/QML engine on first load, which can take 8-12 seconds on a
// cold cache.
inline constexpr int kReadyTimeoutMs = 30000;

// Absolute upper bound on the ready handshake, independent of heartbeats.
// event::kLoading heartbeats push the kReadyTimeoutMs deadline forward, but
// the heartbeat is a fixed timer — not a real plugin-progress signal — so a
// plugin whose load hangs while the sandbox's message loop stays responsive
// would otherwise heartbeat forever and never trip the watchdog. This cap
// fails the spawn regardless of heartbeats. Bumped to 20x kReadyTimeoutMs
// (10 min) after a tester reproduced Archetype Gojira X tripping the
// previous 5-min cap with ~58 kLoading heartbeats from a healthy sandbox
// — Neural DSP's large neural-net models can genuinely take that long on
// a cold cache + slow disk. The heartbeat-silence watchdog (kReadyTimeoutMs,
// 30 s) is the real hang detector; this absolute cap is just a backstop
// against a sandbox that keeps heart-beating but never finishes.
inline constexpr int kReadyAbsoluteTimeoutMs = 600000;

// Control channel — operation names.
//
// Kept as string constants (rather than an enum) because they appear verbatim
// in JSON on the wire and in log output; matching tools downstream don't have
// to know the enum.
namespace op {
    // Host → sandbox requests
    inline constexpr const char* kPrepare        = "prepare";
    inline constexpr const char* kSetBlockSize   = "setBlockSize";
    inline constexpr const char* kSetParameter   = "setParameter";
    inline constexpr const char* kListParameters = "listParameters";
    inline constexpr const char* kGetState       = "getState";
    inline constexpr const char* kSetState       = "setState";
    // Removed as of protocol v2: MIDI now flows inline in the audio shm
    // (see MidiQueue below). The version handshake rejects v1 hosts before
    // they can reach this op, but the sandbox keeps a warn-log no-op
    // handler as a paranoid fallback. Drop the handler when v1 host
    // binaries are no longer in circulation.
    inline constexpr const char* kMidiEvent      = "midiEvent";
    inline constexpr const char* kOpenEditor     = "openEditor";
    inline constexpr const char* kResizeEditor   = "resizeEditor";
    inline constexpr const char* kCloseEditor    = "closeEditor";
    inline constexpr const char* kShutdown       = "shutdown";
}

// Control channel — sandbox-originated event names (requestId is null).
namespace event {
    inline constexpr const char* kReady             = "ready";
    // Heartbeat emitted by the sandbox while a slow plugin is still loading,
    // so the host's kReadyTimeoutMs handshake watchdog measures its deadline
    // against the last heartbeat rather than fast-failing a legitimately slow
    // first-run load. Purely additive: pre-kLoading hosts ignore it as an
    // unknown event, and pre-kLoading sandboxes simply never send it, so no
    // protocol-version bump is needed.
    inline constexpr const char* kLoading           = "loading";
    inline constexpr const char* kParameterChanged  = "parameterChanged";
    inline constexpr const char* kEditorClosed      = "editorClosed";
    inline constexpr const char* kLog               = "log";
    inline constexpr const char* kError             = "error";
    inline constexpr const char* kGoodbye           = "goodbye";
}

// Per-block MIDI bundling.
//
// In v1 the host posted each MIDI event over the control pipe from the audio
// thread, which is a classic priority-inversion footgun (mutex + overlapped
// pipe I/O on the audio callback). v2 inlines MIDI in the audio shared memory
// so the audio thread does no IPC beyond two atomic stores and a SetEvent.
//
// Layout: one `MidiQueue` per *input* slot (host → sandbox direction). The
// host fills the upcoming slot's queue immediately before pushing the audio
// block; the sandbox drains it immediately after popping the same slot.
//
// Caps are deliberately tight. SysEx > 4 bytes does not fit and is silently
// dropped (overflow counter); a separate v3 op can carry it if a real
// workload ever hits the case.
inline constexpr uint32_t kMidiEventMaxBytes = 4;
inline constexpr uint32_t kMidiEventsPerSlot = 64;

struct MidiEvent
{
    uint32_t frame = 0;                          // sample offset within the block
    uint32_t size  = 0;                          // 1..kMidiEventMaxBytes
    uint8_t  bytes[kMidiEventMaxBytes] = {};
};

struct MidiQueue
{
    // `count` is published with release semantics by the host after the
    // matching `events[]` entries are written; sandbox loads with acquire
    // semantics. Accessed via std::atomic_ref<uint32_t> from .cpp so the
    // shm layout stays trivially copyable.
    alignas(8) uint32_t count = 0;
    MidiEvent           events[kMidiEventsPerSlot] = {};
    // Per-slot overflow was here in the first v2 cut, but slots are reused
    // round-robin and the value would accumulate across all uses of that
    // slot — confusing for any "did this block overflow?" reader, redundant
    // for any "lifetime total" reader. Cumulative count lives in
    // AudioShmHeader.midiOverflows.
};

// Shared-memory layout for the audio fast path.
//
// All offsets are bytes from the start of the mapping. Sized at spawn time
// from the prepared sample rate / block size / channel count; the values
// below are hard caps a sandbox will refuse to exceed.
//
// Atomic indices are stored as plain uint64_t for shm layout portability
// and accessed via std::atomic_ref<uint64_t> at the call site (C++20). Don't
// reintroduce reinterpret_cast to std::atomic<uint64_t>* — that's not
// layout-guaranteed. Modulo-`maxBlocks` of an index gives the block slot.
//
// Per-direction split (v2): input ring (host → sandbox) and output ring
// (sandbox → host) each get their own writer/reader index pair so the two
// directions can advance independently without sharing state.
struct AudioShmHeader
{
    uint32_t magic = 0;             // kAudioShmMagic
    uint32_t protocolVersion = 0;   // kProtocolVersion
    uint32_t maxBlocks = 0;
    uint32_t maxBlockSamples = 0;
    uint32_t maxChannels = 0;
    uint32_t sampleRate = 0;

    alignas(8) uint64_t inWriteIdx  = 0;  // host produces ring A (input audio + MIDI)
    alignas(8) uint64_t inReadIdx   = 0;  // sandbox consumes ring A
    alignas(8) uint64_t outWriteIdx = 0;  // sandbox produces ring B (output audio)
    alignas(8) uint64_t outReadIdx  = 0;  // host consumes ring B

    // Direction-agnostic diagnostic counters. xruns covers any pushBlock
    // where the destination ring was full; dropouts covers any popBlock that
    // timed out waiting for the partner; midiOverflows covers any MIDI event
    // dropped by pushInputBlock for being SysEx-sized or because the slot's
    // MidiQueue already held kMidiEventsPerSlot entries.
    alignas(8) uint64_t xruns         = 0;
    alignas(8) uint64_t dropouts      = 0;
    alignas(8) uint64_t midiOverflows = 0;

    // Byte offsets of the two rings + per-slot MIDI region inside the mapping.
    // Convenient for tools and asserts; computed at spawn time.
    uint64_t inputRingOffset  = 0;
    uint64_t outputRingOffset = 0;
    uint64_t midiQueueOffset  = 0;     // base of MidiQueue[maxBlocks]
    uint64_t ringBytesPerSlot = 0;
};

// Convenience: a fully-validated set of audio dimensions, agreed at handshake.
struct AudioDimensions
{
    uint32_t maxBlocks = kAudioMaxBlocks;
    uint32_t maxBlockSamples = kAudioMaxBlockSamples;
    uint32_t maxChannels = 2;
    uint32_t sampleRate = 48000;

    constexpr bool operator==(const AudioDimensions& o) const
    {
        return maxBlocks == o.maxBlocks && maxBlockSamples == o.maxBlockSamples
            && maxChannels == o.maxChannels && sampleRate == o.sampleRate;
    }

    constexpr uint64_t bytesPerSlot() const
    {
        // float32, planar across channels.
        return uint64_t(maxBlockSamples) * uint64_t(maxChannels) * sizeof(float);
    }

    constexpr uint64_t totalShmBytes() const
    {
        // Header + two audio rings + per-input-slot MidiQueue.
        return sizeof(AudioShmHeader)
             + 2 * uint64_t(maxBlocks) * bytesPerSlot()
             + uint64_t(maxBlocks) * sizeof(MidiQueue);
    }
};

// JSON helpers — thin wrappers around juce::JSON to keep call sites tidy.
// All sandbox messages flow through these so the framing/encoding is
// consistent (and any future migration to a binary codec is one-spot).
namespace wire {

// Build the standard envelope:
//   { "v": 1, "id": <int|null>, "op": "<name>", "args": { ... } }
juce::var makeRequest(int requestId, const char* op, const juce::var& args);

// Sandbox-originated:
//   { "v": 1, "event": "<name>", "data": { ... } }
juce::var makeEvent(const char* eventName, const juce::var& data);

// Reply:
//   { "v": 1, "id": <int>, "ok": true/false, "result": ..., "error": ... }
juce::var makeReply(int requestId, bool ok, const juce::var& result,
                    const juce::String& errorMessage = {});

// Serialise to a UTF-8 buffer (no length prefix — that's the channel's job).
juce::MemoryBlock encode(const juce::var& v);

// Parse a UTF-8 buffer back into a var. Returns juce::var::undefined() on parse
// error (and stashes the reason in `errorOut` if provided).
juce::var decode(const void* data, size_t bytes, juce::String* errorOut = nullptr);

} // namespace wire

} // namespace slopsmith::sandbox
