#include "owned_manifest.h"

#include <cctype>
#include <cstdio>

// owned_manifest implementation.
//
// JSON is hand-rolled and self-contained (project convention: the pure core is
// dependency-free — no third-party JSON lib, mirror of bank_model / bank_book /
// tail_control). The shape is a single object with one string array:
//
//     {"owned":["reasampler_bank/a.wav","reasampler_bank/b.wav"]}
//
// so a compact writer + a focused string-array parser is all it needs — far smaller
// than bank_model's full recursive-descent parser, because there is exactly one key
// and one value kind.

namespace reasampler {

// ---------------------------------------------------------------------------
// path invariant (mirror of bank_model's isAbsolutePath)
// ---------------------------------------------------------------------------

namespace {

// Any leading '/' or '\' (POSIX root / UNC), or a leading <alpha>: (Windows drive,
// incl. drive-relative "C:foo") is absolute. Same rejection bank_model applies to
// Sample.relativePath — the manifest holds the SAME kind of path, so the invariant
// must match exactly (a path the index accepts must be recordable, and vice versa).
bool isAbsolutePath(const std::string& p) {
    if (p.empty()) return false;
    if (p[0] == '/' || p[0] == '\\') return true;
    if (p.size() >= 2 && std::isalpha(static_cast<unsigned char>(p[0])) && p[1] == ':')
        return true;
    return false;
}

}  // namespace

// ---------------------------------------------------------------------------
// mutation / query
// ---------------------------------------------------------------------------

ManifestAddResult OwnedFileManifest::add(const std::string& relativePath) {
    if (relativePath.empty()) return ManifestAddResult::RejectedEmptyPath;
    if (isAbsolutePath(relativePath)) return ManifestAddResult::RejectedAbsolutePath;
    if (contains(relativePath)) return ManifestAddResult::AlreadyPresent;
    paths_.push_back(relativePath);
    return ManifestAddResult::Added;
}

bool OwnedFileManifest::contains(const std::string& relativePath) const {
    for (const auto& p : paths_)
        if (p == relativePath) return true;
    return false;
}

// ---------------------------------------------------------------------------
// JSON writer
// ---------------------------------------------------------------------------

namespace {

void writeEscaped(std::string& out, const std::string& s) {
    out += '"';
    for (char c : s) {
        switch (c) {
        case '"':  out += "\\\""; break;
        case '\\': out += "\\\\"; break;
        case '\b': out += "\\b";  break;
        case '\f': out += "\\f";  break;
        case '\n': out += "\\n";  break;
        case '\r': out += "\\r";  break;
        case '\t': out += "\\t";  break;
        default:
            if (static_cast<unsigned char>(c) < 0x20) {
                char buf[8];
                std::snprintf(buf, sizeof(buf), "\\u%04x",
                              static_cast<unsigned char>(c));
                out += buf;
            } else {
                out += c;
            }
        }
    }
    out += '"';
}

}  // namespace

std::string OwnedFileManifest::serialize() const {
    std::string out = "{\"owned\":[";
    for (std::size_t i = 0; i < paths_.size(); ++i) {
        if (i) out += ',';
        writeEscaped(out, paths_[i]);
    }
    out += "]}";
    return out;
}

// ---------------------------------------------------------------------------
// JSON parser (string-array only)
// ---------------------------------------------------------------------------

namespace {

class Parser {
public:
    explicit Parser(const std::string& s) : s_(s) {}

    // Parse the manifest object into `out`. Tolerates unknown keys (forward-compat)
    // and requires the "owned" value to be an array of strings.
    bool parseManifest(OwnedFileManifest& out);

private:
    const std::string& s_;
    std::size_t pos_ = 0;

    bool eof() const { return pos_ >= s_.size(); }

    void skipWs() {
        while (!eof()) {
            char c = s_[pos_];
            if (c == ' ' || c == '\t' || c == '\n' || c == '\r') ++pos_;
            else break;
        }
    }

    bool consume(char c) {
        skipWs();
        if (eof() || s_[pos_] != c) return false;
        ++pos_;
        return true;
    }

    bool parseString(std::string& out);
    bool parseStringArray(std::vector<std::string>& out);
    bool skipValue();  // for forward-compat unknown keys
};

// Parses a JSON string literal (with the escapes our writer emits, plus \uXXXX for
// control chars). Positioned before the opening quote (skips leading whitespace).
bool Parser::parseString(std::string& out) {
    skipWs();
    if (eof() || s_[pos_] != '"') return false;
    ++pos_;
    out.clear();
    while (!eof()) {
        char c = s_[pos_++];
        if (c == '"') return true;
        if (c == '\\') {
            if (eof()) return false;
            char e = s_[pos_++];
            switch (e) {
            case '"':  out += '"';  break;
            case '\\': out += '\\'; break;
            case '/':  out += '/';  break;
            case 'b':  out += '\b'; break;
            case 'f':  out += '\f'; break;
            case 'n':  out += '\n'; break;
            case 'r':  out += '\r'; break;
            case 't':  out += '\t'; break;
            case 'u': {
                auto readHex4 = [&](unsigned int& cp) -> bool {
                    if (pos_ + 4 > s_.size()) return false;
                    cp = 0;
                    for (int i = 0; i < 4; ++i) {
                        char h = s_[pos_++];
                        cp <<= 4;
                        if (h >= '0' && h <= '9') cp |= static_cast<unsigned>(h - '0');
                        else if (h >= 'a' && h <= 'f') cp |= static_cast<unsigned>(h - 'a' + 10);
                        else if (h >= 'A' && h <= 'F') cp |= static_cast<unsigned>(h - 'A' + 10);
                        else return false;
                    }
                    return true;
                };

                unsigned int hi = 0;
                if (!readHex4(hi)) return false;

                unsigned int codePoint = hi;
                if (hi >= 0xD800 && hi <= 0xDBFF) {
                    if (pos_ + 6 > s_.size()) return false;
                    if (s_[pos_] != '\\' || s_[pos_ + 1] != 'u') return false;
                    pos_ += 2;
                    unsigned int lo = 0;
                    if (!readHex4(lo)) return false;
                    if (lo < 0xDC00 || lo > 0xDFFF) return false;
                    codePoint = 0x10000 + ((hi - 0xD800) << 10) + (lo - 0xDC00);
                } else if (hi >= 0xDC00 && hi <= 0xDFFF) {
                    return false;  // unpaired low surrogate
                }

                if (codePoint <= 0x7F) {
                    out += static_cast<char>(codePoint);
                } else if (codePoint <= 0x7FF) {
                    out += static_cast<char>(0xC0 | (codePoint >> 6));
                    out += static_cast<char>(0x80 | (codePoint & 0x3F));
                } else if (codePoint <= 0xFFFF) {
                    out += static_cast<char>(0xE0 | (codePoint >> 12));
                    out += static_cast<char>(0x80 | ((codePoint >> 6) & 0x3F));
                    out += static_cast<char>(0x80 | (codePoint & 0x3F));
                } else {
                    out += static_cast<char>(0xF0 | (codePoint >> 18));
                    out += static_cast<char>(0x80 | ((codePoint >> 12) & 0x3F));
                    out += static_cast<char>(0x80 | ((codePoint >> 6) & 0x3F));
                    out += static_cast<char>(0x80 | (codePoint & 0x3F));
                }
                break;
            }
            default: return false;
            }
        } else {
            out += c;
        }
    }
    return false;  // unterminated string
}

bool Parser::parseStringArray(std::vector<std::string>& out) {
    if (!consume('[')) return false;
    skipWs();
    if (consume(']')) return true;  // empty array
    for (;;) {
        std::string s;
        if (!parseString(s)) return false;
        out.push_back(std::move(s));
        skipWs();
        if (consume(',')) continue;
        if (consume(']')) return true;
        return false;  // neither separator nor terminator — malformed
    }
}

// Skip a single JSON value (string / array / object / bare scalar) so an unknown key
// does not abort the parse. Minimal: enough for forward-compat siblings we don't know.
bool Parser::skipValue() {
    skipWs();
    if (eof()) return false;
    char c = s_[pos_];
    if (c == '"') {
        std::string tmp;
        return parseString(tmp);
    }
    if (c == '[' || c == '{') {
        // Balance nested brackets of either kind, ignoring bracket chars inside
        // strings. Enough to step over an unknown nested value; not a full validator.
        int depth = 0;
        bool inStr = false;
        while (!eof()) {
            char d = s_[pos_];
            if (inStr) {
                if (d == '\\') { pos_ += 2; continue; }
                if (d == '"') inStr = false;
                ++pos_;
                continue;
            }
            if (d == '"') { inStr = true; ++pos_; continue; }
            if (d == '[' || d == '{') ++depth;
            else if (d == ']' || d == '}') {
                --depth;
                if (depth == 0) { ++pos_; return true; }
            }
            ++pos_;
        }
        return false;
    }
    // bare scalar (number / true / false / null) — read to the next structural char
    while (!eof()) {
        char d = s_[pos_];
        if (d == ',' || d == '}' || d == ']' || d == ' ' || d == '\t' ||
            d == '\n' || d == '\r')
            break;
        ++pos_;
    }
    return true;
}

bool Parser::parseManifest(OwnedFileManifest& out) {
    if (!consume('{')) return false;
    skipWs();
    if (consume('}')) return true;  // empty object -> empty manifest
    for (;;) {
        std::string key;
        if (!parseString(key)) return false;
        if (!consume(':')) return false;
        if (key == "owned") {
            std::vector<std::string> paths;
            if (!parseStringArray(paths)) return false;
            for (auto& p : paths) {
                // Feed through add() so the persisted invariants (dedup, reject
                // empty/absolute) are re-asserted on load — a hand-edited or corrupt
                // blob cannot smuggle an absolute or duplicate path into the manifest.
                out.add(p);
            }
        } else {
            if (!skipValue()) return false;  // forward-compat: tolerate unknown keys
        }
        skipWs();
        if (consume(',')) continue;
        if (consume('}')) return true;
        return false;
    }
}

}  // namespace

std::optional<OwnedFileManifest> OwnedFileManifest::deserialize(const std::string& json) {
    OwnedFileManifest m;
    Parser p(json);
    if (!p.parseManifest(m)) return std::nullopt;
    return m;
}

} // namespace reasampler
