// sample_map — pure implementation (the resolution half; the ComponentState codec lives
// in component_state_io.cpp). See sample_map.h.

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

#include "core/instrument/engine/period_detect.h"  // the load-time Preserve source period

#include <algorithm>  // std::remove_if
#include <cassert>    // assert
#include <utility>    // std::move

namespace reasampler::instrument::map {

namespace {

// The bank stores loop points as an optional LoopPoints (both-or-neither); the core wants
// a SampleLoop with an explicit hasLoop. Absent -> no loop.
SampleLoop loopFromSample(const Sample& s) {
    SampleLoop out;
    if (s.loop) {
        out.hasLoop = true;
        out.start = s.loop->start;
        out.end = s.loop->end;
    }
    return out;
}

// rootNote defaults to middle C (60) when the bank left the intrinsic empty — an
// un-rooted sample plays unity at C4 rather than failing to play.
SelectedSample distill(const Sample& s) {
    SelectedSample out;
    out.relativePath = s.relativePath;
    out.rootNote = s.rootNote ? *s.rootNote : 60;
    out.loop = loopFromSample(s);
    out.channelCount = s.channelCount;  // 0 = unknown (older entry)
    return out;
}

}  // namespace

std::optional<SelectedSample> selectSample(const std::string& banksJson,
                                           const std::string& sampleId) {
    // An empty selection is SILENCE, not the first sample — by design.
    if (sampleId.empty()) return std::nullopt;
    if (banksJson.empty()) return std::nullopt;
    std::optional<BankBook> book = BankBook::deserialize(banksJson);
    if (!book) return std::nullopt;  // malformed -> nothing to play (never throw)

    // Search every bank (ordinal order) for the stored id; a sample lives in exactly
    // one bank, so first hit wins.
    for (const Bank& b : book->banks()) {
        if (const Sample* s = b.index.query(sampleId)) {
            return distill(*s);
        }
    }
    // A stale stored id is SILENCE too — the editor's empty state, not a substitution.
    return std::nullopt;
}

ChannelMode channelModeFor(int channelCount, ChannelMode current, bool isExplicit) {
    if (isExplicit) return current;         // user's explicit choice is never fought
    if (channelCount <= 0) return current;  // unknown (0) or pathological -> no change
    return channelCount >= 2 ? ChannelMode::Stereo : ChannelMode::Mono;
}

// --- Instance-owned sample references (self-contained playback) -------------

const SelectedSample* findRef(const SampleRefs& refs, const std::string& sampleId) {
    if (sampleId.empty()) return nullptr;
    for (const SampleRefEntry& e : refs) {
        if (e.sampleId == sampleId) return &e.ref;
    }
    return nullptr;
}

std::vector<std::string> referencedSampleIds(const std::string& selectionId) {
    std::vector<std::string> ids;
    if (!selectionId.empty()) ids.push_back(selectionId);
    return ids;
}

void refreshRefsFromBank(SampleRefs& refs, const std::string& banksJson,
                         const std::vector<std::string>& ids) {
    if (ids.empty() || banksJson.empty()) return;
    std::optional<BankBook> book = BankBook::deserialize(banksJson);
    if (!book) return;  // malformed blob -> no-op (the instance keeps its own copies)
    for (const std::string& id : ids) {
        const Sample* found = nullptr;
        for (const Bank& b : book->banks()) {
            if (const Sample* s = b.index.query(id)) { found = s; break; }
        }
        if (!found) continue;  // bank miss: NEVER strips a ref — the instance owns its copy
        const SelectedSample distilled = distill(*found);
        bool updated = false;
        for (SampleRefEntry& e : refs) {
            if (e.sampleId == id) {
                e.ref = distilled;
                e.displayName = found->displayName;  // rename sync
                updated = true;
                break;
            }
        }
        if (!updated) refs.push_back(SampleRefEntry{id, distilled, found->displayName});
    }
}

bool sameDecodeSource(const SelectedSample& a, const SelectedSample& b) {
    return a.relativePath == b.relativePath && a.rootNote == b.rootNote &&
           a.channelCount == b.channelCount && a.loop.hasLoop == b.loop.hasLoop &&
           a.loop.start == b.loop.start && a.loop.end == b.loop.end;
}

LegacyLiftDecision legacyLiftDecision(const std::optional<std::string>& banksJson,
                                      const std::vector<std::string>& ids) {
    if (!banksJson || banksJson->empty()) return LegacyLiftDecision::Retry;
    const std::optional<BankBook> book = BankBook::deserialize(*banksJson);
    if (!book) return LegacyLiftDecision::Retry;  // present but unparseable: not readable YET
    for (const std::string& id : ids) {
        for (const Bank& b : book->banks()) {
            if (b.index.query(id)) return LegacyLiftDecision::Lift;
        }
    }
    // The blob parses and knows none of the referenced ids (or there are none): provably
    // stale — a lift can never make progress against this bank.
    return LegacyLiftDecision::Stale;
}

void retainRefs(SampleRefs& refs, const std::vector<std::string>& ids) {
    refs.erase(std::remove_if(refs.begin(), refs.end(),
                              [&ids](const SampleRefEntry& e) {
                                  for (const std::string& id : ids) {
                                      if (id == e.sampleId) return false;
                                  }
                                  return true;
                              }),
               refs.end());
}

std::vector<SampleChoice> listSamples(const std::string& banksJson) {
    std::vector<SampleChoice> out;
    if (banksJson.empty()) return out;
    std::optional<BankBook> book = BankBook::deserialize(banksJson);
    if (!book) return out;
    for (const Bank& b : book->banks()) {
        for (const Sample& s : b.index.all()) {
            out.push_back(SampleChoice{s.id, s.displayName, s.rootNote, s.key, b.id});
        }
    }
    return out;
}

std::vector<BankChoice> listBanks(const std::string& banksJson) {
    std::vector<BankChoice> out;
    if (banksJson.empty()) return out;
    std::optional<BankBook> book = BankBook::deserialize(banksJson);
    if (!book) return out;
    for (const Bank& b : book->banks()) {
        out.push_back(BankChoice{b.id, b.displayName});
    }
    return out;
}

std::vector<AudioSample> downmixToMono(const std::vector<AudioSample>& interleaved,
                                       int channelCount) {
    std::vector<AudioSample> out;
    if (channelCount <= 0 || interleaved.empty()) return out;
    const std::size_t stride = static_cast<std::size_t>(channelCount);
    const std::size_t frames = interleaved.size() / stride;
    out.resize(frames);
    const double inv = 1.0 / static_cast<double>(channelCount);
    for (std::size_t f = 0; f < frames; ++f) {
        double acc = 0.0;
        const std::size_t base = f * stride;
        for (std::size_t c = 0; c < stride; ++c) {
            acc += static_cast<double>(interleaved[base + c]);
        }
        out[f] = static_cast<AudioSample>(acc * inv);
    }
    return out;
}

std::vector<AudioSample> extractChannel(const std::vector<AudioSample>& interleaved,
                                        int channelCount, int which) {
    std::vector<AudioSample> out;
    if (channelCount <= 0 || interleaved.empty()) return out;
    const std::size_t stride = static_cast<std::size_t>(channelCount);
    // Clamp the requested channel into the source's range: a channel past the last one reads
    // the last channel (a mono source asked for channel 1 yields channel 0 — dual-mono).
    std::size_t ch = which < 0 ? 0 : static_cast<std::size_t>(which);
    if (ch >= stride) ch = stride - 1;
    const std::size_t frames = interleaved.size() / stride;
    out.resize(frames);
    for (std::size_t f = 0; f < frames; ++f) out[f] = interleaved[f * stride + ch];
    return out;
}

DecodedPcm decodeChannels(const std::vector<AudioSample>& interleaved,
                          int sourceChannels, ChannelMode mode, int sampleRate) {
    assert(sampleRate > 0 && "decodeChannels: sampleRate must be > 0 (programming error)");
    DecodedPcm out;
    if (sampleRate <= 0) return out;  // safe early-return; caller supplied an invalid rate
    out.sampleRate = sampleRate;
    if (mode == ChannelMode::Mono) {
        out.monoFrames = downmixToMono(interleaved, sourceChannels);
        return out;  // framesR stays empty
    }
    // extractChannel clamps out-of-range, so a mono source yields L == R (dual-mono).
    out.monoFrames = extractChannel(interleaved, sourceChannels, 0);
    out.framesR = extractChannel(interleaved, sourceChannels, 1);
    return out;
}

PlayParams resolvePlay(const PlaySeconds& stored, int sampleRate) {
    // seconds -> frames at the LIVE rate; the source-timeline quantity (trigger %-length)
    // carries through untouched, already a fraction.
    assert(sampleRate > 0 && "resolvePlay: sampleRate must be > 0 (programming error)");
    const double sr = sampleRate > 0 ? static_cast<double>(sampleRate) : 1.0;  // 1.0 avoids div-by-zero; assert fires first
    const auto secToFrames = [sr](double sec) { return secondsToFrames(sec, sr); };
    // The one seconds->frames fold for a stored AHD; the fraction and the curves are rate-free.
    const auto resolveAhd = [&secToFrames](const AhdSeconds& s) {
        AhdParams a;
        a.attackFrames = secToFrames(s.attackSeconds);
        a.decayFrames  = secToFrames(s.decaySeconds);
        a.holdFraction = s.holdFraction;
        a.attackCurve  = s.attackCurve;
        a.decayCurve   = s.decayCurve;
        return a;
    };
    PlayParams out;
    out.playMode = stored.playMode;
    out.adsr.attackFrames  = secToFrames(stored.adsr.attackSeconds);
    out.adsr.holdFrames    = secToFrames(stored.adsr.holdSeconds);
    out.adsr.decayFrames   = secToFrames(stored.adsr.decaySeconds);
    out.adsr.sustainLevel  = stored.adsr.sustainLevel;   // level, not a time
    out.adsr.releaseFrames = secToFrames(stored.adsr.releaseSeconds);
    out.adsr.attackCurve   = stored.adsr.attackCurve;    // dimensionless
    out.adsr.decayCurve    = stored.adsr.decayCurve;
    out.adsr.releaseCurve  = stored.adsr.releaseCurve;
    out.trigger = stored.trigger;                        // fraction, unchanged
    out.trigAhd = resolveAhd(stored.trigAhd);
    out.pitchEngine = stored.pitchEngine;
    out.playRate               = stored.playRate;             // a ratio, rate-free
    out.pitchOffsetSemitones   = stored.pitchOffsetSemitones; // semitones, rate-free
    out.pitchEnv.enabled       = stored.pitchEnv.enabled;
    out.pitchEnv.peakSemitones = stored.pitchEnv.peakSemitones;  // depth, not a time
    out.pitchEnv.shape         = resolveAhd(stored.pitchEnv.shape);
    out.pitchVelocityCurve     = stored.pitchVelocityCurve;  // transfer curve, not a time
    // Filter: the control positions are already rate-free and carry through untouched; only
    // its envelope resolves to frames.
    out.filter.enabled   = stored.filter.enabled;
    out.filter.settings  = stored.filter.settings;
    out.filter.modAmount = stored.filter.modAmount;
    out.filter.velAmount = stored.filter.velAmount;
    out.filter.keyTrack  = stored.filter.keyTrack;
    out.filter.velocityCurve   = stored.filter.velocityCurve;
    out.filter.env.attackFrames  = secToFrames(stored.filter.env.attackSeconds);
    out.filter.env.holdFrames    = secToFrames(stored.filter.env.holdSeconds);
    out.filter.env.decayFrames   = secToFrames(stored.filter.env.decaySeconds);
    out.filter.env.sustainLevel  = stored.filter.env.sustainLevel;
    out.filter.env.releaseFrames = secToFrames(stored.filter.env.releaseSeconds);
    out.filter.env.attackCurve   = stored.filter.env.attackCurve;
    out.filter.env.decayCurve    = stored.filter.env.decayCurve;
    out.filter.env.releaseCurve  = stored.filter.env.releaseCurve;
    out.filter.trigEnv           = resolveAhd(stored.filter.trigEnv);
    // The three drawn contours are normalized over the sample's own length, so no rate resolves
    // them — they carry through verbatim, which is also what makes a different-length capture
    // replay the same shape proportionally.
    out.ampSpline    = stored.ampSpline;
    out.pitchSpline  = stored.pitchSpline;
    out.filterSpline = stored.filterSpline;
    // Every field splineActive reads on `out` is already copied from `stored` above, so this
    // enforces the same rule enforceGateUnavailableWhileDrawn's doc comment (play_params.h)
    // describes — the editor's applyControl is the other caller, so the two cannot drift.
    enforceGateUnavailableWhileDrawn(out);
    return out;
}

// --- The one parameter set ----------------------------------------------------

ResolvedCapture resolveCapture(const SelectedSample& ref, const InstrumentParams& params) {
    ResolvedCapture rs;
    rs.relativePath = ref.relativePath;
    rs.rootNote = params.rootOverride ? *params.rootOverride : ref.rootNote;
    // Key tracking + velocity curve are instrument state — carried straight through.
    rs.keyTrack = params.keyTrack;
    rs.velocityCurve = params.velocityCurve;
    // The override wins over the intrinsic; absent -> intrinsic (loop) / frame 0 (start).
    // The bank is never mutated.
    rs.loop = params.loopOverride ? *params.loopOverride : ref.loop;
    rs.loopCrossfadeFrames = params.loopCrossfadeFrames;
    rs.startFrame = params.startPoint ? *params.startPoint : 0;
    rs.play = params.play;  // SECONDS; buildSampleData resolves to frames
    return rs;
}

std::optional<ResolvedCapture> resolveFromBank(const std::string& banksJson,
                                              const std::string& selectionId,
                                              const InstrumentParams& params) {
    const std::optional<SelectedSample> sel = selectSample(banksJson, selectionId);
    if (!sel) return std::nullopt;
    return resolveCapture(*sel, params);
}

std::optional<ResolvedCapture> resolveFromRefs(const SampleRefs& refs,
                                              const std::string& selectionId,
                                              const InstrumentParams& params) {
    const SelectedSample* ref = findRef(refs, selectionId);
    if (ref == nullptr) return std::nullopt;
    return resolveCapture(*ref, params);
}

SampleData buildSampleData(const ResolvedCapture& resolved, DecodedPcm decoded) {
    SampleData data;
    if (decoded.monoFrames.empty()) return data;  // unreadable/empty WAV -> silence
    assert(decoded.sampleRate > 0 &&
           "buildSampleData: DecodedPcm::sampleRate must be > 0 (programming error)");
    if (decoded.sampleRate <= 0) return data;  // safe early-return; assert fires first
    data.frames = std::move(decoded.monoFrames);
    // Carry the second channel only when it length-matches channel 0 (channelCount()
    // enforces the same rule; a mismatched pair falls back to mono rather than half-play).
    if (!decoded.framesR.empty() && decoded.framesR.size() == data.frames.size()) {
        data.framesR = std::move(decoded.framesR);
    }
    data.sampleRate = decoded.sampleRate;
    data.rootNote = resolved.rootNote;
    data.loop = resolved.loop;
    data.loopCrossfadeFrames = resolved.loopCrossfadeFrames;
    data.startFrame = resolved.startFrame;
    data.keyTrack = resolved.keyTrack;
    data.velocityCurve = resolved.velocityCurve;
    // Resolve the stored wall-clock SECONDS (AHDSR, pitch env A/D) to frames at THIS WAV's
    // actual rate; source-timeline params (trigger %-length + fades, start) carry through.
    data.play = resolvePlay(resolved.play, data.sampleRate);
    // The one place Preserve's source period is computed: the load, off the audio thread.
    // Channel 0 only — a stereo pair's two channels share a fundamental, and the splice
    // schedule is linked across them anyway. The span is the sustain loop where one is long
    // enough (periodAnalysisSpan owns that rule) — every input to it commits through a full
    // reload, so the cache is re-derived whenever the span it was chosen from moves.
    const instrument::engine::AnalysisSpan span = instrument::engine::periodAnalysisSpan(
        data.frames.size(), data.loop.start, data.loop.end, data.loop.hasLoop, data.sampleRate);
    data.sourcePeriodFrames =
        instrument::engine::detectPeriod(data.frames, data.sampleRate, span.from, span.count)
            .frames;
    return data;
}

}  // namespace reasampler::instrument::map
