// wav_codec — pure implementation. See wav_codec.h. The one RIFF chunk
// traversal lives here (nextWavChunk); layout parse and content hash both
// walk with it, so their view of the container cannot drift.

#include "core/capture/wav_codec.h"

#include <cstdio>   // std::snprintf (hash hex render)
#include <cstring>  // std::memcpy, std::memcmp
#include <utility>  // std::move

namespace reasampler::capture {

namespace {

// Little-endian readers. Caller checks bounds before each read (off + N <= size).
std::uint16_t readU16LE(const std::vector<std::uint8_t>& b, std::size_t off) {
    return static_cast<std::uint16_t>(b[off] | (b[off + 1] << 8));
}
std::uint32_t readU32LE(const std::vector<std::uint8_t>& b, std::size_t off) {
    return static_cast<std::uint32_t>(b[off]) |
           (static_cast<std::uint32_t>(b[off + 1]) << 8) |
           (static_cast<std::uint32_t>(b[off + 2]) << 16) |
           (static_cast<std::uint32_t>(b[off + 3]) << 24);
}

bool tagEquals(const std::vector<std::uint8_t>& b, std::size_t off, const char* tag) {
    return off + 4 <= b.size() && std::memcmp(b.data() + off, tag, 4) == 0;
}

// WAVE format tags we accept as 32-bit float (see wav_codec.h).
constexpr std::uint16_t kWaveFormatIeeeFloat  = 0x0003;
constexpr std::uint16_t kWaveFormatExtensible = 0xFFFE;

// FNV-1a 64-bit constants (http://www.isthe.com/chongo/tech/comp/fnv/).
constexpr std::uint64_t kFnvOffsetBasis = 14695981039346656037ULL;
constexpr std::uint64_t kFnvPrime       = 1099511628211ULL;

std::string fnvHex(std::uint64_t h) {
    char buf[17];  // 16 hex digits, zero-padded
    std::snprintf(buf, sizeof(buf), "%016llx", static_cast<unsigned long long>(h));
    return std::string(buf);
}

// --- The one RIFF chunk traversal --------------------------------------------
//
// One sub-chunk of a RIFF/WAVE container: header at `headerOffset` (id(4) +
// size(4)), body at `bodyOffset`/`bodySize`. `bodyInBounds` false means the
// declared body runs past the buffer — still reported, but must not be read.
struct WavChunkView {
    std::size_t   headerOffset = 0;
    std::size_t   bodyOffset   = 0;
    std::uint32_t bodySize     = 0;
    bool          bodyInBounds = false;
};

// Advances one chunk. `pos` starts at 12 (after "RIFF" size "WAVE"); each call
// fills `out` and moves `pos` past the chunk's body, honoring RIFF even-byte
// padding. Returns false when no further chunk header fits. If the padded advance
// would overrun the buffer, the chunk is still reported (return true) and `pos`
// is parked past the end so the next call returns false.
bool nextWavChunk(const std::vector<std::uint8_t>& bytes, std::size_t& pos,
                  WavChunkView& out) {
    if (pos + 8 > bytes.size()) return false;

    out.headerOffset = pos;
    out.bodyOffset   = pos + 8;
    out.bodySize     = readU32LE(bytes, pos + 4);
    out.bodyInBounds = (out.bodyOffset + out.bodySize <= bytes.size());

    std::size_t advance = out.bodySize;
    if (advance & 1u) ++advance;  // RIFF pad byte
    if (advance > bytes.size() - out.bodyOffset) {
        pos = bytes.size();  // overrun -> this is the last reported chunk
    } else {
        pos = out.bodyOffset + advance;
    }
    return true;
}

bool isRiffWave(const std::vector<std::uint8_t>& bytes) {
    // Minimum viable RIFF/WAVE: "RIFF"(4) size(4) "WAVE"(4) = 12 bytes.
    return bytes.size() >= 12 && tagEquals(bytes, 0, "RIFF") &&
           tagEquals(bytes, 8, "WAVE");
}

}  // namespace

WavLayout parseWavLayout(const std::vector<std::uint8_t>& bytes) {
    WavLayout out;

    if (!isRiffWave(bytes)) return out;

    bool haveFmt = false;
    std::uint16_t fmtTag = 0, channels = 0, bitsPerSample = 0;
    std::uint32_t sampleRate = 0;
    std::uint16_t extensibleSubFormatTag = 0;  // set only when fmtTag == kWaveFormatExtensible

    // Walk the sub-chunks after "WAVE" (offset 12). A malformed/truncated file
    // is "invalid", never an OOB read.
    std::size_t pos = 12;
    WavChunkView c;
    while (nextWavChunk(bytes, pos, c)) {
        if (tagEquals(bytes, c.headerOffset, "fmt ")) {
            // fmt body: at least 16 bytes (PCM/float common fields).
            if (c.bodyOffset + 16 > bytes.size() || c.bodySize < 16) return out;
            fmtTag        = readU16LE(bytes, c.bodyOffset + 0);
            channels      = readU16LE(bytes, c.bodyOffset + 2);
            sampleRate    = readU32LE(bytes, c.bodyOffset + 4);
            bitsPerSample = readU16LE(bytes, c.bodyOffset + 14);
            // WAVE_FORMAT_EXTENSIBLE: the real format lives in the SubFormat GUID's
            // leading 2-byte tag at body offset 24, not in fmtTag itself. Body must
            // reach offset 24+16; otherwise leave the tag at 0 (rejected).
            if (fmtTag == kWaveFormatExtensible) {
                if (c.bodySize >= 40 && c.bodyOffset + 40 <= bytes.size()) {
                    extensibleSubFormatTag = readU16LE(bytes, c.bodyOffset + 24);
                }
            }
            haveFmt = true;
        } else if (tagEquals(bytes, c.headerOffset, "data")) {
            // Reject if the declared body runs past the buffer (truncated/lying
            // header), or if data arrived before fmt.
            if (!c.bodyInBounds) return out;
            if (!haveFmt) return out;

            // Extensible tag (0xFFFE) is float only when its SubFormat sub-tag is
            // also IEEE-float (0x0003) — PCM-integer-in-extensible must be rejected.
            const bool floatTag = (fmtTag == kWaveFormatIeeeFloat) ||
                                  (fmtTag == kWaveFormatExtensible &&
                                   extensibleSubFormatTag == kWaveFormatIeeeFloat);
            if (!floatTag || bitsPerSample != 32 || channels == 0) return out;

            out.valid               = true;
            out.channelCount        = channels;
            out.sampleRate          = sampleRate;
            out.dataByteOffset      = c.bodyOffset;
            out.dataByteLength      = c.bodySize;
            out.riffSizeFieldOffset = 4;
            out.dataSizeFieldOffset = c.headerOffset + 4;  // the `data` size field (LE uint32)
            return out;
        }
    }

    return out;  // no data chunk found -> invalid
}

std::vector<AudioSample> extractFloatFrames(const std::vector<std::uint8_t>& bytes,
                                            const WavLayout& layout,
                                            std::size_t startFrame,
                                            std::size_t frameCount) {
    std::vector<AudioSample> out;
    if (!layout.valid) return out;

    const std::size_t bytesPerFrame =
        static_cast<std::size_t>(layout.channelCount) * 4u;
    const std::size_t totalFrames = layout.frameCount();
    if (startFrame >= totalFrames) return out;

    // Clamp the requested span to the frames that actually exist.
    const std::size_t avail = totalFrames - startFrame;
    const std::size_t frames = (frameCount < avail) ? frameCount : avail;
    if (frames == 0) return out;

    const std::size_t firstByte =
        layout.dataByteOffset + startFrame * bytesPerFrame;
    out.resize(frames * layout.channelCount);
    // memcpy each float (LE on target hosts — see header's byte-order note).
    for (std::size_t i = 0; i < out.size(); ++i) {
        float f = 0.0f;
        std::memcpy(&f, bytes.data() + firstByte + i * 4u, 4u);
        out[i] = f;
    }
    return out;
}

WavTruncatePlan planWavTruncate(const WavLayout& layout, std::size_t keptFrames) {
    WavTruncatePlan plan;
    if (!layout.valid) return plan;

    const std::size_t totalFrames = layout.frameCount();
    if (keptFrames > totalFrames) return plan;  // never grow

    const std::size_t bytesPerFrame =
        static_cast<std::size_t>(layout.channelCount) * 4u;
    const std::size_t keptDataBytes = keptFrames * bytesPerFrame;

    plan.valid               = true;
    plan.newFileByteLength   = layout.dataByteOffset + keptDataBytes;
    plan.dataSizeFieldOffset = layout.dataSizeFieldOffset;
    plan.newDataSize         = static_cast<std::uint32_t>(keptDataBytes);
    plan.riffSizeFieldOffset = layout.riffSizeFieldOffset;
    // RIFF size counts everything after the 8-byte "RIFF"+size prefix.
    plan.newRiffSize         = static_cast<std::uint32_t>(plan.newFileByteLength - 8);
    return plan;
}

void patchU32LE(std::vector<std::uint8_t>& bytes, std::size_t off, std::uint32_t v) {
    bytes[off + 0] = static_cast<std::uint8_t>(v & 0xFF);
    bytes[off + 1] = static_cast<std::uint8_t>((v >> 8) & 0xFF);
    bytes[off + 2] = static_cast<std::uint8_t>((v >> 16) & 0xFF);
    bytes[off + 3] = static_cast<std::uint8_t>((v >> 24) & 0xFF);
}

std::vector<std::uint8_t> buildFloat32Wav(int nch, std::uint32_t rate,
                                          std::size_t frameCount,
                                          const std::vector<double>& interleaved) {
    const std::size_t sampleCount     = frameCount * static_cast<std::size_t>(nch);
    const std::size_t dataBytesCount  = sampleCount * 4u;  // 4 bytes per float32

    // The WAV is: RIFF(4)+size(4)+WAVE(4) = 12, fmt (4)+size(4)+16 body = 24, data (4)+size(4)+payload.
    // Total = 12 + 24 + 8 + dataBytesCount = 44 + dataBytesCount.
    const std::uint32_t riffSize =
        static_cast<std::uint32_t>(36u + dataBytesCount);  // 4("WAVE")+24(fmt chunk)+8(data hdr)+data

    std::vector<std::uint8_t> out;
    out.reserve(44u + dataBytesCount);

    auto putU16 = [&](std::uint16_t v) {
        out.push_back(static_cast<std::uint8_t>(v & 0xFF));
        out.push_back(static_cast<std::uint8_t>((v >> 8) & 0xFF));
    };
    auto putU32 = [&](std::uint32_t v) {
        out.push_back(static_cast<std::uint8_t>(v & 0xFF));
        out.push_back(static_cast<std::uint8_t>((v >> 8) & 0xFF));
        out.push_back(static_cast<std::uint8_t>((v >> 16) & 0xFF));
        out.push_back(static_cast<std::uint8_t>((v >> 24) & 0xFF));
    };
    auto putTag = [&](const char* t) {
        for (int i = 0; i < 4; ++i)
            out.push_back(static_cast<std::uint8_t>(t[i]));
    };
    auto putF32 = [&](float f) {
        std::uint8_t tmp[4];
        std::memcpy(tmp, &f, 4);
        for (int i = 0; i < 4; ++i) out.push_back(tmp[i]);
    };

    // RIFF header
    putTag("RIFF");
    putU32(riffSize);
    putTag("WAVE");

    // fmt chunk (16-byte body, WAVE_FORMAT_IEEE_FLOAT = 0x0003)
    putTag("fmt ");
    putU32(16u);                                // chunk body size
    putU16(0x0003u);                            // WAVE_FORMAT_IEEE_FLOAT
    putU16(static_cast<std::uint16_t>(nch));
    putU32(rate);
    putU32(rate * static_cast<std::uint32_t>(nch) * 4u);  // avgBytesPerSec
    putU16(static_cast<std::uint16_t>(nch * 4));           // blockAlign
    putU16(32u);                                            // bitsPerSample

    // data chunk
    putTag("data");
    putU32(static_cast<std::uint32_t>(dataBytesCount));
    for (std::size_t i = 0; i < sampleCount && i < interleaved.size(); ++i)
        putF32(static_cast<float>(interleaved[i]));

    return out;
}

MonoCollapse collapseToMono(const std::vector<std::uint8_t>& bytes) {
    MonoCollapse out;

    const WavLayout layout = parseWavLayout(bytes);
    if (!layout.valid || layout.channelCount < 2) return out;

    const std::size_t frames = layout.frameCount();
    if (frames == 0) return out;

    const std::size_t stride = layout.channelCount;
    const std::vector<AudioSample> pcm = extractFloatFrames(bytes, layout, 0, frames);
    if (pcm.size() != frames * stride) return out;  // short read -> decline, never guess

    // Bit patterns, not values: see the header. memcpy is the only defined float->bits
    // read, and it compiles to a register move.
    auto bitsOf = [](AudioSample s) {
        std::uint32_t bits = 0;
        std::memcpy(&bits, &s, 4u);
        return bits;
    };
    for (std::size_t f = 0; f < frames; ++f) {
        const std::uint32_t first = bitsOf(pcm[f * stride]);
        for (std::size_t c = 1; c < stride; ++c) {
            if (bitsOf(pcm[f * stride + c]) != first) return out;
        }
    }

    // float -> double -> float round-trips exactly for every finite value and for
    // +-0/+-infinity (double represents every float bit pattern in those classes), so
    // channel 0 reaches the rebuilt file unaltered. The one hole: a signaling NaN is
    // quieted by the float->double promotion, so an identical-bit sNaN pair could
    // collapse to a different bit pattern than it started with. Not reachable from
    // REAPER-rendered audio, but the bit-identical predicate above admits NaN inputs,
    // so this rebuild is not exempt from the claim it makes.
    std::vector<double> mono(frames);
    for (std::size_t f = 0; f < frames; ++f)
        mono[f] = static_cast<double>(pcm[f * stride]);

    out.collapsed = true;
    out.bytes = buildFloat32Wav(1, layout.sampleRate, frames, mono);
    return out;
}

CollapsedWav applyMonoCollapse(std::vector<std::uint8_t> bytes) {
    CollapsedWav out;
    MonoCollapse collapse = collapseToMono(bytes);
    if (collapse.collapsed) {
        const WavLayout rebuilt = parseWavLayout(collapse.bytes);
        if (rebuilt.valid) {
            out.bytes = std::move(collapse.bytes);
            out.layout = rebuilt;
            out.collapsed = true;
            return out;
        }
    }
    out.bytes = std::move(bytes);
    out.layout = parseWavLayout(out.bytes);
    return out;
}

std::string monoCollapseSuffix(MonoCollapseOutcome outcome) {
    switch (outcome) {
        case MonoCollapseOutcome::Declined:  return {};
        case MonoCollapseOutcome::Collapsed: return " (collapsed to mono)";
        case MonoCollapseOutcome::Failed:
            return " (mono collapse failed -- left as captured)";
    }
    return {};  // unreachable for a valid enum; claim nothing rather than a wrong outcome
}

std::string hashBytes(const std::uint8_t* data, std::size_t len) {
    // FNV-1a 64-bit: deterministic, no dependencies, adequate for dedup identity.
    std::uint64_t h = kFnvOffsetBasis;
    for (std::size_t i = 0; i < len; ++i) {
        h ^= static_cast<std::uint64_t>(data[i]);
        h *= kFnvPrime;
    }
    return fnvHex(h);
}

std::string hashWavContent(const std::vector<std::uint8_t>& bytes) {
    if (isRiffWave(bytes)) {
        std::uint64_t h = kFnvOffsetBasis;
        auto feedByte = [&](std::uint8_t b) {
            h ^= static_cast<std::uint64_t>(b);
            h *= kFnvPrime;
        };

        bool haveFmt  = false;
        bool haveData = false;

        feedByte(static_cast<std::uint8_t>('W'));  // domain-separation prefix

        std::size_t pos = 12;
        WavChunkView c;
        while (nextWavChunk(bytes, pos, c)) {
            if (tagEquals(bytes, c.headerOffset, "fmt ")) {
                if (c.bodyInBounds) {
                    for (std::uint32_t i = 0; i < c.bodySize; ++i)
                        feedByte(bytes[c.bodyOffset + i]);
                    haveFmt = true;
                }
            } else if (tagEquals(bytes, c.headerOffset, "data")) {
                if (c.bodyInBounds) {
                    for (std::uint32_t i = 0; i < c.bodySize; ++i)
                        feedByte(bytes[c.bodyOffset + i]);
                    haveData = true;
                }
            }
            // All other chunks (bext, iXML, LIST, SMED, cue, etc.) are skipped.
        }

        if (haveFmt && haveData) return fnvHex(h);
        // Falls through to whole-file fallback if chunks were missing/malformed.
    }

    // Fallback: not a parseable RIFF/WAVE — hash the whole file (identical to
    // hashBytes(data, size); no prefix tag).
    return hashBytes(bytes.data(), bytes.size());
}

} // namespace reasampler::capture
