#include "dps/json.hpp"

#include <cctype>
#include <iomanip>
#include <sstream>

namespace dps {

namespace {

class Parser {
public:
    explicit Parser(const std::string &text) : text_(text) {}

    Json parse() {
        skip_ws();
        Json value = parse_value();
        skip_ws();
        if (pos_ != text_.size()) {
            throw std::runtime_error("unexpected trailing json content");
        }
        return value;
    }

private:
    Json parse_value() {
        if (match("null")) {
            return Json();
        }
        if (match("true")) {
            return Json(true);
        }
        if (match("false")) {
            return Json(false);
        }
        if (peek() == '"') {
            return Json(parse_string());
        }
        if (peek() == '[') {
            return parse_array();
        }
        if (peek() == '{') {
            return parse_object();
        }
        return Json(parse_number());
    }

    Json parse_array() {
        consume('[');
        Json::array_t items;
        skip_ws();
        if (peek() == ']') {
            consume(']');
            return Json(std::move(items));
        }
        while (true) {
            skip_ws();
            items.push_back(parse_value());
            skip_ws();
            if (peek() == ']') {
                consume(']');
                break;
            }
            consume(',');
        }
        return Json(std::move(items));
    }

    Json parse_object() {
        consume('{');
        Json::object_t object;
        skip_ws();
        if (peek() == '}') {
            consume('}');
            return Json(std::move(object));
        }
        while (true) {
            skip_ws();
            std::string key = parse_string();
            skip_ws();
            consume(':');
            skip_ws();
            object.emplace(std::move(key), parse_value());
            skip_ws();
            if (peek() == '}') {
                consume('}');
                break;
            }
            consume(',');
        }
        return Json(std::move(object));
    }

    std::string parse_string() {
        consume('"');
        std::ostringstream out;
        while (pos_ < text_.size()) {
            char ch = text_[pos_++];
            if (ch == '"') {
                return out.str();
            }
            if (ch != '\\') {
                out << ch;
                continue;
            }
            if (pos_ >= text_.size()) {
                throw std::runtime_error("unterminated escape");
            }
            char esc = text_[pos_++];
            switch (esc) {
                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': {
                    if (pos_ + 4 > text_.size()) {
                        throw std::runtime_error("bad unicode escape");
                    }
                    const std::string hex = text_.substr(pos_, 4);
                    pos_ += 4;
                    const unsigned int code = static_cast<unsigned int>(std::stoul(hex, nullptr, 16));
                    if (code <= 0x7F) {
                        out << static_cast<char>(code);
                    } else if (code <= 0x7FF) {
                        out << static_cast<char>(0xC0 | ((code >> 6) & 0x1F));
                        out << static_cast<char>(0x80 | (code & 0x3F));
                    } else {
                        out << static_cast<char>(0xE0 | ((code >> 12) & 0x0F));
                        out << static_cast<char>(0x80 | ((code >> 6) & 0x3F));
                        out << static_cast<char>(0x80 | (code & 0x3F));
                    }
                    break;
                }
                default:
                    throw std::runtime_error("unsupported escape");
            }
        }
        throw std::runtime_error("unterminated string");
    }

    double parse_number() {
        const std::size_t start = pos_;
        if (peek() == '-') {
            ++pos_;
        }
        while (std::isdigit(static_cast<unsigned char>(peek()))) {
            ++pos_;
        }
        if (peek() == '.') {
            ++pos_;
            while (std::isdigit(static_cast<unsigned char>(peek()))) {
                ++pos_;
            }
        }
        if (peek() == 'e' || peek() == 'E') {
            ++pos_;
            if (peek() == '+' || peek() == '-') {
                ++pos_;
            }
            while (std::isdigit(static_cast<unsigned char>(peek()))) {
                ++pos_;
            }
        }
        return std::stod(text_.substr(start, pos_ - start));
    }

    char peek() const {
        return pos_ < text_.size() ? text_[pos_] : '\0';
    }

    void consume(char expected) {
        if (peek() != expected) {
            throw std::runtime_error("unexpected token");
        }
        ++pos_;
    }

    bool match(const char *literal) {
        const std::size_t len = std::char_traits<char>::length(literal);
        if (text_.compare(pos_, len, literal) == 0) {
            pos_ += len;
            return true;
        }
        return false;
    }

    void skip_ws() {
        while (pos_ < text_.size() && std::isspace(static_cast<unsigned char>(text_[pos_]))) {
            ++pos_;
        }
    }

    const std::string &text_;
    std::size_t pos_ = 0;
};

std::string escape_string(const std::string &value) {
    std::ostringstream out;
    for (const unsigned char ch : value) {
        switch (ch) {
            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 (ch < 0x20) {
                    out << "\\u" << std::hex << std::setw(4) << std::setfill('0') << static_cast<int>(ch);
                } else {
                    out << static_cast<char>(ch);
                }
        }
    }
    return out.str();
}

}  // namespace

Json::Json() : value_(nullptr) {}
Json::Json(std::nullptr_t) : value_(nullptr) {}
Json::Json(bool value) : value_(value) {}
Json::Json(int value) : value_(static_cast<double>(value)) {}
Json::Json(long long value) : value_(static_cast<double>(value)) {}
Json::Json(double value) : value_(value) {}
Json::Json(const char *value) : value_(std::string(value == nullptr ? "" : value)) {}
Json::Json(const std::string &value) : value_(value) {}
Json::Json(std::string &&value) : value_(std::move(value)) {}
Json::Json(const array_t &value) : value_(value) {}
Json::Json(array_t &&value) : value_(std::move(value)) {}
Json::Json(const object_t &value) : value_(value) {}
Json::Json(object_t &&value) : value_(std::move(value)) {}

Json Json::parse(const std::string &text) {
    return Parser(text).parse();
}

Json Json::object() {
    return Json(object_t{});
}

Json Json::array() {
    return Json(array_t{});
}

bool Json::is_null() const { return std::holds_alternative<std::nullptr_t>(value_); }
bool Json::is_bool() const { return std::holds_alternative<bool>(value_); }
bool Json::is_number() const { return std::holds_alternative<double>(value_); }
bool Json::is_string() const { return std::holds_alternative<std::string>(value_); }
bool Json::is_array() const { return std::holds_alternative<array_t>(value_); }
bool Json::is_object() const { return std::holds_alternative<object_t>(value_); }

bool Json::as_bool() const { return std::get<bool>(value_); }
double Json::as_number() const { return std::get<double>(value_); }
int Json::as_int() const { return static_cast<int>(as_number()); }
long long Json::as_int64() const { return static_cast<long long>(as_number()); }
const std::string &Json::as_string() const { return std::get<std::string>(value_); }
const Json::array_t &Json::as_array() const { return std::get<array_t>(value_); }
const Json::object_t &Json::as_object() const { return std::get<object_t>(value_); }
Json::array_t &Json::as_array() { return std::get<array_t>(value_); }
Json::object_t &Json::as_object() { return std::get<object_t>(value_); }

bool Json::contains(const std::string &key) const {
    if (!is_object()) {
        return false;
    }
    return as_object().find(key) != as_object().end();
}

const Json &Json::at(const std::string &key) const {
    return as_object().at(key);
}

Json &Json::operator[](const std::string &key) {
    if (!is_object()) {
        value_ = object_t{};
    }
    return as_object()[key];
}

const Json &Json::operator[](const std::string &key) const {
    static const Json kNull;
    if (!is_object()) {
        return kNull;
    }
    const auto it = as_object().find(key);
    return it == as_object().end() ? kNull : it->second;
}

const Json &Json::at(std::size_t index) const {
    return as_array().at(index);
}

Json &Json::at(std::size_t index) {
    return as_array().at(index);
}

void Json::push_back(const Json &value) {
    if (!is_array()) {
        value_ = array_t{};
    }
    as_array().push_back(value);
}

std::size_t Json::size() const {
    if (is_array()) {
        return as_array().size();
    }
    if (is_object()) {
        return as_object().size();
    }
    return 0;
}

std::string Json::dump() const {
    if (is_null()) {
        return "null";
    }
    if (is_bool()) {
        return as_bool() ? "true" : "false";
    }
    if (is_number()) {
        std::ostringstream out;
        out << std::setprecision(15) << as_number();
        std::string result = out.str();
        if (result.find('.') != std::string::npos) {
            while (!result.empty() && result.back() == '0') {
                result.pop_back();
            }
            if (!result.empty() && result.back() == '.') {
                result.pop_back();
            }
        }
        return result.empty() ? "0" : result;
    }
    if (is_string()) {
        return "\"" + escape_string(as_string()) + "\"";
    }
    if (is_array()) {
        std::ostringstream out;
        out << "[";
        bool first = true;
        for (const auto &item : as_array()) {
            if (!first) {
                out << ",";
            }
            first = false;
            out << item.dump();
        }
        out << "]";
        return out.str();
    }
    std::ostringstream out;
    out << "{";
    bool first = true;
    for (const auto &entry : as_object()) {
        if (!first) {
            out << ",";
        }
        first = false;
        out << "\"" << escape_string(entry.first) << "\":" << entry.second.dump();
    }
    out << "}";
    return out.str();
}

std::string Json::to_string_or(const std::string &fallback) const {
    return is_string() ? as_string() : fallback;
}

int Json::to_int_or(int fallback) const {
    return is_number() ? as_int() : fallback;
}

bool Json::to_bool_or(bool fallback) const {
    return is_bool() ? as_bool() : fallback;
}

}  // namespace dps
