#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/Role.h>

#include <xrpl/json/json_value.h>
#include <xrpl/json/json_writer.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/digest.h>
#include <xrpl/protocol/jss.h>

#include <boost/algorithm/string.hpp>

#include <set>
#include <string_view>
#include <unordered_map>

namespace xrpl {

namespace detail {

class ServerDefinitions
{
private:
    static std::string
    // translate e.g. STI_LEDGERENTRY to LedgerEntry
    translate(std::string const& inp);

    uint256 defsHash_;
    Json::Value defs_;

public:
    ServerDefinitions();

    bool
    hashMatches(uint256 hash) const
    {
        return defsHash_ == hash;
    }

    Json::Value const&
    get() const
    {
        return defs_;
    }
};

std::string
ServerDefinitions::translate(std::string const& inp)
{
    auto replace = [&](std::string_view oldStr, std::string_view newStr) -> std::string {
        std::string out = inp;
        boost::replace_all(out, oldStr, newStr);
        return out;
    };

    // TODO: use string::contains with C++23
    auto contains = [&](std::string_view s) -> bool { return inp.find(s) != std::string::npos; };

    if (contains("UINT"))
    {
        if (contains("512") || contains("384") || contains("256") || contains("192") ||
            contains("160") || contains("128"))
        {
            return replace("UINT", "Hash");
        }

        return replace("UINT", "UInt");
    }

    static std::unordered_map<std::string_view, std::string_view> const replacements{
        {"OBJECT", "STObject"},
        {"ARRAY", "STArray"},
        {"ACCOUNT", "AccountID"},
        {"LEDGERENTRY", "LedgerEntry"},
        {"NOTPRESENT", "NotPresent"},
        {"PATHSET", "PathSet"},
        {"VL", "Blob"},
        {"XCHAIN_BRIDGE", "XChainBridge"},
    };

    if (auto const& it = replacements.find(inp); it != replacements.end())
    {
        return std::string(it->second);
    }

    std::string out;
    size_t pos = 0;
    std::string inpToProcess = inp;

    // convert snake_case to CamelCase
    for (;;)
    {
        pos = inpToProcess.find("_");
        if (pos == std::string::npos)
            pos = inpToProcess.size();
        std::string token = inpToProcess.substr(0, pos);
        if (token.size() > 1)
        {
            boost::algorithm::to_lower(token);
            token.data()[0] -= ('a' - 'A');
            out += token;
        }
        else
        {
            out += token;
        }
        if (pos == inpToProcess.size())
            break;
        inpToProcess = inpToProcess.substr(pos + 1);
    }
    return out;
};

ServerDefinitions::ServerDefinitions() : defs_{Json::objectValue}
{
    // populate SerializedTypeID names and values
    defs_[jss::TYPES] = Json::objectValue;

    defs_[jss::TYPES]["Done"] = -1;
    std::map<int32_t, std::string> typeMap{{-1, "Done"}};
    for (auto const& [rawName, typeValue] : sTypeMap)
    {
        std::string typeName = translate(std::string(rawName).substr(4) /* remove STI_ */);
        defs_[jss::TYPES][typeName] = typeValue;
        typeMap[typeValue] = typeName;
    }

    // populate LedgerEntryType names and values
    defs_[jss::LEDGER_ENTRY_TYPES] = Json::objectValue;
    defs_[jss::LEDGER_ENTRY_TYPES][jss::Invalid] = -1;

    for (auto const& f : LedgerFormats::getInstance())
    {
        defs_[jss::LEDGER_ENTRY_TYPES][f.getName()] = f.getType();
    }

    // populate SField serialization data
    defs_[jss::FIELDS] = Json::arrayValue;

    uint32_t i = 0;
    {
        Json::Value a = Json::arrayValue;
        a[0U] = "Generic";
        Json::Value v = Json::objectValue;
        v[jss::nth] = 0;
        v[jss::isVLEncoded] = false;
        v[jss::isSerialized] = false;
        v[jss::isSigningField] = false;
        v[jss::type] = "Unknown";
        a[1U] = v;
        defs_[jss::FIELDS][i++] = a;
    }

    {
        Json::Value a = Json::arrayValue;
        a[0U] = "Invalid";
        Json::Value v = Json::objectValue;
        v[jss::nth] = -1;
        v[jss::isVLEncoded] = false;
        v[jss::isSerialized] = false;
        v[jss::isSigningField] = false;
        v[jss::type] = "Unknown";
        a[1U] = v;
        defs_[jss::FIELDS][i++] = a;
    }

    {
        Json::Value a = Json::arrayValue;
        a[0U] = "ObjectEndMarker";
        Json::Value v = Json::objectValue;
        v[jss::nth] = 1;
        v[jss::isVLEncoded] = false;
        v[jss::isSerialized] = true;
        v[jss::isSigningField] = true;
        v[jss::type] = "STObject";
        a[1U] = v;
        defs_[jss::FIELDS][i++] = a;
    }

    {
        Json::Value a = Json::arrayValue;
        a[0U] = "ArrayEndMarker";
        Json::Value v = Json::objectValue;
        v[jss::nth] = 1;
        v[jss::isVLEncoded] = false;
        v[jss::isSerialized] = true;
        v[jss::isSigningField] = true;
        v[jss::type] = "STArray";
        a[1U] = v;
        defs_[jss::FIELDS][i++] = a;
    }

    {
        Json::Value a = Json::arrayValue;
        a[0U] = "taker_gets_funded";
        Json::Value v = Json::objectValue;
        v[jss::nth] = 258;
        v[jss::isVLEncoded] = false;
        v[jss::isSerialized] = false;
        v[jss::isSigningField] = false;
        v[jss::type] = "Amount";
        a[1U] = v;
        defs_[jss::FIELDS][i++] = a;
    }

    {
        Json::Value a = Json::arrayValue;
        a[0U] = "taker_pays_funded";
        Json::Value v = Json::objectValue;
        v[jss::nth] = 259;
        v[jss::isVLEncoded] = false;
        v[jss::isSerialized] = false;
        v[jss::isSigningField] = false;
        v[jss::type] = "Amount";
        a[1U] = v;
        defs_[jss::FIELDS][i++] = a;
    }

    for (auto const& [code, field] : xrpl::SField::getKnownCodeToField())
    {
        if (field->fieldName.empty())
            continue;

        Json::Value innerObj = Json::objectValue;

        uint32_t type = field->fieldType;

        innerObj[jss::nth] = field->fieldValue;

        // whether the field is variable-length encoded this means that the length is included
        // before the content
        innerObj[jss::isVLEncoded] =
            (type == 7U /* Blob */ || type == 8U /* AccountID */ || type == 19U /* Vector256 */);

        // whether the field is included in serialization
        innerObj[jss::isSerialized] =
            (type < 10000 && field->fieldName != "hash" &&
             field->fieldName !=
                 "index");  // hash, index, TRANSACTION, LEDGER_ENTRY, VALIDATION, METADATA

        // whether the field is included in serialization when signing
        innerObj[jss::isSigningField] = field->shouldInclude(false);

        innerObj[jss::type] = typeMap[type];

        Json::Value innerArray = Json::arrayValue;
        innerArray[0U] = field->fieldName;
        innerArray[1U] = innerObj;

        defs_[jss::FIELDS][i++] = innerArray;
    }

    // populate TER code names and values
    defs_[jss::TRANSACTION_RESULTS] = Json::objectValue;

    for (auto const& [code, terInfo] : transResults())
    {
        defs_[jss::TRANSACTION_RESULTS][terInfo.first] = code;
    }

    // populate TxType names and values
    defs_[jss::TRANSACTION_TYPES] = Json::objectValue;
    defs_[jss::TRANSACTION_TYPES][jss::Invalid] = -1;
    for (auto const& f : TxFormats::getInstance())
    {
        defs_[jss::TRANSACTION_TYPES][f.getName()] = f.getType();
    }

    // populate TxFormats
    defs_[jss::TRANSACTION_FORMATS] = Json::objectValue;

    defs_[jss::TRANSACTION_FORMATS][jss::common] = Json::arrayValue;
    auto txCommonFields = std::set<std::string>();
    for (auto const& element : TxFormats::getCommonFields())
    {
        Json::Value elementObj = Json::objectValue;
        elementObj[jss::name] = element.sField().getName();
        elementObj[jss::optionality] = element.style();
        defs_[jss::TRANSACTION_FORMATS][jss::common].append(elementObj);
        txCommonFields.insert(element.sField().getName());
    }

    for (auto const& format : TxFormats::getInstance())
    {
        auto const& soTemplate = format.getSOTemplate();
        Json::Value templateArray = Json::arrayValue;
        for (auto const& element : soTemplate)
        {
            if (txCommonFields.contains(element.sField().getName()))
                continue;  // skip common fields, already added
            Json::Value elementObj = Json::objectValue;
            elementObj[jss::name] = element.sField().getName();
            elementObj[jss::optionality] = element.style();
            templateArray.append(elementObj);
        }
        defs_[jss::TRANSACTION_FORMATS][format.getName()] = templateArray;
    }

    // populate LedgerFormats
    defs_[jss::LEDGER_ENTRY_FORMATS] = Json::objectValue;
    defs_[jss::LEDGER_ENTRY_FORMATS][jss::common] = Json::arrayValue;
    auto ledgerCommonFields = std::set<std::string>();
    for (auto const& element : LedgerFormats::getCommonFields())
    {
        Json::Value elementObj = Json::objectValue;
        elementObj[jss::name] = element.sField().getName();
        elementObj[jss::optionality] = element.style();
        defs_[jss::LEDGER_ENTRY_FORMATS][jss::common].append(elementObj);
        ledgerCommonFields.insert(element.sField().getName());
    }
    for (auto const& format : LedgerFormats::getInstance())
    {
        auto const& soTemplate = format.getSOTemplate();
        Json::Value templateArray = Json::arrayValue;
        for (auto const& element : soTemplate)
        {
            if (ledgerCommonFields.contains(element.sField().getName()))
                continue;  // skip common fields, already added
            Json::Value elementObj = Json::objectValue;
            elementObj[jss::name] = element.sField().getName();
            elementObj[jss::optionality] = element.style();
            templateArray.append(elementObj);
        }
        defs_[jss::LEDGER_ENTRY_FORMATS][format.getName()] = templateArray;
    }

    defs_[jss::TRANSACTION_FLAGS] = Json::objectValue;
    for (auto const& [name, value] : getAllTxFlags())
    {
        Json::Value txObj = Json::objectValue;
        for (auto const& [flagName, flagValue] : value)
        {
            txObj[flagName] = flagValue;
        }
        defs_[jss::TRANSACTION_FLAGS][name] = txObj;
    }

    defs_[jss::LEDGER_ENTRY_FLAGS] = Json::objectValue;
    for (auto const& [name, value] : getAllLedgerFlags())
    {
        Json::Value ledgerObj = Json::objectValue;
        for (auto const& [flagName, flagValue] : value)
        {
            ledgerObj[flagName] = flagValue;
        }
        defs_[jss::LEDGER_ENTRY_FLAGS][name] = ledgerObj;
    }

    defs_[jss::ACCOUNT_SET_FLAGS] = Json::objectValue;
    for (auto const& [name, value] : getAsfFlagMap())
    {
        defs_[jss::ACCOUNT_SET_FLAGS][name] = value;
    }

    // generate hash
    {
        std::string const out = Json::FastWriter().write(defs_);
        defsHash_ = xrpl::sha512Half(xrpl::Slice{out.data(), out.size()});
        defs_[jss::hash] = to_string(defsHash_);
    }
}

}  // namespace detail

Json::Value
doServerDefinitions(RPC::JsonContext& context)
{
    auto& params = context.params;

    uint256 hash;
    if (params.isMember(jss::hash))
    {
        if (!params[jss::hash].isString() || !hash.parseHex(params[jss::hash].asString()))
            return RPC::invalid_field_error(jss::hash);
    }

    static detail::ServerDefinitions const defs{};
    if (defs.hashMatches(hash))
    {
        Json::Value jv = Json::objectValue;
        jv[jss::hash] = to_string(hash);
        return jv;
    }
    return defs.get();
}

}  // namespace xrpl
