// capture_name — pure implementation. See the header.

#include "core/capture/capture_name.h"

#include <cstdio>

namespace reasampler::capture {

namespace {

// A track name padded with spaces would render ragged in the label and as underscores in
// the stem, so both ends are trimmed before anything else looks at it.
std::string trimmed(const std::string& s) {
    std::size_t b = 0;
    std::size_t e = s.size();
    auto isSpace = [](unsigned char c) {
        return c == ' ' || c == '\t' || c == '\r' || c == '\n';
    };
    while (b < e && isSpace(static_cast<unsigned char>(s[b]))) ++b;
    while (e > b && isSpace(static_cast<unsigned char>(s[e - 1]))) --e;
    return s.substr(b, e - b);
}

// Truncating mid-sequence would put invalid UTF-8 into the persisted label, so the cut
// backs off over continuation bytes (10xxxxxx). The stem does not care — sanitizeStem
// replaces every non-ASCII byte anyway — but one rule for both keeps them the same name.
std::string truncateUtf8(const std::string& s, std::size_t maxBytes) {
    if (s.size() <= maxBytes) return s;
    std::size_t cut = maxBytes;
    while (cut > 0 && (static_cast<unsigned char>(s[cut]) & 0xC0) == 0x80) --cut;
    return s.substr(0, cut);
}

int clampTo(int v, int lo, int hi) { return v < lo ? lo : (v > hi ? hi : v); }

} // namespace

std::string formatCaptureStamp(const CaptureStamp& stamp) {
    if (stamp.month < 1 || stamp.day < 1) return {};
    char buf[24];
    std::snprintf(buf, sizeof(buf), "%02d-%02d %02d%02d",
                  clampTo(stamp.month, 1, 12), clampTo(stamp.day, 1, 31),
                  clampTo(stamp.hour, 0, 23), clampTo(stamp.minute, 0, 59));
    return buf;
}

CaptureName composeCaptureName(const CaptureNameInputs& in) {
    std::string base;
    int named = 0;
    for (const std::string& raw : in.sourceNames) {
        const std::string n = trimmed(raw);
        if (n.empty()) continue;
        if (base.empty()) base = n;
        ++named;
    }
    if (base.empty()) base = trimmed(in.fallback);
    if (base.empty()) base = "capture";
    base = truncateUtf8(base, kMaxSourceNameBytes);
    // truncateUtf8 backs off over continuation bytes, so a name whose first kMaxSourceNameBytes
    // bytes are ALL continuation bytes (0x80-0xBF) backs off to nothing — re-apply the "never an
    // empty label" fallback after truncation, not just before it.
    if (base.empty()) base = "capture";

    CaptureName out;
    out.label    = base;
    out.stemBase = base;

    // Several sources collapse onto the first one's name plus a count of the rest — the
    // alternative (joining every name) produces a stem no one can read and a label that
    // no longer fits a card.
    if (named > 1) {
        const std::string extra = std::to_string(named - 1);
        out.label    += " +" + extra;
        out.stemBase += "+" + extra;
    }

    if (in.ordinal > 0) {
        const std::string ord = std::to_string(in.ordinal);
        out.label    += " #" + ord;
        out.stemBase += "-" + ord;
    }

    const std::string stamp = formatCaptureStamp(in.stamp);
    if (!stamp.empty()) out.label += " " + stamp;

    return out;
}

std::string captureTrackName(const std::string& sourceName) {
    const std::string prefix(kCaptureTrackPrefix);
    // A source with no readable name yields the bare word rather than a trailing
    // space; both spellings are fixed points, which is what makes the whole function
    // one (a track named exactly "Capture" must not become "Capture Capture"). Read
    // from kCaptureTrackPrefixBare rather than chopped off prefix, so the two names
    // can't drift out of sync with each other (both expand from the same header token).
    const std::string bare = kCaptureTrackPrefixBare;

    if (sourceName.empty()) return bare;
    if (sourceName == bare) return sourceName;
    if (sourceName.rfind(prefix, 0) == 0) return sourceName;
    return prefix + sourceName;
}

} // namespace reasampler::capture
