// panel_thumbnails.cpp — thumbnail compute + cache seam of the docked bank panel.
// Owns the per-sample PCM read fed to peaks::computeEnvelope and the in-memory
// thumbnail cache keyed by (sample id, bin width, bank generation), plus the
// bank-change fingerprint pass that owns that generation key — it lives here
// because the cache + generation it invalidates are this seam's state.
//
// main.cpp owns the API pointers; here they are extern. DAW-verified, not unit tested.

#include <cstdint>
#include <string>
#include <vector>

#include "shell/panel/panel_state.h"

#define REAPERAPI_MINIMAL
#define REAPERAPI_WANT_PCM_Source_CreateFromFile
#define REAPERAPI_WANT_PCM_Source_Destroy
#include "reaper_plugin_functions.h"

namespace reasampler::panel {

namespace {

constexpr int kMaxThumbnailFrames = 1 << 20;  // ~1M frames (~22s @ 48k)

// `width` is a BIN count.
Envelope computeThumbnail(const std::string& absPath, int width) {
    if (width <= 0 || absPath.empty()) return {};

    PCM_source* src = PCM_Source_CreateFromFile(absPath.c_str());
    if (!src) return {};

    const int nch = src->GetNumChannels();
    const double srate = src->GetSampleRate();
    const double lengthSec = src->GetLength();
    if (nch <= 0 || srate < 1.0 || lengthSec <= 0.0) {
        PCM_Source_Destroy(src);
        return {};
    }

    std::int64_t totalFrames = static_cast<std::int64_t>(lengthSec * srate);
    if (totalFrames <= 0) { PCM_Source_Destroy(src); return {}; }
    int frames = totalFrames > kMaxThumbnailFrames
                     ? kMaxThumbnailFrames
                     : static_cast<int>(totalFrames);

    std::vector<ReaSample> buf(static_cast<std::size_t>(frames) * nch, 0.0);
    PCM_source_transfer_t block{};
    block.time_s = 0.0;
    block.samplerate = srate;
    block.nch = nch;
    block.length = frames;
    block.samples = buf.data();
    block.samples_out = 0;
    src->GetSamples(&block);

    PCM_Source_Destroy(src);

    const int got = block.samples_out;
    if (got <= 0) return {};

    const std::size_t sampleCount = static_cast<std::size_t>(got) * nch;
    std::vector<float> pcm(sampleCount);
    for (std::size_t i = 0; i < sampleCount; ++i)
        pcm[i] = static_cast<float>(buf[i]);

    // Clamp bins to the frame count: computeEnvelope pads binCount > frameCount with
    // trailing empty {0,0} bins, which would render a very short sample as a comb of
    // spikes over flat gaps.
    const int binCount = width < got ? width : got;
    return computeEnvelope(pcm, static_cast<std::size_t>(nch),
                           static_cast<std::size_t>(got),
                           static_cast<std::size_t>(binCount));
}

}  // namespace

const Envelope& thumbnailFor(const Sample& sample, int width,
                             const std::string& projectDir) {
    ThumbnailKey key{sample.id, width, g_panel.generation};
    const std::string ks = thumbnailKeyString(key);

    auto it = g_panel.cache.find(ks);
    if (it != g_panel.cache.end()) return it->second.envelope;

    const std::string abs = resolveBankFile(projectDir, sample.relativePath);
    CachedThumbnail thumb;
    thumb.width = width;
    thumb.envelope = computeThumbnail(abs, width);
    auto ins = g_panel.cache.emplace(ks, std::move(thumb));
    return ins.first->second.envelope;
}

namespace {

// A fingerprint of the WHOLE BOOK: for each bank, its id + display name + active flag
// + per-sample id/path. Catches every mutation the panel must redraw for: capture,
// project load, and create/rename/delete/move/activate.
std::string bookFingerprint() {
    BankBook* b = book();
    if (!b) return {};
    std::string fp = std::to_string(b->size());
    fp += '\x1e'; fp += b->activeBankId();
    for (const Bank& bk : b->banks()) {
        fp += '\x1d';
        fp += bk.id;
        fp += '\x1c';
        fp += bk.displayName;
        for (const Sample& s : bk.index.all()) {
            fp += '\x1f';
            fp += s.id;
            fp += '\x1f';
            fp += s.relativePath;
        }
    }
    return fp;
}

}  // namespace

// Reconciles shownBankId against the live named banks: keep it if it still names a
// named bank; otherwise fall to the first named bank (or empty when none). Keeps the
// banks region always showing a valid tab. Never touches the ACTIVE bank.
void reconcileShownBank() {
    BankBook* b = book();
    if (!b) { g_panel.shownBankId.clear(); return; }
    if (!g_panel.shownBankId.empty()) {
        const Bank* bk = b->bank(g_panel.shownBankId);
        if (bk && !bk->isPool()) return;  // still valid
    }
    const std::vector<const Bank*> named = namedBanks();
    g_panel.shownBankId = named.empty() ? std::string() : named.front()->id;
}

bool refreshFingerprint() {
    if (!book()) return false;
    std::string fp = bookFingerprint();
    if (fp == g_panel.bankFingerprint) return false;
    g_panel.bankFingerprint = std::move(fp);
    ++g_panel.generation;
    g_panel.cache.clear();
    // The selection indexes into the OLD order; a change can invalidate those, so
    // clear it and stop any audition.
    if (!g_panel.selection.empty() || g_panel.selection.focus >= 0) {
        g_panel.selection = Selection{};
        stopAudition();
    }
    reconcileShownBank();
    const BankModel* idx = indexForRegion(g_panel.focusedRegion);
    g_panel.selItemCount = idx ? static_cast<int>(idx->size()) : 0;
    return true;
}

}  // namespace reasampler::panel
