// component_state_io — the ComponentState ENVELOPE codec. See component_state_io.h for both
// format ladders (envelope v1..v11, params payload v1..v11); the payload half lives in
// params_payload, which grows on its own version axis. Every wire format is FROZEN —
// byte-identical across revisions.

#include "core/instrument/map/component_state_io.h"

#include <cmath>      // std::isfinite (v8 master-gain validation)
#include <cstring>    // std::memcpy (serializeSelection)
#include <utility>    // std::move

#include "core/instrument/engine/master_gain.h"  // masterGainMaxLinear — the v8 master-gain wire cap
#include "core/instrument/map/params_payload.h"  // the payload half of this codec
#include "core/wire/bytes.h"  // putLE / ByteReader / doubleToBits (the ONE LE codec)

namespace reasampler::instrument::map {

using engine::masterGainMaxLinear;
using reasampler::wire::ByteReader;
using reasampler::wire::asU64;
using reasampler::wire::bitsToDouble;
using reasampler::wire::doubleToBits;
using reasampler::wire::putLE;

namespace {

// Apply a payload read to the state: the adoption rule (a retired payload's first zone
// supersedes the envelope's selection id) lives here, once.
void applyPayload(ComponentState& out, PayloadRead read) {
    out.params = std::move(read.params);
    if (!read.adoptedSampleId.empty()) out.selectionId = std::move(read.adoptedSampleId);
}

}  // namespace

// --- Combined component state --------------------------------------

std::vector<std::uint8_t> serializeComponentState(const ComponentState& state) {
    std::vector<std::uint8_t> out;
    putLE(out, kComponentStateVersion);
    // v4 addition: channel mode (0 mono/1 stereo) precedes the v3 body.
    out.push_back(state.channelMode == ChannelMode::Stereo ? 1 : 0);
    // v5 addition: last-consumed assignment generation, 8-byte LE two's-complement, follows
    // the mode byte so a v4 reader stopping there is a strict prefix (see the v4 lift below).
    putLE(out, asU64(state.lastConsumedAssignGeneration));
    // v6 addition: preview-trigger velocity, 1 byte (MIDI 1..127) — a v5 blob is a strict
    // prefix up to this byte (see the v5 lift).
    out.push_back(state.previewVelocity);
    // v7 addition: voice count (1..32), voice mode (0 Poly/1 Mono), mono trigger
    // (0 Retrigger/1 Legato) — one byte each, a v6 blob is a strict prefix up to here.
    const int vc = state.voiceCount < kMinVoiceCount   ? kDefaultVoiceCount
                   : state.voiceCount > kMaxVoiceCount ? kMaxVoiceCount
                                                       : state.voiceCount;
    out.push_back(static_cast<std::uint8_t>(vc));
    out.push_back(state.voiceMode == VoiceMode::Mono ? 1 : 0);
    out.push_back(state.monoTrigger == MonoTrigger::Legato ? 1 : 0);
    // v8 addition: post-mixer LINEAR gain as a double (bit-cast to u64 LE) — a v7 blob is a
    // strict prefix up to here. The WRITER never emits out-of-range: non-finite/negative
    // falls back to unity; above the +24 dB cap clamps to the cap.
    {
        double g = state.masterGainLinear;
        const double maxLin = masterGainMaxLinear();
        if (!std::isfinite(g) || g < 0.0) g = 1.0;
        if (g > maxLin) g = maxLin;
        putLE(out, doubleToBits(g));
    }
    // v9 addition: channel-mode-EXPLICIT flag, 1 byte — a v8 blob is a strict prefix up to
    // here. 0 = implicit (shell may auto-default from the loaded capture's channel count);
    // 1 = user deliberately toggled the mode (never fought).
    out.push_back(state.channelModeExplicit ? 1 : 0);
    // v10 addition: the instance-owned sample-refs table — a v9 blob is a strict prefix up
    // to here. Wire shape per kSelectionRefsV10Version: entry count, then per entry id
    // + path (length-prefixed), rootNote, loop (hasLoop + start/end, always written),
    // channelCount, displayName (length-prefixed; display-only).
    putLE(out, static_cast<std::uint32_t>(state.sampleRefs.size()));
    for (const SampleRefEntry& e : state.sampleRefs) {
        putLE(out, static_cast<std::uint32_t>(e.sampleId.size()));
        out.insert(out.end(), e.sampleId.begin(), e.sampleId.end());
        putLE(out, static_cast<std::uint32_t>(e.ref.relativePath.size()));
        out.insert(out.end(), e.ref.relativePath.begin(), e.ref.relativePath.end());
        putLE(out, static_cast<std::uint32_t>(static_cast<std::int32_t>(e.ref.rootNote)));
        out.push_back(e.ref.loop.hasLoop ? 1 : 0);
        putLE(out, asU64(e.ref.loop.start));
        putLE(out, asU64(e.ref.loop.end));
        putLE(out,
                 static_cast<std::uint32_t>(static_cast<std::int32_t>(e.ref.channelCount)));
        putLE(out, static_cast<std::uint32_t>(e.displayName.size()));
        out.insert(out.end(), e.displayName.begin(), e.displayName.end());
    }
    // v11 envelope addition (usage instance identity): the minted per-instance guid,
    // length-prefixed, following the refs table so a v10 blob is a strict prefix up to
    // here (see the v10 lift). Empty = never published — legal, round-trips as empty.
    putLE(out, static_cast<std::uint32_t>(state.instanceGuid.size()));
    out.insert(out.end(), state.instanceGuid.begin(), state.instanceGuid.end());
    // Length-prefixed selection id (it precedes the params payload, so it MUST be framed —
    // unlike the v1 selection blob where the id ran to end-of-stream).
    putLE(out, static_cast<std::uint32_t>(state.selectionId.size()));
    out.insert(out.end(), state.selectionId.begin(), state.selectionId.end());
    putParamsPayload(out, state.params);
    return out;
}

ComponentState deserializeComponentState(const std::vector<std::uint8_t>& bytes,
                                         double projectRate) {
    // projectRate is only consumed for a LEGACY v3 payload; unused for v5+.
    ComponentState out;
    ByteReader r(bytes);
    const std::uint32_t version = r.u32();
    if (!r.ok) return out;  // no version tag -> empty (the silent empty state)

    // BACK-COMPAT: an older blob predates the v3 {selection, params} split.
    //   * v1 (original single-selection: version 1 + id-to-end): restore the id as the
    //     loaded capture with default parameters.
    //   * v2 (zones-only): the adopted first zone supplies BOTH the capture and the params.
    if (version == kSelectionStateVersion) {
        out.selectionId = deserializeSelection(bytes);
        return out;
    }
    if (version == kPerformanceStateVersion) {
        applyPayload(out, readParamsPayload(r, projectRate));  // body starts after the tag
        return out;                                            // channelMode stays Mono
    }
    // BACK-COMPAT: a v3 blob ({selection, params}, no channel mode) restores as MONO — the id
    // length + id + payload starts right after the version tag (no mode byte).
    if (version == kSelectionV3Version) {
        const std::uint32_t idLen = r.u32();
        out.selectionId = r.str(idLen);
        if (!r.ok) { out.selectionId.clear(); return out; }  // truncated id -> empty
        applyPayload(out, readParamsPayload(r, projectRate));
        return out;                    // channelMode stays Mono, marker stays 0
    }
    // BACK-COMPAT: a v4 blob ({mode, selection, params}, no consumed marker): mode byte, then
    // the id + payload — no 8-byte marker. lastConsumedAssignGeneration defaults to 0, so
    // a first assign still applies for a pre-marker instance.
    if (version == kSelectionModeV4Version) {
        const std::uint8_t modeByte = r.u8();
        if (!r.ok) return out;  // truncated before the mode byte -> empty (mono default holds)
        out.channelMode = (modeByte == 1) ? ChannelMode::Stereo : ChannelMode::Mono;
        const std::uint32_t idLen = r.u32();
        out.selectionId = r.str(idLen);
        if (!r.ok) { out.selectionId.clear(); return out; }  // truncated id -> empty
        applyPayload(out, readParamsPayload(r, projectRate));
        return out;                    // marker stays 0
    }
    // BACK-COMPAT: a v5 blob ({mode, marker, selection, params}, no preview-velocity byte).
    // previewVelocity defaults to kPreviewVelocityDefault (construction default), so an
    // already-saved instance restores at the mid default.
    if (version == kSelectionModeMarkerV5Version) {
        const std::uint8_t modeByte = r.u8();
        if (!r.ok) return out;  // truncated before the mode byte -> empty (mono default holds)
        out.channelMode = (modeByte == 1) ? ChannelMode::Stereo : ChannelMode::Mono;
        out.lastConsumedAssignGeneration = r.i64();
        if (!r.ok) return out;  // truncated before/inside the marker -> empty (marker 0 holds)
        const std::uint32_t idLen = r.u32();
        out.selectionId = r.str(idLen);
        if (!r.ok) { out.selectionId.clear(); return out; }  // truncated id -> empty
        applyPayload(out, readParamsPayload(r, projectRate));
        return out;                    // previewVelocity stays at the mid default
    }
    if (version != kComponentStateVersion &&
        version != kSelectionRefsV10Version &&
        version != kSelectionModeMarkerVelVoiceGainExplicitV9Version &&
        version != kSelectionModeMarkerVelVoiceGainV8Version &&
        version != kSelectionModeMarkerVelVoiceV7Version &&
        version != kSelectionModeMarkerVelV6Version) {
        return out;  // unknown -> empty
    }

    // v6..v11 shared prefix: channel-mode byte, 8-byte consumed-assignment marker, 1-byte
    // preview velocity. A non-{0,1} mode byte treats as mono (conservative default) rather
    // than rejected — a corrupt mode never silences the instance.
    const std::uint8_t modeByte = r.u8();
    if (!r.ok) return out;  // truncated before the mode byte -> empty (mono default holds)
    out.channelMode = (modeByte == 1) ? ChannelMode::Stereo : ChannelMode::Mono;
    out.lastConsumedAssignGeneration = r.i64();
    if (!r.ok) return out;  // truncated before/inside the marker -> empty (marker 0 holds)
    const std::uint8_t previewVel = r.u8();
    if (!r.ok) return out;  // truncated before the velocity byte -> empty (mid default holds)
    // Clamp to the documented MIDI 1..127 range: a 0 byte (or any out-of-spec value from a
    // corrupt blob) falls back to the mid default rather than silencing the preview trigger.
    out.previewVelocity = (previewVel >= 1 && previewVel <= 127)
                              ? previewVel
                              : kPreviewVelocityDefault;
    // v7+: the three voice-system bytes. A v6 blob skips them — the construction defaults
    // {16, Poly, Retrigger} hold, reproducing pre-voice-system behavior.
    if (version >= kSelectionModeMarkerVelVoiceV7Version) {
        const std::uint8_t vc = r.u8();
        const std::uint8_t vm = r.u8();
        const std::uint8_t mt = r.u8();
        if (!r.ok) return out;  // truncated inside the voice bytes -> empty (defaults hold)
        // Out-of-range bytes fall back to the field's DEFAULT (the previewVelocity precedent
        // for a corrupt blob) rather than clamping to an edge the user never chose.
        out.voiceCount = (vc >= kMinVoiceCount && vc <= kMaxVoiceCount)
                             ? static_cast<int>(vc)
                             : kDefaultVoiceCount;
        out.voiceMode = (vm == 1) ? VoiceMode::Mono : VoiceMode::Poly;
        out.monoTrigger = (mt == 1) ? MonoTrigger::Legato : MonoTrigger::Retrigger;
    }
    // v8+: the master-gain LINEAR double. A v7 blob skips it — the construction default
    // (unity) holds. A non-finite, negative, or above-cap value falls back to unity rather
    // than silencing/blasting.
    if (version >= kSelectionModeMarkerVelVoiceGainV8Version) {
        const double g = bitsToDouble(r.u64());
        if (!r.ok) return out;  // truncated inside the gain double — out already carries
                                // mode/marker/velocity/voice fields from above; unity holds
        out.masterGainLinear =
            (std::isfinite(g) && g >= 0.0 && g <= masterGainMaxLinear() * (1.0 + 1e-9))
                ? g
                : 1.0;
    }
    // v9: the channel-mode-EXPLICIT flag. A v8-or-older blob skips it — the construction
    // default (false = implicit) holds, so an already-saved instance's mode is treated as
    // the untouched default and the shell may auto-default it from the loaded capture.
    if (version >= kSelectionModeMarkerVelVoiceGainExplicitV9Version) {
        const std::uint8_t explicitByte = r.u8();
        if (!r.ok) return out;  // truncated before the flag -> empty (implicit holds)
        out.channelModeExplicit = (explicitByte == 1);
    }
    // v10: the sample-refs table. A v9-or-older blob skips it — the EMPTY-table default
    // holds, and the shell lifts the refs once via the bridge-resolve path (then re-saves
    // self-contained). A truncated mid-entry read keeps the entries that parsed cleanly and
    // drops the rest (the selection/params behind it are unreadable anyway).
    if (version >= kSelectionRefsV10Version) {
        const std::uint32_t refCount = r.u32();
        for (std::uint32_t i = 0; i < refCount && r.ok; ++i) {
            SampleRefEntry e;
            const std::uint32_t refIdLen = r.u32();
            e.sampleId = r.str(refIdLen);
            const std::uint32_t pathLen = r.u32();
            e.ref.relativePath = r.str(pathLen);
            // Range fallbacks: the refs table is the ONLY copy on the play path, so a
            // corrupt field must degrade to the field's default, never poison playback. An
            // out-of-MIDI-range root falls back to the middle-C default distill() uses; a
            // negative channel count falls back to 0 = unknown (auto-default then skips it).
            const std::int32_t root = r.i32();
            e.ref.rootNote = (root >= 0 && root <= 127) ? root : 60;
            e.ref.loop.hasLoop = (r.u8() != 0);
            e.ref.loop.start = r.i64();
            e.ref.loop.end = r.i64();
            const std::int32_t channels = r.i32();
            e.ref.channelCount = channels >= 0 ? channels : 0;
            const std::uint32_t nameLen = r.u32();
            e.displayName = r.str(nameLen);
            if (!r.ok) break;  // truncated mid-entry -> keep what parsed, drop the rest
            out.sampleRefs.push_back(std::move(e));
        }
        if (!r.ok) return out;
    }
    // v11: the minted instance guid. A v10-or-older blob skips it — the EMPTY default
    // holds and the processor mints a fresh identity on first publish.
    if (version >= kSelectionRefsIdentityV11Version) {
        const std::uint32_t guidLen = r.u32();
        out.instanceGuid = r.str(guidLen);
        if (!r.ok) { out.instanceGuid.clear(); return out; }  // truncated -> empty
    }
    const std::uint32_t idLen = r.u32();
    out.selectionId = r.str(idLen);
    if (!r.ok) { out.selectionId.clear(); return out; }  // truncated id -> empty
    applyPayload(out, readParamsPayload(r, projectRate));
    return out;
}

std::vector<std::uint8_t> serializeSelection(const std::string& sampleId) {
    std::vector<std::uint8_t> out;
    out.resize(4 + sampleId.size());
    const std::uint32_t v = kSelectionStateVersion;
    out[0] = static_cast<std::uint8_t>(v & 0xFF);
    out[1] = static_cast<std::uint8_t>((v >> 8) & 0xFF);
    out[2] = static_cast<std::uint8_t>((v >> 16) & 0xFF);
    out[3] = static_cast<std::uint8_t>((v >> 24) & 0xFF);
    std::memcpy(out.data() + 4, sampleId.data(), sampleId.size());
    return out;
}

std::string deserializeSelection(const std::vector<std::uint8_t>& bytes) {
    if (bytes.size() < 4) return {};  // no version tag -> no selection
    const std::uint32_t v = static_cast<std::uint32_t>(bytes[0]) |
                            (static_cast<std::uint32_t>(bytes[1]) << 8) |
                            (static_cast<std::uint32_t>(bytes[2]) << 16) |
                            (static_cast<std::uint32_t>(bytes[3]) << 24);
    if (v != kSelectionStateVersion) return {};  // unknown version -> ignore
    return std::string(reinterpret_cast<const char*>(bytes.data() + 4),
                       bytes.size() - 4);
}

}  // namespace reasampler::instrument::map
