// tail_control — pure implementation. See tail_control.h. NO REAPER / SWELL / vendor.

#include "tail_control.h"

#include <algorithm>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>

namespace reasampler {

TailMode cycleTailMode(TailMode current) {
    switch (current) {
        case TailMode::None:   return TailMode::Auto;
        case TailMode::Auto:   return TailMode::Manual;
        case TailMode::Manual: return TailMode::None;
    }
    return TailMode::None;  // unreachable for a valid enum; fail to the safe default
}

double clampManualMs(double manualMs) {
    // Same runaway guard the pure tailRenderSettingsFor applies to Manual: floor a
    // negative request to 0, cap at the 8 s ceiling.
    return std::clamp(manualMs, 0.0, kMaxTailMs);
}

double adjustManualMs(double current, int notches, double stepMs) {
    // Clamp the stepped value so both scroll directions saturate at the bounds rather
    // than running away (the same [0, kMaxTailMs] guard clampManualMs enforces).
    return clampManualMs(current + notches * stepMs);
}

std::string tailToggleLabel(const TailSetting& setting) {
    switch (setting.mode) {
        case TailMode::None: return "Tail: Off";
        case TailMode::Auto: return "Tail: Auto";
        case TailMode::Manual: {
            // Append the CLAMPED length in seconds to one decimal so the readout can
            // never show an over-cap value even if manualMs was stored past the cap.
            const double seconds = clampManualMs(setting.manualMs) / 1000.0;
            char buf[32];
            std::snprintf(buf, sizeof(buf), "Tail: Manual %.1fs", seconds);
            return std::string(buf);
        }
    }
    return "Tail: Off";  // unreachable for a valid enum; fail to the safe default
}

// ---------------------------------------------------------------------------
// JSON round-trip
// ---------------------------------------------------------------------------
//
// The setting is a flat object of one enum + one double, so a compact hand-rolled
// writer + a tolerant minimal reader is the simplest thing that works (mirroring
// bank_model's dependency-free JSON choice). manualMs is emitted with 17 significant
// digits (%.17g) — the shortest form that round-trips every IEEE-754 double exactly —
// so deserialize(serialize(x)) == x holds bit-for-bit. deserialize is deliberately
// forgiving: any parse failure returns nullopt so the caller falls back to a default,
// exactly as an absent ext-state key does.

namespace {

// The persisted integer for a mode. Stable forever (stored in the .rpp): never
// renumber these values or an already-saved project reads back the wrong mode.
int modeToInt(TailMode m) {
    switch (m) {
        case TailMode::None:   return 0;
        case TailMode::Auto:   return 1;
        case TailMode::Manual: return 2;
    }
    return 0;
}

std::optional<TailMode> modeFromInt(int v) {
    switch (v) {
        case 0: return TailMode::None;
        case 1: return TailMode::Auto;
        case 2: return TailMode::Manual;
        default: return std::nullopt;  // unknown enumerant -> malformed -> default
    }
}

// Find the value token following `"key":` in `json`. Returns a pointer just past the
// colon (skipping whitespace) or nullptr if the key is absent. Minimal: the writer
// emits exactly one flat object with unique keys, so a substring search is sufficient
// and there is no nesting to confuse it.
const char* valueAfterKey(const std::string& json, const char* key) {
    const std::string needle = std::string("\"") + key + "\"";
    const std::size_t pos = json.find(needle);
    if (pos == std::string::npos) return nullptr;
    const char* p = json.c_str() + pos + needle.size();
    while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') ++p;
    if (*p != ':') return nullptr;
    ++p;
    while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') ++p;
    return p;
}

}  // namespace

std::string serializeTailSetting(const TailSetting& setting) {
    char buf[128];
    std::snprintf(buf, sizeof(buf), "{\"mode\":%d,\"manualMs\":%.17g}",
                  modeToInt(setting.mode), setting.manualMs);
    return std::string(buf);
}

std::optional<TailSetting> deserializeTailSetting(const std::string& json) {
    const char* modeTok = valueAfterKey(json, "mode");
    const char* msTok = valueAfterKey(json, "manualMs");
    if (!modeTok || !msTok) return std::nullopt;  // absent key -> malformed -> default

    char* end = nullptr;
    errno = 0;
    const long modeVal = std::strtol(modeTok, &end, 10);
    if (end == modeTok || errno != 0) return std::nullopt;
    const std::optional<TailMode> mode = modeFromInt(static_cast<int>(modeVal));
    if (!mode) return std::nullopt;

    end = nullptr;
    errno = 0;
    const double ms = std::strtod(msTok, &end);
    if (end == msTok || errno != 0) return std::nullopt;

    TailSetting out;
    out.mode = *mode;
    out.manualMs = ms;
    return out;
}

} // namespace reasampler
