#include "json/dom.h"
#include "json/json.h"
#include "json/stats.h"
#include "json/selector.h"
#include <cstring>
#include <memory>
#include <iostream>
#include <iomanip>
#include <cmath>
#include "json/rapidjson_includes.h"

#define STATIC /* decorator for static functions, remove so that backtrace symbols include these */

#define CHECK_DOCUMENT_SIZE_LIMIT(ctx, curr_doc_size, input_json_val_size) \
{ \
    if (ctx != nullptr && !(ValkeyModule_GetContextFlags(ctx) & VALKEYMODULE_CTX_FLAGS_REPLICATED) && \
        json_get_max_document_size() > 0 && (curr_doc_size + input_json_val_size > json_get_max_document_size())) { \
         ValkeyModule_Log(ctx, "debug", \
         "Document size limit is exceeded. The attempted operation will result in a document with %lu bytes of " \
         "memory size.", curr_doc_size + input_json_val_size); \
        return JSONUTIL_DOCUMENT_SIZE_LIMIT_EXCEEDED; \
    } \
}

#define CHECK_DOCUMENT_PATH_LIMIT(ctx, selector, new_val) \
{ \
    size_t __depth_would_be = selector.getMaxPathDepth() + new_val.GetMaxDepth(); \
    if (__depth_would_be > json_get_max_path_limit()) { \
         ValkeyModule_Log(ctx, "debug", \
         "Document path limit is exceeded. The attempted operation will result in a document with %lu nesting" \
         " levels.", __depth_would_be); \
        return JSONUTIL_DOCUMENT_PATH_LIMIT_EXCEEDED; \
    } else { \
        jsonstats_update_max_depth_ever_seen(__depth_would_be); \
    } \
}

// the one true allocator
RapidJsonAllocator allocator;

/**
 * We want to avoid all redundant creations of an allocator -- for performance reasons.
 * So we use the constructor to detect that situation. It's free after startup. If you trip
 * this trap, then you let a rapidjson allocator instance get defaulted to 0 somewhere in your code.
 */
RapidJsonAllocator::RapidJsonAllocator() {
    ValkeyModule_Assert(this == &allocator);  // Only this one is allowed :)
}

JValue& dom_get_value(JDocument &doc) {
    return doc.GetJValue();
}

JParser& JParser::Parse(const char *json, size_t len) {
    int64_t begin_val = jsonstats_begin_track_mem();
    RJParser::Parse(json, len);
    int64_t delta = jsonstats_end_track_mem(begin_val);
    ValkeyModule_Assert(delta >= 0);
    allocated_size = static_cast<size_t>(delta);
    return *this;
}

JParser& JParser::Parse(const std::string_view &sv) {
    return Parse(sv.data(), sv.length());
}

jsn::string validate(const JDocument *doc) {
    std::string s = doc->GetJValue().Validate();
    return jsn::string(s.c_str(), s.length());
}

STATIC JDocument *create_doc() {
    return new JDocument();
}

void dom_free_doc(JDocument *doc) {
    ValkeyModule_Assert(doc != nullptr);
    delete doc;
}

size_t dom_get_doc_size(const JDocument *doc) {
    return doc->size;
}

size_t dom_get_doc_size_no_opt(const JDocument *doc) {
    return sizeof(JDocument) + doc->GetJValue().ComputeMallocedSize();
}

void dom_set_doc_size(JDocument *doc, const size_t size) {
    doc->size = size;
}

size_t dom_get_bucket_id(const JDocument *doc) {
    return doc->bucket_id;
}

void dom_set_bucket_id(JDocument *doc, const uint32_t bucket_id) {
    doc->bucket_id = bucket_id;
}

JsonUtilCode dom_parse(ValkeyModuleCtx *ctx, const char *json_buf, const size_t buf_len, JDocument **doc) {
    *doc = nullptr;
    // begin tracking memory
    int64_t begin_val = jsonstats_begin_track_mem();

    {
        JParser parser;
        if (parser.Parse(json_buf, buf_len).HasParseError()) {
            return parser.GetParseErrorCode();
        }
        CHECK_DOCUMENT_SIZE_LIMIT(ctx, size_t(0), parser.GetJValueSize())
        *doc = create_doc();
        (*doc)->SetJValue(parser.GetJValue());
        jsonstats_update_max_depth_ever_seen(parser.GetMaxDepth());
    }

    // end tracking memory
    int64_t delta = jsonstats_end_track_mem(begin_val);
    dom_set_doc_size(*doc, dom_get_doc_size(*doc) + delta);

    return JSONUTIL_SUCCESS;
}

STATIC bool has_custom_format(const PrintFormat *format) {
    return (format != nullptr && (format->indent != nullptr || format->space != nullptr || format->newline != nullptr));
}
/**
 * Serialize a value.
 * @param json OUTPUT param, serialized string is appended to the param.
 */
STATIC void serialize_value(const JValue &val, size_t initialLevel, const PrintFormat *format,
                            rapidjson::StringBuffer &oss) {
    size_t max_depth = 0;
    rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(oss);
    if (has_custom_format(format)) {
        if (format && format->newline) writer.SetNewline(std::string_view(format->newline, strlen(format->newline)));
        if (format && format->indent) writer.SetIndent(std::string_view(format->indent, strlen(format->indent)));
        if (format && format->space) writer.SetSpace(std::string_view(format->space, strlen(format->space)));
        writer.SetInitialLevel(initialLevel);
        val.Accept(writer);
        jsonstats_update_max_depth_ever_seen(writer.GetMaxDepth());
    } else {
        writer.FastWrite(val, &max_depth);
        jsonstats_update_max_depth_ever_seen(max_depth);
    }
}

STATIC void serialize_value(const JValue &val, size_t initialLevel, const PrintFormat *format, ReplyBuffer& oss) {
    size_t max_depth = 0;
    rapidjson::PrettyWriter<ReplyBuffer> writer(oss);
    if (has_custom_format(format)) {
        if (format && format->newline) writer.SetNewline(std::string_view(format->newline, strlen(format->newline)));
        if (format && format->indent) writer.SetIndent(std::string_view(format->indent, strlen(format->indent)));
        if (format && format->space) writer.SetSpace(std::string_view(format->space, strlen(format->space)));
        writer.SetInitialLevel(initialLevel);
        val.Accept(writer);
        jsonstats_update_max_depth_ever_seen(writer.GetMaxDepth());
    } else {
        writer.FastWrite(val, &max_depth);
        jsonstats_update_max_depth_ever_seen(max_depth);
    }
}

void dom_serialize(JDocument *doc, const PrintFormat *format, rapidjson::StringBuffer &oss) {
    serialize_value(*(doc), 0, format, oss);
}

void dom_serialize_value(const JValue &val, const PrintFormat *format, rapidjson::StringBuffer &oss) {
    serialize_value(val, 0, format, oss);
}

JsonUtilCode dom_verify_value(ValkeyModuleCtx *ctx, JDocument *doc, const char *json_path, const char *new_val_json,
                           size_t new_val_size) {
    Selector selector;
    JsonUtilCode rc = selector.prepareSetValues(doc->GetJValue(), json_path);
    if (rc != JSONUTIL_SUCCESS) return rc;
    
    JParser new_val;
    if (new_val.Parse(new_val_json, new_val_size).HasParseError()) {
        return new_val.GetParseErrorCode();
    }

    CHECK_DOCUMENT_PATH_LIMIT(ctx, selector, new_val);
    CHECK_DOCUMENT_SIZE_LIMIT(ctx, doc->size, new_val.GetJValueSize());
    return JSONUTIL_SUCCESS;
}

JsonUtilCode dom_set_value(ValkeyModuleCtx *ctx, JDocument *doc, const char *json_path, const char *new_val_json,
                           size_t new_val_size, const bool is_create_only, const bool is_update_only) {
    if (is_create_only && is_update_only) return JSONUTIL_NX_XX_SHOULD_BE_MUTUALLY_EXCLUSIVE;

    Selector selector;
    JsonUtilCode rc = selector.prepareSetValues(doc->GetJValue(), json_path);
    if (rc != JSONUTIL_SUCCESS) return rc;

    if (is_create_only && selector.hasUpdates()) return JSONUTIL_NX_XX_CONDITION_NOT_SATISFIED;
    if (is_update_only && selector.hasInserts()) return JSONUTIL_NX_XX_CONDITION_NOT_SATISFIED;

    JParser new_val;
    if (new_val.Parse(new_val_json, new_val_size).HasParseError()) {
        return new_val.GetParseErrorCode();
    }

    CHECK_DOCUMENT_PATH_LIMIT(ctx, selector, new_val)
    CHECK_DOCUMENT_SIZE_LIMIT(ctx, doc->size, new_val.GetJValueSize())

    selector.commit(new_val);
    return JSONUTIL_SUCCESS;
}

template<typename OutputBuffer>
STATIC void PutString(OutputBuffer& oss, const char *str) {
    while (*str) oss.Put(*str++);
}

template<typename OutputBuffer>
STATIC void PutEscapedString(OutputBuffer& oss, const char *str) {
    JValue tmp;
    tmp.SetString(str, strlen(str));
    serialize_value(tmp, 0, nullptr, oss);
}

// Build stringified JSON array directly from a vector of values.
template<typename T>
STATIC void build_json_array(const jsn::vector<JValue*> &values, const PrintFormat *format, T &oss) {
    bool has_format = has_custom_format(format);
    oss.Put('[');
    if (has_format && format->newline) PutString(oss, format->newline);
    for (size_t i=0; i < values.size(); i++) {
        if (has_format && format->indent) PutString(oss, format->indent);
        serialize_value(*values[i], 1, format, oss);
        if (i < values.size() - 1) oss.Put(',');
        if (has_format && format->newline) PutString(oss, format->newline);
    }
    oss.Put(']');
}

template STATIC void build_json_array(const jsn::vector<JValue*> &values, const PrintFormat *format, ReplyBuffer &oss);
template STATIC void build_json_array(const jsn::vector<JValue*> &values, const PrintFormat *format,
        rapidjson::StringBuffer &oss);

template<typename T>
JsonUtilCode dom_get_value_as_str(JDocument *doc, const char *json_path, const PrintFormat *format,
                                  T &oss, const bool update_stats) {
    Selector selector;
    JsonUtilCode rc = selector.getValues(*doc, json_path);
    if (rc != JSONUTIL_SUCCESS) {
        if (selector.isLegacyJsonPathSyntax()) return rc;
        // For v2 path, return error code only if it's a syntax error.
        if (selector.isSyntaxError(rc)) return rc;
    }

    jsn::vector<JValue*> values;
    selector.getSelectedValues(values);

    // If legacy path, return either the first value, or NONEXISTENT error if no value is found.
    if (selector.isLegacyJsonPathSyntax()) {
        if (values.empty()) {
            return JSONUTIL_JSON_PATH_NOT_EXIST;
        } else {
            serialize_value(*values[0], 0, format, oss);
            // update stats
            if (update_stats) jsonstats_update_stats_on_read(oss.GetLength());
            return JSONUTIL_SUCCESS;
        }
    }

    // v2 path: return an array of values.
    if (values.empty()) {
        // return an empty array
        oss.Put('[');
        oss.Put(']');
    } else {
        // Multiple values are returned to the client as a JSON array.
        build_json_array(values, format, oss);
    }

    // update stats
    if (update_stats) jsonstats_update_stats_on_read(oss.GetLength());
    return JSONUTIL_SUCCESS;
}

template JsonUtilCode dom_get_value_as_str(JDocument *doc, const char *json_path, const PrintFormat *format,
                                           ReplyBuffer &oss, const bool update_stats);
template JsonUtilCode dom_get_value_as_str(JDocument *doc, const char *json_path, const PrintFormat *format,
                                           rapidjson::StringBuffer &oss, const bool update_stats);

STATIC void appendPathAndValue(const char *key, const JValue &val, const bool isLastPath,
                               const bool has_format, const PrintFormat *format, ReplyBuffer &oss) {
    if (has_format && format->indent) PutString(oss, format->indent);
    PutEscapedString(oss, key);
    oss.Put(':');
    if (has_format && format->space) PutString(oss, format->space);
    serialize_value(val, 1, format, oss);
    if (!isLastPath) oss.Put(',');
    if (has_format && format->newline) PutString(oss, format->newline);
}

STATIC void appendPathAndValues(const char *key, const jsn::vector<JValue*> &values, const bool isLastPath,
                                const bool has_format, const PrintFormat *format, ReplyBuffer &oss) {
    if (has_format && format->indent) PutString(oss, format->indent);
    PutEscapedString(oss, key);
    oss.Put(':');
    if (has_format && format->space) PutString(oss, format->space);
    oss.Put('[');
    if (has_format && format->newline) PutString(oss, format->newline);

    for (size_t i=0; i < values.size(); i++) {
        if (has_format && format->indent) {
            PutString(oss, format->indent);
            PutString(oss, format->indent);
        }
        serialize_value(*values[i], 2, format, oss);
        if (i < values.size() - 1) oss.Put(',');
        if (has_format && format->newline) PutString(oss, format->newline);
    }

    if (has_format && format->indent) PutString(oss, format->indent);
    oss.Put(']');
    if (!isLastPath) oss.Put(',');
    if (has_format && format->newline) PutString(oss, format->newline);
}

STATIC JsonUtilCode buildJsonForMultiPaths(JDocument *doc, const char **paths, const int num_paths,
                                           const bool is_v2path, const PrintFormat *format,
                                           ReplyBuffer &oss) {
    bool has_format = has_custom_format(format);
    Selector selector(is_v2path);
    JsonUtilCode rc;
    oss.Put('{');
    if (has_format && format->newline) PutString(oss, format->newline);
    for (int i = 0; i < num_paths; i++) {
        rc = selector.getValues(*doc, paths[i]);
        if (rc != JSONUTIL_SUCCESS) {
            if (!is_v2path) return rc;
            // For v2 path, return error code only if it's a syntax error.
            if (selector.isSyntaxError(rc)) return rc;
        }

        jsn::vector<JValue*> values;
        selector.getSelectedValues(values);

        if (!is_v2path) {  // legacy path
            if (values.empty()) {
                return JSONUTIL_JSON_PATH_NOT_EXIST;
            } else {
                appendPathAndValue(paths[i], *values[0], (i == num_paths - 1), has_format, format, oss);
            }
        } else {
            appendPathAndValues(paths[i], values, (i == num_paths - 1), has_format, format, oss);
        }
    }
    oss.Put('}');
    return JSONUTIL_SUCCESS;
}

JsonUtilCode dom_get_values_as_str(JDocument *doc, const char **paths, const int num_paths,
                                   PrintFormat *format, ReplyBuffer &oss, const bool update_stats) {
    // If there are multiple paths mixed with both v1 and v2 syntax, the returned value should conform to the V2
    // behavior (returning an array of values).
    // We can't start processing the first element until we know if we should conform to V1 or V2 behavior.
    // Example:
    //   cmd1: json.get wikipedia .foo .address
    //   cmd2:  json.get wikipedia .foo $.address
    // The expected behavior is: Cmd1 should fail because .foo does not exist, while cmd2 should succeed because
    // overall the command should conform to V2 behavior (as the 2nd path is V2 path). Cmd2 should return the
    // following result:
    //   127.0.0.1:6379> json.get wikipedia .foo $.address
    //   {"$.address":[{"street":"21 2nd Street","city":"New York","state":"NY","zipcode":"10021-3100"}],".foo":[]}
    //
    // Without the pre-knowledge of V1 vs V2, both commands would fail, because when the selector first runs ".foo",
    // it would think it is V1 and returns an error. The loop below would then exit without attempting the 2nd path.
    bool is_v2path = Selector::has_at_least_one_v2path(paths, num_paths);

    // Values at multiple paths are combined to form a serialized JSON object string, in which each path is a key.
    JsonUtilCode rc = buildJsonForMultiPaths(doc, paths, num_paths, is_v2path, format, oss);
    if (rc != JSONUTIL_SUCCESS) return rc;

    // update stats
    if (update_stats) jsonstats_update_stats_on_read(oss.GetLength());
    return JSONUTIL_SUCCESS;
}

JsonUtilCode dom_delete_value(JDocument *doc, const char *json_path, size_t &num_vals_deleted) {
    Selector selector;
    return selector.deleteValues(doc->GetJValue(), json_path, num_vals_deleted);
}

// check if there is at least one number value
STATIC bool has_number_value(jsn::vector<JValue*> &values) {
    for (auto &v : values) {
        if (v->IsNumber()) return true;
    }
    return false;
}

JsonUtilCode dom_increment_by(JDocument *doc, const char *json_path, const JValue *incr_by,
                              jsn::vector<double> &out_vals, bool &is_v2_path) {
    out_vals.clear();
    Selector selector;
    JsonUtilCode rc = selector.getValues(doc->GetJValue(), json_path);
    is_v2_path = selector.isV2Path;
    if (rc != JSONUTIL_SUCCESS) return rc;

    jsn::vector<JValue*> values;
    selector.getSelectedValues(values);

    // Legacy path:
    if (!is_v2_path) {
        // return NONEXISTENT error if no value is selected
        if (values.empty()) return JSONUTIL_JSON_PATH_NOT_EXIST;
        // return WRONGTYPE error if no number value is selected
        if (!has_number_value(values)) return JSONUTIL_JSON_ELEMENT_NOT_NUMBER;
    }

    for (auto &val : selector.getUniqueResultSet()) {
        if (val.first->IsNumber()) {
            if (val.first->IsInt64() && incr_by->IsInt64()) {
                // All are integers
                int64_t res;
                rc = jsonutil_add_int64(val.first->GetInt64(), incr_by->GetInt64(), &res);
                if (rc == JSONUTIL_SUCCESS) {
                    val.first->SetInt64(res);
                    out_vals.push_back(res);
                    continue;
                }
            }

            double res;
            rc = jsonutil_add_double(val.first->GetDouble(), incr_by->GetDouble(), &res);
            if (rc != JSONUTIL_SUCCESS) return rc;
            char double_string[BUF_SIZE_DOUBLE_JSON];
            size_t len = jsonutil_double_to_string(res, double_string, sizeof(double_string));
            val.first->SetDouble(double_string, len, allocator);

            out_vals.push_back(res);
        } else {
            out_vals.push_back(std::nan("NaN"));  // indicates the value is not number
        }
    }

    return JSONUTIL_SUCCESS;
}

JsonUtilCode dom_multiply_by(JDocument *doc, const char *json_path, const JValue *mult_by,
                             jsn::vector<double> &out_vals, bool &is_v2_path) {
    out_vals.clear();
    Selector selector;
    JsonUtilCode rc = selector.getValues(doc->GetJValue(), json_path);
    is_v2_path = selector.isV2Path;
    if (rc != JSONUTIL_SUCCESS) return rc;

    jsn::vector<JValue*> values;
    selector.getSelectedValues(values);

    // Legacy path:
    if (!is_v2_path) {
        // return NONEXISTENT error if no value is selected
        if (values.empty()) return JSONUTIL_JSON_PATH_NOT_EXIST;
        // return WRONGTYPE error if no number value is selected
        if (!has_number_value(values)) return JSONUTIL_JSON_ELEMENT_NOT_NUMBER;
    }

    for (auto &val : selector.getUniqueResultSet()) {
        if (val.first->IsNumber()) {
            double res;
            rc = jsonutil_multiply_double(val.first->GetDouble(), mult_by->GetDouble(), &res);
            if (rc != JSONUTIL_SUCCESS) return rc;

            if (jsonutil_is_int64(res)) {
                val.first->SetInt64(static_cast<int64_t>(res));
            } else {
                char double_string[BUF_SIZE_DOUBLE_JSON];
                size_t len = jsonutil_double_to_string(res, double_string, sizeof(double_string));
                val.first->SetDouble(double_string, len, allocator);
            }

            out_vals.push_back(res);
        } else {
            out_vals.push_back(std::nan("NaN"));  // indicates the value is not number
        }
    }
    return JSONUTIL_SUCCESS;
}

// check if there is at least one boolean value
STATIC bool has_boolean_value(jsn::vector<JValue*> &values) {
    for (auto &v : values) {
        if (v->IsBool()) return true;
    }
    return false;
}

JsonUtilCode dom_toggle(JDocument *doc, const char *path, jsn::vector<int> &vec, bool &is_v2_path) {
    vec.clear();
    Selector selector;
    JsonUtilCode rc = selector.getValues(doc->GetJValue(), path);
    is_v2_path = selector.isV2Path;
    if (rc != JSONUTIL_SUCCESS) {
        if (selector.isLegacyJsonPathSyntax()) return rc;
        // For v2 path, return error code only if it's a syntax error.
        if (selector.isSyntaxError(rc)) return rc;
    }

    jsn::vector<JValue*> values;
    selector.getSelectedValues(values);

    // Legacy path
    if (!is_v2_path) {
        // return NONEXISTENT error if no value is selected
        if (values.empty()) return JSONUTIL_JSON_PATH_NOT_EXIST;
        // return WRONGTYPE error if no boolean value is selected
        if (!has_boolean_value(values)) return JSONUTIL_JSON_ELEMENT_NOT_BOOL;
    }

    for (auto &v : selector.getUniqueResultSet()) {
        if (v.first->IsBool()) {
            bool res = v.first->GetBool();
            res = !res;
            v.first->SetBool(res);
            vec.push_back(res? 1 : 0);
        } else {
            vec.push_back(-1);  // -1 means the source value is not boolean
        }
    }
    return JSONUTIL_SUCCESS;
}

// check if there is at least one string value
STATIC bool has_string_value(jsn::vector<JValue*> &values) {
    for (auto &v : values) {
        if (v->IsString()) return true;
    }
    return false;
}

JsonUtilCode dom_string_length(JDocument *doc, const char *path, jsn::vector<size_t> &vec, bool &is_v2_path) {
    vec.clear();
    Selector selector;
    JsonUtilCode rc = selector.getValues(doc->GetJValue(), path);
    is_v2_path = selector.isV2Path;
    if (rc != JSONUTIL_SUCCESS) {
        if (selector.isLegacyJsonPathSyntax()) return rc;
        // For v2 path, return error code only if it's a syntax error.
        if (selector.isSyntaxError(rc)) return rc;
    }

    jsn::vector<JValue*> values;
    selector.getSelectedValues(values);

    // Legacy path
    if (!is_v2_path) {
        // return NONEXISTENT error if no value is selected
        if (values.empty()) return JSONUTIL_JSON_PATH_NOT_EXIST;
        // return WRONGTYPE error if no string value is selected
        if (!has_string_value(values)) return JSONUTIL_JSON_ELEMENT_NOT_STRING;
    }

    for (auto &v : values) {
        if (v->IsString()) {
            vec.push_back(v->GetStringLength());
        } else {
            vec.push_back(SIZE_MAX);  // indicates non-string value
        }
    }
    return JSONUTIL_SUCCESS;
}

JsonUtilCode dom_string_append(JDocument *doc, const char *path, const char *json, const size_t json_len,
                               jsn::vector<size_t> &vec, bool &is_v2_path) {
    vec.clear();
    Selector selector;
    JsonUtilCode rc = selector.getValues(doc->GetJValue(), path);
    is_v2_path = selector.isV2Path;
    if (rc != JSONUTIL_SUCCESS) return rc;

    jsn::vector<JValue*> values;
    selector.getSelectedValues(values);

    // Legacy path:
    if (!is_v2_path) {
        // return NONEXISTENT error if no value is selected
        if (values.empty()) return JSONUTIL_JSON_PATH_NOT_EXIST;
        // return WRONGTYPE error if no string value is selected
        if (!has_string_value(values)) return JSONUTIL_JSON_ELEMENT_NOT_STRING;
    }

    // verify the input json string is a valid
    JParser appendVal;
    if (appendVal.Parse(json, json_len).HasParseError()) return appendVal.GetParseErrorCode();
    if (!appendVal.GetJValue().IsString()) return JSONUTIL_VALUE_NOT_STRING;

    jsn::string str_append = jsn::string(appendVal.GetString());
    for (auto &v : selector.getUniqueResultSet()) {
        if (v.first->IsString()) {
            jsn::string new_string = jsn::string(v.first->GetString()) + str_append;
            v.first->SetString(new_string.c_str(), new_string.length(), allocator);
            vec.push_back(new_string.length());
        } else {
            vec.push_back(SIZE_MAX);  // indicates non-string value
        }
    }
    return JSONUTIL_SUCCESS;
}

// check if there is at least one object value
STATIC bool has_object_value(jsn::vector<JValue*> &values) {
    for (auto &v : values) {
        if (v->IsObject()) return true;
    }
    return false;
}

JsonUtilCode dom_object_length(JDocument *doc, const char *path, jsn::vector<size_t> &vec, bool &is_v2_path) {
    vec.clear();
    Selector selector;
    JsonUtilCode rc = selector.getValues(doc->GetJValue(), path);
    is_v2_path = selector.isV2Path;
    if (rc != JSONUTIL_SUCCESS) {
        if (selector.isLegacyJsonPathSyntax()) return rc;
        // For v2 path, return error code only if it's a syntax error.
        if (selector.isSyntaxError(rc)) return rc;
    }

    jsn::vector<JValue*> values;
    selector.getSelectedValues(values);

    // Legacy path
    if (!is_v2_path) {
        // return NONEXISTENT error if no value is selected
        if (values.empty()) return JSONUTIL_JSON_PATH_NOT_EXIST;
        // return WRONGTYPE error if no object value is selected
        if (!has_object_value(values)) return JSONUTIL_JSON_ELEMENT_NOT_OBJECT;
    }

    for (auto &v : values) {
        if (v->IsObject()) {
            vec.push_back(v->MemberCount());
        } else {
            vec.push_back(SIZE_MAX);  // indicates non-object value
        }
    }
    return JSONUTIL_SUCCESS;
}

JsonUtilCode dom_object_keys(JDocument *doc, const char *path,
                             jsn::vector<jsn::vector<jsn::string>> &vec, bool &is_v2_path) {
    vec.clear();
    Selector selector;
    JsonUtilCode rc = selector.getValues(doc->GetJValue(), path);
    is_v2_path = selector.isV2Path;
    if (rc != JSONUTIL_SUCCESS) {
        if (selector.isLegacyJsonPathSyntax()) return rc;
        // For v2 path, return error code only if it's a syntax error.
        if (selector.isSyntaxError(rc)) return rc;
    }

    jsn::vector<JValue*> values;
    selector.getSelectedValues(values);

    // Legacy path
    if (!is_v2_path) {
        // return NONEXISTENT error if no value is selected
        if (values.empty()) return JSONUTIL_JSON_PATH_NOT_EXIST;
        // return WRONGTYPE error if no object value is selected
        if (!has_object_value(values)) return JSONUTIL_JSON_ELEMENT_NOT_OBJECT;
    }

    for (auto &v : values) {
        jsn::vector<jsn::string> keys;
        if (v->IsObject()) {
            for (auto &m : v->GetObject()) {
                keys.push_back(std::move(jsn::string(m.name.GetString(), m.name.GetStringLength())));
            }
        }
        vec.push_back(keys);
    }
    return JSONUTIL_SUCCESS;
}

// check if there is at least one array value
STATIC bool has_array_value(jsn::vector<JValue*> &values) {
    for (auto &v : values) {
        if (v->IsArray()) return true;
    }
    return false;
}

JsonUtilCode dom_array_length(JDocument *doc, const char *path, jsn::vector<size_t> &vec, bool &is_v2_path) {
    vec.clear();
    Selector selector;
    JsonUtilCode rc = selector.getValues(doc->GetJValue(), path);
    is_v2_path = selector.isV2Path;
    if (rc != JSONUTIL_SUCCESS) {
        if (selector.isLegacyJsonPathSyntax()) return rc;
        // For v2 path, return error code only if it's a syntax error.
        if (selector.isSyntaxError(rc)) return rc;
    }

    jsn::vector<JValue*> values;
    selector.getSelectedValues(values);

    // Legacy path
    if (!is_v2_path) {
        // return NONEXISTENT error if no value is selected
        if (values.empty()) return JSONUTIL_JSON_PATH_NOT_EXIST;
        // return WRONGTYPE error if no array value is selected
        if (!has_array_value(values)) return JSONUTIL_JSON_ELEMENT_NOT_ARRAY;
    }

    for (auto &v : values) {
        if (v->IsArray()) {
            vec.push_back(v->Size());
        } else {
            vec.push_back(SIZE_MAX);  // indicates non-array value
        }
    }
    return JSONUTIL_SUCCESS;
}

JsonUtilCode dom_array_append(ValkeyModuleCtx *ctx, JDocument *doc, const char *path,
                              const char **jsons, size_t *json_lens, const size_t num_values,
                              jsn::vector<size_t> &vec, bool &is_v2_path) {
    vec.clear();
    Selector selector;
    JsonUtilCode rc = selector.getValues(doc->GetJValue(), path);
    is_v2_path = selector.isV2Path;
    if (rc != JSONUTIL_SUCCESS) return rc;

    jsn::vector<JValue*> values;
    selector.getSelectedValues(values);

    // Legacy path:
    if (!is_v2_path) {
        // return NONEXISTENT error if no value is selected
        if (values.empty()) return JSONUTIL_JSON_PATH_NOT_EXIST;
        // return WRONGTYPE error if no array value is selected
        if (!has_array_value(values)) return JSONUTIL_JSON_ELEMENT_NOT_ARRAY;
    }

    // parse json values
    jsn::vector<JParser> appendVals(num_values);
    size_t totalJValueSize = 0;
    for (size_t i=0; i < num_values; i++) {
        if (appendVals[i].Parse(jsons[i], json_lens[i]).HasParseError()) {
            return appendVals[i].GetParseErrorCode();
        }
        CHECK_DOCUMENT_PATH_LIMIT(ctx, selector, appendVals[i])
        totalJValueSize += appendVals[i].GetJValueSize();
    }
    CHECK_DOCUMENT_SIZE_LIMIT(ctx, doc->size, totalJValueSize)

    for (auto &v : selector.getUniqueResultSet()) {
        if (v.first->IsArray()) {
            for (size_t i=0; i < num_values; i++) {
                // Need to make a copy of the value because after the first call of JValue::PushBack,
                // the object is moved and can no longer be pushed into anther array.
                JValue copy(appendVals[i], allocator);
                v.first->PushBack(copy, allocator);
            }
            vec.push_back(v.first->Size());
        } else {
            vec.push_back(SIZE_MAX);  // indicates non-array value
        }
    }
    return JSONUTIL_SUCCESS;
}

STATIC void internal_array_pop(JValue &arrVal, int64_t index, jsn::vector<rapidjson::StringBuffer> &vec,
                               rapidjson::StringBuffer &oss) {
    // Convert negative index to positive
    int64_t size = arrVal.Size();
    if (index < 0) index = (arrVal.Size() == 0 ? 0 : size + index);

    // Out-of-bound index is rounded to respective array bounds
    if (index >= size) index = size - 1;
    if (index < 0) index = 0;

    serialize_value(arrVal[index], 0, nullptr, oss);
    arrVal.Erase(arrVal.Begin() + index);
    vec.push_back(std::move(oss));
}

JsonUtilCode dom_array_pop(JDocument *doc, const char *path, int64_t index,
                           jsn::vector<rapidjson::StringBuffer> &vec, bool &is_v2_path) {
    vec.clear();
    Selector selector;
    JsonUtilCode rc = selector.getValues(doc->GetJValue(), path);
    is_v2_path = selector.isV2Path;
    if (rc != JSONUTIL_SUCCESS) return rc;

    jsn::vector<JValue*> values;
    selector.getSelectedValues(values);

    // Legacy path:
    if (!is_v2_path) {
        // return NONEXISTENT error if no value is selected
        if (values.empty()) return JSONUTIL_JSON_PATH_NOT_EXIST;
        // return WRONGTYPE error if no array value is selected
        if (!has_array_value(values)) return JSONUTIL_JSON_ELEMENT_NOT_ARRAY;
    }

    for (auto &v : selector.getUniqueResultSet()) {
        rapidjson::StringBuffer oss;
        if (v.first->IsArray()) {
            if (v.first->Empty()) {
                vec.push_back(std::move(oss));  // empty array, oss is empty
            } else {
                internal_array_pop(*v.first, index, vec, oss);
            }
        } else {
            vec.push_back(std::move(oss));  // non-array value, oss is empty
        }
    }

    return JSONUTIL_SUCCESS;
}

STATIC JsonUtilCode internal_array_insert(JValue &arrVal, jsn::vector<JParser> &insertVals,
                                          const size_t num_values, int64_t index, jsn::vector<size_t> &vec) {
    size_t size = arrVal.Size();

    // Negative index values are interpreted as starting from the end.
    if (index < 0) index = (arrVal.Size() == 0 ? 0 : size + index);

    // Return error if the index is out of bounds.
    // If index is size-1, we are inserting before the last element.
    // If index is size, we are appending to the array.
    if (index < 0 || index > static_cast<int64_t>(size)) return JSONUTIL_INDEX_OUT_OF_ARRAY_BOUNDARIES;

    // append num_values empty values
    for (size_t i=0; i < num_values; i++) {
        JValue empty;
        arrVal.PushBack(empty, allocator);
    }

    // shift values [index+1..end-num_values] to the right by num_values positions
    for (int64_t i = arrVal.Size() - 1; i >= static_cast<int64_t>(num_values) + index; i--) {
        arrVal[i] = arrVal[i - num_values];
    }

    // overwrite values [index..index+num_values-1]
    for (int64_t i=index; i < index + static_cast<int64_t>(num_values); i++) {
        // Need to make a copy of the value to insert because after the value is assigned,
        // is is moved and can no longer be assigned into anther value.
        JValue copy(insertVals[i - index].GetJValue(), allocator);
        arrVal[i] = copy;
    }

    vec.push_back(arrVal.Size());
    return JSONUTIL_SUCCESS;
}

JsonUtilCode dom_array_insert(ValkeyModuleCtx *ctx, JDocument *doc, const char *path, int64_t index,
                              const char **jsons, size_t *json_lens, const size_t num_values,
                              jsn::vector<size_t> &vec, bool &is_v2_path) {
    vec.clear();
    Selector selector;
    JsonUtilCode rc = selector.getValues(doc->GetJValue(), path);
    is_v2_path = selector.isV2Path;
    if (rc != JSONUTIL_SUCCESS) return rc;

    jsn::vector<JValue*> values;
    selector.getSelectedValues(values);

    // Legacy path:
    if (!is_v2_path) {
        // return NONEXISTENT error if no value is selected
        if (values.empty()) return JSONUTIL_JSON_PATH_NOT_EXIST;
        // return WRONGTYPE error if no array value is selected
        if (!has_array_value(values)) return JSONUTIL_JSON_ELEMENT_NOT_ARRAY;
    }

    // parse json values
    jsn::vector<JParser> insertVals(num_values);
    size_t totalJValueSize = 0;
    for (size_t i=0; i < num_values; i++) {
        if (insertVals[i].Parse(jsons[i], json_lens[i]).HasParseError()) {
            return insertVals[i].GetParseErrorCode();
        }
        CHECK_DOCUMENT_PATH_LIMIT(ctx, selector, insertVals[i])
        totalJValueSize += insertVals[i].GetJValueSize();
    }
    CHECK_DOCUMENT_SIZE_LIMIT(ctx, doc->size, totalJValueSize)

    for (auto &v : selector.getUniqueResultSet()) {
        if (v.first->IsArray()) {
            rc = internal_array_insert(*v.first, insertVals, num_values, index, vec);
            if (rc != JSONUTIL_SUCCESS) return rc;
        } else {
            vec.push_back(SIZE_MAX);  // indicates non-array value
        }
    }
    return JSONUTIL_SUCCESS;
}

STATIC void internal_array_trim(JValue &arrVal, int64_t start, int64_t stop, jsn::vector<size_t> &vec) {
    int64_t size = static_cast<int64_t>(arrVal.Size());
    if (size == 0) {
        vec.push_back(0);
        return;
    }

    // if start < 0, set it to 0.
    if (start < 0) start = 0;

    // if stop >= size, set it to size-1
    if (stop >= size) stop = size - 1;

    if (start >= size || start > stop) {
        // If start >= size or start > stop, empty the array and return *new_len as 0.
        arrVal.Erase(arrVal.Begin(), arrVal.End());
        vec.push_back(0);
        return;
    }

    if (stop < size-1)
        arrVal.Erase(arrVal.Begin() + stop + 1, arrVal.Begin() + size);
    if (start > 0)
        arrVal.Erase(arrVal.Begin(), arrVal.Begin() + start);

    vec.push_back(arrVal.Size());
}

JsonUtilCode dom_array_trim(JDocument *doc, const char *path, int64_t start, int64_t stop,
                            jsn::vector<size_t> &vec, bool &is_v2_path) {
    vec.clear();
    Selector selector;
    JsonUtilCode rc = selector.getValues(doc->GetJValue(), path);
    is_v2_path = selector.isV2Path;
    if (rc != JSONUTIL_SUCCESS) return rc;

    jsn::vector<JValue*> values;
    selector.getSelectedValues(values);

    // Legacy path:
    if (!is_v2_path) {
        // return NONEXISTENT error if no value is selected
        if (values.empty()) return JSONUTIL_JSON_PATH_NOT_EXIST;
        // return WRONGTYPE error if no array value is selected
        if (!has_array_value(values)) return JSONUTIL_JSON_ELEMENT_NOT_ARRAY;
    }

    for (auto &v : selector.getUniqueResultSet()) {
        if (v.first->IsArray()) {
            internal_array_trim(*v.first, start, stop, vec);
        } else {
            vec.push_back(SIZE_MAX);  // indicates non-array value
        }
    }
    return JSONUTIL_SUCCESS;
}

JsonUtilCode dom_clear(JDocument *doc, const char *path, size_t &elements_cleared) {
    elements_cleared = 0;
    Selector selector;
    JsonUtilCode rc = selector.getValues(doc->GetJValue(), path);
    if (rc != JSONUTIL_SUCCESS) return rc;

    for (auto &v : selector.getUniqueResultSet()) {
        if (v.first->IsArray()) {
            if (!v.first->Empty()) {
                v.first->Erase(v.first->Begin(), v.first->End());
                elements_cleared++;
            }
        } else if (v.first->IsObject()) {
            if (!v.first->ObjectEmpty()) {
                v.first->RemoveAllMembers();
                elements_cleared++;
            }
        } else if (v.first->IsBool()) {
            if (v.first->IsTrue()) {
                v.first->SetBool(false);
                elements_cleared++;
            }
        } else if (v.first->IsString()) {
            if (v.first->GetStringLength() > 0) {
                v.first->SetString("");
                elements_cleared++;
            }
        } else if (v.first->IsInt()) {
            if (v.first->GetInt() != 0) {
                v.first->SetInt(0);
                elements_cleared++;
            }
        } else if (v.first->IsInt64()) {
            if (v.first->GetInt64() !=0) {
                v.first->SetInt64(0);
                elements_cleared++;
            }
        } else if (v.first->IsUint()) {
            if (v.first->GetUint() != 0) {
                v.first->SetUint(0);
                elements_cleared++;
            }
        } else if (v.first->IsUint64()) {
            if (v.first->GetUint64() != 0) {
                v.first->SetUint64(0);
                elements_cleared++;
            }
        } else if (v.first->IsDouble()) {
            if (v.first->GetDouble() < 0.0 || v.first->GetDouble() > 0.0) {
                v.first->SetDouble("0.0", 3, allocator);
                elements_cleared++;
            }
        }
    }
    return JSONUTIL_SUCCESS;
}

STATIC void internal_array_index_of(const JValue &arrVal, const JValue &inputVal, int64_t start, int64_t stop,
                                    jsn::vector<int64_t> &vec) {
    int64_t size = static_cast<int64_t>(arrVal.Size());
    if (size == 0) {
        vec.push_back(-1);
        return;
    }

    // if stop == 0 or -1, the last element is included.
    if (stop == 0 || stop == -1) stop = size;

    // if stop > size, set it to size.
    if (stop > size) stop = size;

    if (start > stop) {
        vec.push_back(-1);
        return;
    }

    for (int64_t i=start; i < stop; i++) {
        if (arrVal[i] == inputVal) {
            vec.push_back(i);
            return;
        }
    }

    vec.push_back(-1);  // not found
}

JsonUtilCode dom_array_index_of(JDocument *doc, const char *path, const char *scalar_val,
                                const size_t scalar_val_len, int64_t start, int64_t stop,
                                jsn::vector<int64_t> &vec, bool &is_v2_path) {
    vec.clear();
    Selector selector;
    JsonUtilCode rc = selector.getValues(doc->GetJValue(), path);
    is_v2_path = selector.isV2Path;
    if (rc != JSONUTIL_SUCCESS) {
        if (selector.isLegacyJsonPathSyntax()) return rc;
        // For v2 path, return error code only if it's a syntax error.
        if (selector.isSyntaxError(rc)) return rc;
    }

    jsn::vector<JValue*> values;
    selector.getSelectedValues(values);

    // Legacy path
    if (!is_v2_path) {
        // return NONEXISTENT error if no value is selected
        if (values.empty()) return JSONUTIL_JSON_PATH_NOT_EXIST;
        // return WRONGTYPE error if no array value is selected
        if (!has_array_value(values)) return JSONUTIL_JSON_ELEMENT_NOT_ARRAY;
    }

    // if start < 0, set it to 0.
    if (start < 0) start = 0;

    // verify the input value is valid JSON
    JParser inputVal;
    if (inputVal.Parse(scalar_val, scalar_val_len).HasParseError()) return inputVal.GetParseErrorCode();

    for (auto &v : values) {
        if (v->IsArray()) {
            internal_array_index_of(*v, inputVal.GetJValue(), start, stop, vec);
        } else {
            vec.push_back(INT64_MAX);  // indicates non-array value
        }
    }
    return JSONUTIL_SUCCESS;
}

JsonUtilCode dom_value_type(JDocument *doc, const char *path, jsn::vector<jsn::string> &vec, bool &is_v2_path) {
    vec.clear();
    Selector selector;
    JsonUtilCode rc = selector.getValues(doc->GetJValue(), path);
    is_v2_path = selector.isV2Path;
    if (rc != JSONUTIL_SUCCESS) {
        if (selector.isLegacyJsonPathSyntax()) return rc;
        // For v2 path, return error code only if it's a syntax error.
        if (selector.isSyntaxError(rc)) return rc;
    }

    // Legacy path
    if (!is_v2_path) {
        // return NONEXISTENT error if no value is selected
        if (selector.getResultSet().empty()) return JSONUTIL_JSON_PATH_NOT_EXIST;
    }

    // JSON type name returned to client for command JSON.TYPE
    static const char *TYPE_NAMES[] = {"null", "boolean", "string", "number", "integer", "object", "array"};

    for (auto &v : selector.getResultSet()) {
        switch (v.first->GetType()) {
            case rapidjson::kNullType:
                vec.push_back(std::move(jsn::string(TYPE_NAMES[0])));
                break;
            case rapidjson::kTrueType:
            case rapidjson::kFalseType:
                vec.push_back(std::move(jsn::string(TYPE_NAMES[1])));
                break;
            case rapidjson::kStringType:
                vec.push_back(std::move(jsn::string(TYPE_NAMES[2])));
                break;
            case rapidjson::kNumberType: {
                if (v.first->IsDouble())
                    vec.push_back(std::move(jsn::string(TYPE_NAMES[3])));
                else
                    vec.push_back(std::move(jsn::string(TYPE_NAMES[4])));
                break;
            }
            case rapidjson::kObjectType:
                vec.push_back(std::move(jsn::string(TYPE_NAMES[5])));
                break;
            case rapidjson::kArrayType:
                vec.push_back(std::move(jsn::string(TYPE_NAMES[6])));
                break;
            default:
                ValkeyModule_Assert(false);
                break;
        }
    }
    return JSONUTIL_SUCCESS;
}

STATIC void dom_reply_with_resp_internal(ValkeyModuleCtx *ctx, const JValue& val) {
    switch (val.GetType()) {
        case rapidjson::kObjectType: {
            ValkeyModule_ReplyWithArray(ctx, VALKEYMODULE_POSTPONED_ARRAY_LEN);
            ValkeyModule_ReplyWithSimpleString(ctx, "{");
            long len = 1;
            for (auto &m : val.GetObject()) {
                ValkeyModule_ReplyWithArray(ctx, 2);
                ValkeyModule_ReplyWithStringBuffer(ctx, m.name.GetString(), m.name.GetStringLength());
                dom_reply_with_resp_internal(ctx, m.value);
                len++;
            }
            ValkeyModule_ReplySetArrayLength(ctx, len);
            break;
        }
        case rapidjson::kArrayType: {
            ValkeyModule_ReplyWithArray(ctx, VALKEYMODULE_POSTPONED_ARRAY_LEN);
            ValkeyModule_ReplyWithSimpleString(ctx, "[");
            for (auto &m : val.GetArray()) {
                dom_reply_with_resp_internal(ctx, m);
            }
            ValkeyModule_ReplySetArrayLength(ctx, val.Size() + 1);
            break;
        }
        case rapidjson::kNullType:
            ValkeyModule_ReplyWithNull(ctx);
            break;
        case rapidjson::kTrueType:
            ValkeyModule_ReplyWithSimpleString(ctx, "true");
            break;
        case rapidjson::kFalseType:
            ValkeyModule_ReplyWithSimpleString(ctx, "false");
            break;
        case rapidjson::kNumberType: {
            if (val.IsInt()) {
                ValkeyModule_ReplyWithLongLong(ctx, val.GetInt());
            } else if (val.IsInt64()) {
                ValkeyModule_ReplyWithLongLong(ctx, val.GetInt64());
            } else if (val.IsUint()) {
                ValkeyModule_ReplyWithLongLong(ctx, val.GetUint());
            } else if (val.IsUint64()) {
                ValkeyModule_ReplyWithLongLong(ctx, val.GetUint64());
            } else {
                ValkeyModule_Assert(val.IsDouble());
                char str[BUF_SIZE_DOUBLE_RAPID_JSON];
                size_t len = jsonutil_double_to_string_rapidjson(val.GetDouble(), str, sizeof(str));
                ValkeyModule_ReplyWithStringBuffer(ctx, str, len);
            }
            break;
        }
        case rapidjson::kStringType:
            ValkeyModule_ReplyWithStringBuffer(ctx, val.GetString(), val.GetStringLength());
            break;
        default:
            ValkeyModule_Assert(false);
            break;
    }
}

JsonUtilCode dom_reply_with_resp(ValkeyModuleCtx *ctx, JDocument *doc, const char *path) {
    Selector selector;
    JsonUtilCode rc = selector.getValues(doc->GetJValue(), path);
    if (rc != JSONUTIL_SUCCESS) {
        if (selector.isLegacyJsonPathSyntax()) return rc;
        // For v2 path, return error code only if it's a syntax error.
        if (selector.isSyntaxError(rc)) return rc;
    }

    if (selector.getResultSet().empty()) {
        if (!selector.isV2Path) {
            // Legacy path, return NONEXISTENT
            return JSONUTIL_JSON_PATH_NOT_EXIST;
        } else {
            // JSONPath, return empty array
            ValkeyModule_ReplyWithEmptyArray(ctx);
            return JSONUTIL_SUCCESS;
        }
    }

    if (selector.isV2Path) ValkeyModule_ReplyWithArray(ctx, selector.getResultSet().size());

    for (auto &v : selector.getResultSet()) {
        dom_reply_with_resp_internal(ctx, *v.first);
    }
    return JSONUTIL_SUCCESS;
}

JsonUtilCode dom_mem_size(JDocument *doc, const char *path, jsn::vector<size_t> &vec, bool &is_v2_path,
                          bool default_path) {
    vec.clear();
    // Optimization:
    // The size of the whole document should be obtained from the meta data attached to document object.
    if (jsonutil_is_root_path(path) && default_path) {
        vec.push_back(dom_get_doc_size(doc));
        is_v2_path = !strcmp(path, "$");
        return JSONUTIL_SUCCESS;
    }

    Selector selector;
    JsonUtilCode rc = selector.getValues(doc->GetJValue(), path);
    is_v2_path = selector.isV2Path;
    if (rc != JSONUTIL_SUCCESS) {
        if (selector.isLegacyJsonPathSyntax()) return rc;
        // For v2 path, return error code only if it's a syntax error.
        if (selector.isSyntaxError(rc)) return rc;
    }

    // Legacy path
    if (!is_v2_path) {
        // return NONEXISTENT error if no value is selected
        if (selector.getResultSet().empty()) return JSONUTIL_JSON_PATH_NOT_EXIST;
    }

    for (auto &v : selector.getResultSet()) {
        if (jsonutil_is_root_path(path)) {
            vec.push_back(dom_get_doc_size_no_opt(static_cast<const JDocument*>(v.first)));
        } else {
            vec.push_back(sizeof(*v.first) + v.first->ComputeMallocedSize());
        }
    }
    return JSONUTIL_SUCCESS;
}

STATIC size_t num_fields_internal(JValue& v) {
    size_t num_fields = 1;
    if (v.IsObject()) {
        for (auto &m : v.GetObject())
            num_fields += num_fields_internal(m.value);
    } else if (v.IsArray()) {
        for (auto &m : v.GetArray())
            num_fields += num_fields_internal(m);
    }
    return num_fields;
}

/*
 * If the top-level JSON value is a container (object or array), we want to return number of fields IN the container,
 * not to count the container itself. For a nested container object, we want to count the container itself.
 * e.g., { "address": { "street": "21 2nd Street", "city": "New York", "state": "NY", "zipcode": "10021-3100" } }.
 * If we are counting number of fields in the root doc, the address field is counted. So, there are 5 fields.
 * But If we are counting number of fields for the "address" value, the answer is 4, excluding address field itself.
 */
JsonUtilCode dom_num_fields(JDocument *doc, const char *path, jsn::vector<size_t> &vec, bool &is_v2_path) {
    vec.clear();
    Selector selector;
    JsonUtilCode rc = selector.getValues(doc->GetJValue(), path);
    is_v2_path = selector.isV2Path;
    if (rc != JSONUTIL_SUCCESS) {
        if (selector.isLegacyJsonPathSyntax()) return rc;
        // For v2 path, return error code only if it's a syntax error.
        if (selector.isSyntaxError(rc)) return rc;
    }

    // Legacy path
    if (!is_v2_path) {
        // return NONEXISTENT error if no value is selected
        if (selector.getResultSet().empty()) return JSONUTIL_JSON_PATH_NOT_EXIST;
    }

    for (auto &v : selector.getResultSet()) {
        size_t count = num_fields_internal(*v.first);
        if (v.first->IsObject() || v.first->IsArray())
            count--;  // exclude the container itself
        vec.push_back(count);
    }
    return JSONUTIL_SUCCESS;
}

STATIC void find_path_depth_internal(JValue& v, size_t d, size_t *max_depth) {
    *max_depth = std::max(d, *max_depth);
    if (v.IsObject()) {
        for (auto &m : v.GetObject())
            find_path_depth_internal(m.value, d+1, max_depth);
    } else if (v.IsArray()) {
        for (auto &m : v.GetArray())
            find_path_depth_internal(m, d+1, max_depth);
    }
}

void dom_path_depth(JDocument *doc, size_t *depth) {
    *depth = 0;
    find_path_depth_internal(doc->GetJValue(), 0, depth);
}

/*
 * Make a copy of this document
 */
JDocument *dom_copy(const JDocument *src) {
    int64_t begin_val = jsonstats_begin_track_mem();

    JDocument *dst = create_doc();
    dst->CopyFrom(*src, allocator);

    int64_t delta = jsonstats_end_track_mem(begin_val);
    ValkeyModule_Assert(delta > 0);
    dom_set_doc_size(dst, static_cast<size_t>(delta));

    return dst;
}

/*
 * RDB File Format.
 *
 * Each JValue in RDB file format has a type code followed by type-specific data
 */
enum meta_codes {
    JSON_METACODE_NULL    = 0x01,   // Nothing follows
    JSON_METACODE_STRING  = 0x02,   // Followed by the string
    JSON_METACODE_DOUBLE  = 0x04,   // Followed by the double
    JSON_METACODE_INTEGER = 0x08,   // Coded as a 64-bit Signed Integer
    JSON_METACODE_BOOLEAN = 0x10,   // Coded as the string '1' or '0'
    JSON_METACODE_OBJECT  = 0x20,   // Followed by a member count, and then N "pairs"
    JSON_METACODE_ARRAY   = 0x40,   // Followed by a element count and then n JValue elements
    JSON_METACODE_PAIR    = 0x80    // Codes an object Memory, a string(member name) and a JValue
};

//
// save a JValue, recurse as required for object and array
//
STATIC void store_JValue(ValkeyModuleIO *rdb, const JValue *val) {
    if (val->IsNull()) {
        ValkeyModule_SaveUnsigned(rdb, JSON_METACODE_NULL);
    } else if (val->IsString()) {
        ValkeyModule_SaveUnsigned(rdb, JSON_METACODE_STRING);
        ValkeyModule_SaveStringBuffer(rdb, val->GetString(), val->GetStringLength());
    } else if (val->IsNumber()) {
        if (val->IsDouble()) {
            ValkeyModule_SaveUnsigned(rdb, JSON_METACODE_DOUBLE);
            ValkeyModule_SaveDouble(rdb, val->GetDouble());
        } else {
            ValkeyModule_SaveUnsigned(rdb, JSON_METACODE_INTEGER);
            if (val->IsInt64() || val->IsInt()) {
                ValkeyModule_SaveSigned(rdb, val->GetInt64());
            } else {
                // rdb format doesn't understand unsigned, fail on numbers that aren't handled correctly
                ValkeyModule_Assert(val->GetUint64() < static_cast<uint64_t>(1L << 63));
                ValkeyModule_SaveUnsigned(rdb, val->GetUint64());
            }
        }
    } else if (val->IsFalse()) {
        ValkeyModule_SaveUnsigned(rdb, JSON_METACODE_BOOLEAN);
        ValkeyModule_SaveStringBuffer(rdb, "0", 1);
    } else if (val->IsTrue()) {
        ValkeyModule_SaveUnsigned(rdb, JSON_METACODE_BOOLEAN);
        ValkeyModule_SaveStringBuffer(rdb, "1", 1);
    } else if (val->IsObject()) {
        ValkeyModule_SaveUnsigned(rdb, JSON_METACODE_OBJECT);
        ValkeyModule_SaveUnsigned(rdb, val->MemberCount());
        for (auto m = val->MemberBegin(); m != val->MemberEnd(); ++m) {
            ValkeyModule_SaveUnsigned(rdb, JSON_METACODE_PAIR);
            ValkeyModule_SaveStringBuffer(rdb, m->name.GetString(), m->name.GetStringLength());
            store_JValue(rdb, &m->value);
        }
    } else if (val->IsArray()) {
        ValkeyModule_SaveUnsigned(rdb, JSON_METACODE_ARRAY);
        ValkeyModule_SaveUnsigned(rdb, val->Size());
        for (size_t i = 0; i < val->Size(); ++i) {
            store_JValue(rdb, &(*val)[i]);
        }
    } else {
        ValkeyModule_Assert(false);
    }
}

void dom_save(const JDocument *doc, ValkeyModuleIO *rdb, int encver) {
    switch (encver) {
        case 3: {
            rapidjson::StringBuffer oss;
            serialize_value(*(doc), 0, nullptr, oss);
            ValkeyModule_SaveStringBuffer(rdb, oss.GetString(), oss.GetLength());
            break;
        }
        case 0:
            store_JValue(rdb, doc);
            break;
        default:
            ValkeyModule_Assert(0);
            break;
    }
}

// Helper function, read string into a JValue
STATIC JValue readStringAsJValue(ValkeyModuleIO *rdb) {
    // The modern take is that we have doubles as strings
    size_t str_len;
    char *str = ValkeyModule_LoadStringBuffer(rdb, &str_len);
    if (str) {
        JValue v(str, str_len, allocator);
        ValkeyModule_Free(str);
        return v;
    } else {
        ValkeyModule_LogIOError(rdb, "error", "Unable to read string or double");
        return JValue();
    }
}

// Helper function, read legacy double into a new string double JValue
STATIC JValue readLegacyDoubleAsJValue(ValkeyModuleIO *rdb) {
    double d = ValkeyModule_LoadDouble(rdb);
    char str[BUF_SIZE_DOUBLE_JSON];
    size_t str_len = jsonutil_double_to_string(d, str, sizeof(str));
        JValue v(str, str_len, allocator, false, /* isdouble */ true);
        return v;
}

/*
 * One instance of this is passed to all recursive invokations of rdbLoadJValue
 */
typedef struct load_params {
    ValkeyModuleIO *rdb;
    unsigned nestLevel;
    JsonUtilCode status;
} load_params;

JValue rdbLoadJValue(load_params *params) {
    uint64_t code = ValkeyModule_LoadUnsigned(params->rdb);
    switch (code) {
        case JSON_METACODE_NULL:
            return JValue();
        case JSON_METACODE_STRING:
            return readStringAsJValue(params->rdb);
        case JSON_METACODE_DOUBLE:
            return readLegacyDoubleAsJValue(params->rdb);
        case JSON_METACODE_INTEGER:
            return JValue(ValkeyModule_LoadSigned(params->rdb));
        case JSON_METACODE_BOOLEAN: {
            size_t strlen;
            char *s = ValkeyModule_LoadStringBuffer(params->rdb, &strlen);
            char c = (s && strlen == 1) ? *s : 0;
            ValkeyModule_Free(s);
            switch (c) {
                case '1': return JValue(true);
                case '0': return JValue(false);
                default:
                    params->status = JSONUTIL_INVALID_RDB_FORMAT;
                    ValkeyModule_LogIOError(params->rdb, "error", "invalid boolean format");
                    return JValue();
            }
        }
        case JSON_METACODE_OBJECT: {
            uint64_t members = ValkeyModule_LoadUnsigned(params->rdb);
            JValue obj;
            obj.SetObject();
            if (params->nestLevel >= json_get_max_path_limit()) {
                ValkeyModule_LogIOError(params->rdb, "error", "document path limit exceeded");
                params->status = JSONUTIL_DOCUMENT_PATH_LIMIT_EXCEEDED;
                return JValue();
            }
            params->nestLevel++;
            while (members--) {
                uint64_t paircode = ValkeyModule_LoadUnsigned(params->rdb);
                if (paircode != JSON_METACODE_PAIR) {
                    params->status = JSONUTIL_INVALID_RDB_FORMAT;
                    ValkeyModule_LogIOError(params->rdb, "error", "Invalid pair code");
                    params->nestLevel--;
                    return JValue();
                }
                JValue key = readStringAsJValue(params->rdb);
                JValue value = rdbLoadJValue(params);
                obj.AddMember(key, value, allocator);
            }
            params->nestLevel--;
            return obj;
        }
        case JSON_METACODE_ARRAY: {
            uint64_t length = ValkeyModule_LoadUnsigned(params->rdb);
            JValue array;
            array.SetArray();
            array.Reserve(length, allocator);
            if (params->nestLevel >= json_get_max_path_limit()) {
                params->status = JSONUTIL_DOCUMENT_PATH_LIMIT_EXCEEDED;
                ValkeyModule_LogIOError(params->rdb, "error", "document path limit exceeded");
                return JValue();
            }
            params->nestLevel++;
            while (length--) {
                array.PushBack(rdbLoadJValue(params), allocator);
            }
            params->nestLevel--;
            return array;
        }
        default:
            ValkeyModule_LogIOError(params->rdb, "error", "Invalid metadata code %lx", code);
            params->status = JSONUTIL_INVALID_RDB_FORMAT;
            return JValue();
    }
}

JsonUtilCode dom_load(JDocument **doc, ValkeyModuleIO *ctx, int encver) {
    *doc = nullptr;
    ValkeyModule_Log(nullptr, "debug", "Begin dom_load, encver:%d", encver);
    switch (encver) {
        case 3: {
            //
            // New encoding, data is stored as wire-format JSON
            //
            size_t json_len;
            char *json = ValkeyModule_LoadStringBuffer(ctx, &json_len);
            if (!json) return JSONUTIL_INVALID_RDB_FORMAT;
            JsonUtilCode rc = dom_parse(nullptr, json, json_len, doc);
            ValkeyModule_Free(json);
            return rc;
        }
        case 0: {
            //
            // Encoding Version 0, Data is stored JSON node by node.
            //
            load_params params;
            params.rdb = ctx;
            params.nestLevel = 0;
            params.status = JSONUTIL_SUCCESS;
            JValue loadedValue = rdbLoadJValue(&params);
            if (params.status == JSONUTIL_SUCCESS) {
                *doc = create_doc();
                (*doc)->SetJValue(loadedValue);
            }
            return params.status;
        }
        default:
            ValkeyModule_Log(nullptr, "warning", "JSON: Unrecognized rdb encoding level %d", encver);
            return JSONUTIL_INVALID_RDB_FORMAT;
    }
}

//
// Compute Digest
//
STATIC void compute_digest(ValkeyModuleDigest *ctx, const JValue& v) {
    switch (v.GetType()) {
        case rapidjson::Type::kNullType:
            ValkeyModule_DigestAddLongLong(ctx, -1);
            ValkeyModule_DigestEndSequence(ctx);
            break;
        case rapidjson::Type::kFalseType:
            ValkeyModule_DigestAddLongLong(ctx, 0);
            ValkeyModule_DigestEndSequence(ctx);
            break;
        case rapidjson::Type::kTrueType:
            ValkeyModule_DigestAddLongLong(ctx, 1);
            ValkeyModule_DigestEndSequence(ctx);
            break;
        case rapidjson::Type::kArrayType:
            ValkeyModule_DigestAddLongLong(ctx, v.Size());
            ValkeyModule_DigestEndSequence(ctx);
            for (size_t i = 0; i < v.Size(); ++i) {
                compute_digest(ctx, v[i]);
            }
            break;
        case rapidjson::Type::kNumberType:
            if (v.IsDouble()) {
                double d = v.GetDouble();
                int64_t bits;
                memcpy(&bits, &d, sizeof(bits));
                ValkeyModule_DigestAddLongLong(ctx, bits);
            } else if (v.IsUint64()) {
                uint64_t ui = v.GetUint64();
                int64_t bits;
                memcpy(&bits, &ui, sizeof(bits));
                ValkeyModule_DigestAddLongLong(ctx, bits);
            } else {
                ValkeyModule_DigestAddLongLong(ctx, v.GetInt64());
            }
            ValkeyModule_DigestEndSequence(ctx);
            break;
        case rapidjson::Type::kObjectType:
            ValkeyModule_DigestAddLongLong(ctx, v.MemberCount());
            ValkeyModule_DigestEndSequence(ctx);
            for (auto m = v.MemberBegin(); m != v.MemberEnd(); ++m) {
                const char *b = m->name.GetString();
                ValkeyModule_DigestAddStringBuffer(ctx, b, m->name.GetStringLength());
                compute_digest(ctx, m->value);
            }
            break;
        case rapidjson::Type::kStringType:
            ValkeyModule_DigestAddStringBuffer(ctx, v.GetString(), v.GetStringLength());
            ValkeyModule_DigestEndSequence(ctx);
            break;
        default:
            ValkeyModule_Assert(false);
            break;
    }
}

void dom_compute_digest(ValkeyModuleDigest *ctx, const JDocument *doc) {
    compute_digest(ctx, doc->GetJValue());
}

void dom_dump_value(JValue &v) {
    (void)v;
    rapidjson::StringBuffer sb;
    rapidjson::Writer<rapidjson::StringBuffer> writer(sb);
    v.Accept(writer);
    std::cout << "DEBUG DOM\tvalue: " << sb.GetString() << std::endl;
}

/* ========================= functions consumed by unit tests ======================== */

jsn::string dom_get_string(JDocument *d) {
    return d->GetString();
}

