#include "runtime.h"
#include "value.h"

// for converting from JSON to jinja values
#include <nlohmann/json.hpp>

#include <sstream>
#include <string>
#include <cctype>
#include <vector>
#include <optional>
#include <algorithm>

#define FILENAME "jinja-value"

namespace jinja {

// func_args method implementations

value func_args::get_kwarg(const std::string & key, value default_val) const {
    for (const auto & arg : args) {
        if (is_val<value_kwarg>(arg)) {
            auto * kwarg = cast_val<value_kwarg>(arg);
            if (kwarg->key == key) {
                return kwarg->val;
            }
        }
    }
    return default_val;
}

value func_args::get_kwarg_or_pos(const std::string & key, size_t pos) const {
    value val = get_kwarg(key, mk_val<value_undefined>());

    if (val->is_undefined() && pos < count() && !is_val<value_kwarg>(args[pos])) {
        return args[pos];
    }

    return val;
}

value func_args::get_pos(size_t pos) const {
    if (count() > pos) {
        return args[pos];
    }
    throw raised_exception("Function '" + func_name + "' expected at least " + std::to_string(pos + 1) + " arguments, got " + std::to_string(count()));
}

value func_args::get_pos(size_t pos, value default_val) const {
    if (count() > pos) {
        return args[pos];
    }
    return default_val;
}

void func_args::push_back(const value & val) {
    args.push_back(val);
}

void func_args::push_front(const value & val) {
    args.insert(args.begin(), val);
}

const std::vector<value> & func_args::get_args() const {
    return args;
}

/**
 * Function that mimics Python's array slicing.
 */
template<typename T>
static T slice(const T & array, int64_t start, int64_t stop, int64_t step = 1) {
    int64_t len = static_cast<int64_t>(array.size());
    int64_t direction = (step > 0) ? 1 : ((step < 0) ? -1 : 0);
    int64_t start_val = 0;
    int64_t stop_val = 0;
    if (direction >= 0) {
        start_val = start;
        if (start_val < 0) {
            start_val = std::max(len + start_val, (int64_t)0);
        } else {
            start_val = std::min(start_val, len);
        }

        stop_val = stop;
        if (stop_val < 0) {
            stop_val = std::max(len + stop_val, (int64_t)0);
        } else {
            stop_val = std::min(stop_val, len);
        }
    } else {
        start_val = len - 1;
        if (start_val < 0) {
            start_val = std::max(len + start_val, (int64_t)-1);
        } else {
            start_val = std::min(start_val, len - 1);
        }

        stop_val = -1;
        if (stop_val < -1) {
            stop_val = std::max(len + stop_val, (int64_t)-1);
        } else {
            stop_val = std::min(stop_val, len - 1);
        }
    }
    T result;
    if (direction == 0) {
        return result;
    }
    for (int64_t i = start_val; direction * i < direction * stop_val; i += step) {
        if (i >= 0 && i < len) {
            result.push_back(array[static_cast<size_t>(i)]);
        }
    }
    return result;
}

template<typename T>
static value empty_value_fn(const func_args &) {
    if constexpr (std::is_same_v<T, value_int>) {
        return mk_val<T>(0);
    } else if constexpr (std::is_same_v<T, value_float>) {
        return mk_val<T>(0.0);
    } else if constexpr (std::is_same_v<T, value_bool>) {
        return mk_val<T>(false);
    } else {
        return mk_val<T>();
    }
}
template<typename T>
static value test_type_fn(const func_args & args) {
    args.ensure_count(1);
    bool is_type = is_val<T>(args.get_pos(0));
    JJ_DEBUG("test_type_fn: type=%s result=%d", typeid(T).name(), is_type ? 1 : 0);
    return mk_val<value_bool>(is_type);
}
template<typename T, typename U>
static value test_type_fn(const func_args & args) {
    args.ensure_count(1);
    bool is_type = is_val<T>(args.get_pos(0)) || is_val<U>(args.get_pos(0));
    JJ_DEBUG("test_type_fn: type=%s or %s result=%d", typeid(T).name(), typeid(U).name(), is_type ? 1 : 0);
    return mk_val<value_bool>(is_type);
}
template<typename T, typename U, typename V>
static value test_type_fn(const func_args & args) {
    args.ensure_count(1);
    bool is_type = is_val<T>(args.get_pos(0)) || is_val<U>(args.get_pos(0)) || is_val<V>(args.get_pos(0));
    JJ_DEBUG("test_type_fn: type=%s, %s or %s result=%d", typeid(T).name(), typeid(U).name(), typeid(V).name(), is_type ? 1 : 0);
    return mk_val<value_bool>(is_type);
}
template<value_compare_op op>
static value test_compare_fn(const func_args & args) {
    args.ensure_count(2, 2);
    return mk_val<value_bool>(value_compare(args.get_pos(0), args.get_pos(1), op));
}

static value tojson(const func_args & args) {
    args.ensure_count(1, 5);
    value val_ascii      = args.get_kwarg_or_pos("ensure_ascii", 1);
    value val_indent     = args.get_kwarg_or_pos("indent",       2);
    value val_separators = args.get_kwarg_or_pos("separators",   3);
    value val_sort       = args.get_kwarg_or_pos("sort_keys",    4);
    int indent = -1;
    if (args.ctx.is_get_stats) {
        // mark as used (recursively) for stats
        auto val_input = args.get_pos(0);
        value_t::stats_t::mark_used(const_cast<value&>(val_input), true);
    }
    if (is_val<value_int>(val_indent)) {
        indent = static_cast<int>(val_indent->as_int());
    }
    if (val_ascii->as_bool()) { // undefined == false
        throw not_implemented_exception("tojson ensure_ascii=true not implemented");
    }
    if (val_sort->as_bool()) { // undefined == false
        throw not_implemented_exception("tojson sort_keys=true not implemented");
    }
    auto separators = (is_val<value_array>(val_separators) ? val_separators : mk_val<value_array>())->as_array();
    std::string item_sep = separators.size() > 0 ? separators[0]->as_string().str() : (indent < 0 ? ", " : ",");
    std::string key_sep = separators.size() > 1 ? separators[1]->as_string().str() : ": ";
    std::string json_str = value_to_json(args.get_pos(0), indent, item_sep, key_sep);
    return mk_val<value_string>(json_str);
}

template<bool is_reject>
static value selectattr(const func_args & args) {
    args.ensure_count(2, 4);
    args.ensure_vals<value_array, value_string, value_string, value_string>(true, true, false, false);

    auto arr = args.get_pos(0)->as_array();
    auto attribute = args.get_pos(1);
    auto out = mk_val<value_array>();
    value val_default = mk_val<value_undefined>();

    if (args.count() == 2) {
        // example: array | selectattr("active")
        for (const auto & item : arr) {
            if (!is_val<value_object>(item)) {
                throw raised_exception("selectattr: item is not an object");
            }
            value attr_val = item->at(attribute, val_default);
            bool is_selected = attr_val->as_bool();
            if constexpr (is_reject) is_selected = !is_selected;
            if (is_selected) out->push_back(item);
        }
        return out;

    } else if (args.count() == 3) {
        // example: array | selectattr("equalto", "text")
        // translated to: test_is_equalto(item, "text")
        std::string test_name = args.get_pos(1)->as_string().str();
        value test_val = args.get_pos(2);
        auto & builtins = global_builtins();
        auto it = builtins.find("test_is_" + test_name);
        if (it == builtins.end()) {
            throw raised_exception("selectattr: unknown test '" + test_name + "'");
        }
        auto test_fn = it->second;
        for (const auto & item : arr) {
            func_args test_args(args.ctx);
            test_args.push_back(item); // current object
            test_args.push_back(test_val); // extra argument
            value test_result = test_fn(test_args);
            bool is_selected = test_result->as_bool();
            if constexpr (is_reject) is_selected = !is_selected;
            if (is_selected) out->push_back(item);
        }
        return out;

    } else if (args.count() == 4) {
        // example: array | selectattr("status", "equalto", "active")
        // translated to: test_is_equalto(item.status, "active")
        std::string test_name = args.get_pos(2)->as_string().str();
        auto extra_arg = args.get_pos(3);
        auto & builtins = global_builtins();
        auto it = builtins.find("test_is_" + test_name);
        if (it == builtins.end()) {
            throw raised_exception("selectattr: unknown test '" + test_name + "'");
        }
        auto test_fn = it->second;
        for (const auto & item : arr) {
            if (!is_val<value_object>(item)) {
                throw raised_exception("selectattr: item is not an object");
            }
            value attr_val = item->at(attribute, val_default);
            func_args test_args(args.ctx);
            test_args.push_back(attr_val); // attribute value
            test_args.push_back(extra_arg); // extra argument
            value test_result = test_fn(test_args);
            bool is_selected = test_result->as_bool();
            if constexpr (is_reject) is_selected = !is_selected;
            if (is_selected) out->push_back(item);
        }
        return out;
    } else {
        throw raised_exception("selectattr: invalid number of arguments");
    }

    return out;
}

static value default_value(const func_args & args) {
    args.ensure_count(2, 3);
    value val_check = args.get_kwarg_or_pos("boolean", 2);
    bool check_bool = val_check->as_bool(); // undefined == false
    bool no_value = check_bool
        ? (!args.get_pos(0)->as_bool())
        : (args.get_pos(0)->is_undefined() || args.get_pos(0)->is_none());
    return no_value ? args.get_pos(1) : args.get_pos(0);
}

const func_builtins & global_builtins() {
    static const func_builtins builtins = {
        {"raise_exception", [](const func_args & args) -> value {
            args.ensure_vals<value_string>();
            std::string msg = args.get_pos(0)->as_string().str();
            throw raised_exception("Jinja Exception: " + msg);
        }},
        {"namespace", [](const func_args & args) -> value {
            auto out = mk_val<value_object>();
            for (const auto & arg : args.get_args()) {
                if (!is_val<value_kwarg>(arg)) {
                    throw raised_exception("namespace() arguments must be kwargs");
                }
                auto kwarg = cast_val<value_kwarg>(arg);
                JJ_DEBUG("namespace: adding key '%s'", kwarg->key.c_str());
                out->insert(kwarg->key, kwarg->val);
            }
            return out;
        }},
        {"strftime_now", [](const func_args & args) -> value {
            args.ensure_vals<value_string>();
            std::string format = args.get_pos(0)->as_string().str();
            // get current time
            // TODO: make sure this is the same behavior as Python's strftime
            char buf[100];
            if (std::strftime(buf, sizeof(buf), format.c_str(), std::localtime(&args.ctx.current_time))) {
                return mk_val<value_string>(std::string(buf));
            } else {
                throw raised_exception("strftime_now: failed to format time");
            }
        }},
        {"range", [](const func_args & args) -> value {
            args.ensure_count(1, 3);
            args.ensure_vals<value_int, value_int, value_int>(true, false, false);

            auto arg0 = args.get_pos(0);
            auto arg1 = args.get_pos(1, mk_val<value_undefined>());
            auto arg2 = args.get_pos(2, mk_val<value_undefined>());

            int64_t start, stop, step;
            if (args.count() == 1) {
                start = 0;
                stop = arg0->as_int();
                step = 1;
            } else if (args.count() == 2) {
                start = arg0->as_int();
                stop = arg1->as_int();
                step = 1;
            } else {
                start = arg0->as_int();
                stop = arg1->as_int();
                step = arg2->as_int();
            }

            auto out = mk_val<value_array>();
            if (step == 0) {
                throw raised_exception("range() step argument must not be zero");
            }
            if (step > 0) {
                for (int64_t i = start; i < stop; i += step) {
                    out->push_back(mk_val<value_int>(i));
                }
            } else {
                for (int64_t i = start; i > stop; i += step) {
                    out->push_back(mk_val<value_int>(i));
                }
            }
            return out;
        }},
        {"tojson", tojson},

        // tests
        {"test_is_boolean", test_type_fn<value_bool>},
        {"test_is_callable", test_type_fn<value_func>},
        {"test_is_odd", [](const func_args & args) -> value {
            args.ensure_vals<value_int>();
            int64_t val = args.get_pos(0)->as_int();
            return mk_val<value_bool>(val % 2 != 0);
        }},
        {"test_is_even", [](const func_args & args) -> value {
            args.ensure_vals<value_int>();
            int64_t val = args.get_pos(0)->as_int();
            return mk_val<value_bool>(val % 2 == 0);
        }},
        {"test_is_false", [](const func_args & args) -> value {
            args.ensure_count(1);
            bool val = is_val<value_bool>(args.get_pos(0)) && !args.get_pos(0)->as_bool();
            return mk_val<value_bool>(val);
        }},
        {"test_is_true", [](const func_args & args) -> value {
            args.ensure_count(1);
            bool val = is_val<value_bool>(args.get_pos(0)) && args.get_pos(0)->as_bool();
            return mk_val<value_bool>(val);
        }},
        {"test_is_divisibleby", [](const func_args & args) -> value {
            args.ensure_vals<value_int, value_int>();
            bool res = args.get_pos(0)->val_int % args.get_pos(1)->val_int == 0;
            return mk_val<value_bool>(res);
        }},
        {"test_is_string", test_type_fn<value_string>},
        {"test_is_integer", test_type_fn<value_int>},
        {"test_is_float", test_type_fn<value_float>},
        {"test_is_number", test_type_fn<value_int, value_float>},
        {"test_is_iterable", test_type_fn<value_array, value_string, value_undefined>},
        {"test_is_sequence", test_type_fn<value_array, value_string, value_undefined>},
        {"test_is_mapping", test_type_fn<value_object>},
        {"test_is_lower", [](const func_args & args) -> value {
            args.ensure_vals<value_string>();
            return mk_val<value_bool>(args.get_pos(0)->val_str.is_lowercase());
        }},
        {"test_is_upper", [](const func_args & args) -> value {
            args.ensure_vals<value_string>();
            return mk_val<value_bool>(args.get_pos(0)->val_str.is_uppercase());
        }},
        {"test_is_none", test_type_fn<value_none>},
        {"test_is_defined", [](const func_args & args) -> value {
            args.ensure_count(1);
            bool res = !args.get_pos(0)->is_undefined();
            JJ_DEBUG("test_is_defined: result=%d", res ? 1 : 0);
            return mk_val<value_bool>(res);
        }},
        {"test_is_undefined", test_type_fn<value_undefined>},
        {"test_is_eq", test_compare_fn<value_compare_op::eq>},
        {"test_is_equalto", test_compare_fn<value_compare_op::eq>},
        {"test_is_ge", test_compare_fn<value_compare_op::ge>},
        {"test_is_gt", test_compare_fn<value_compare_op::gt>},
        {"test_is_greaterthan", test_compare_fn<value_compare_op::gt>},
        {"test_is_lt", test_compare_fn<value_compare_op::lt>},
        {"test_is_lessthan", test_compare_fn<value_compare_op::lt>},
        {"test_is_ne", test_compare_fn<value_compare_op::ne>},
        {"test_is_in", [](const func_args & args) -> value {
            args.ensure_count(2);
            auto needle   = args.get_pos(0);
            auto haystack = args.get_pos(1);
            if (is_val<value_undefined>(haystack)) {
                return mk_val<value_bool>(false);
            }
            if (is_val<value_array>(haystack)) {
                for (const auto & item : haystack->as_array()) {
                    if (*needle == *item) {
                        return mk_val<value_bool>(true);
                    }
                }
                return mk_val<value_bool>(false);
            }
            if (is_val<value_string>(haystack)) {
                if (!is_val<value_string>(needle)) {
                    throw raised_exception("'in' test expects args[1] as string when args[0] is string, got args[1] as " + needle->type());
                }
                return mk_val<value_bool>(
                    haystack->as_string().str().find(needle->as_string().str()) != std::string::npos);
            }
            if (is_val<value_object>(haystack)) {
                return mk_val<value_bool>(haystack->has_key(needle));
            }
            throw raised_exception("'in' test expects iterable as first argument, got " + haystack->type());
        }},
        {"test_is_test", [](const func_args & args) -> value {
            args.ensure_vals<value_string>();
            auto & builtins = global_builtins();
            std::string test_name = args.get_pos(0)->val_str.str();
            auto it = builtins.find("test_is_" + test_name);
            bool res = it != builtins.end();
            return mk_val<value_bool>(res);
        }},
        {"test_is_sameas", [](const func_args & args) -> value {
            // Check if an object points to the same memory address as another object
            (void)args;
            throw not_implemented_exception("sameas test not implemented");
        }},
        {"test_is_escaped", [](const func_args & args) -> value {
            (void)args;
            throw not_implemented_exception("escaped test not implemented");
        }},
        {"test_is_filter", [](const func_args & args) -> value {
            (void)args;
            throw not_implemented_exception("filter test not implemented");
        }},
    };
    return builtins;
}


const func_builtins & value_int_t::get_builtins() const {
    static const func_builtins builtins = {
        {"default", default_value},
        {"abs", [](const func_args & args) -> value {
            args.ensure_vals<value_int>();
            int64_t val = args.get_pos(0)->as_int();
            return mk_val<value_int>(val < 0 ? -val : val);
        }},
        {"float", [](const func_args & args) -> value {
            args.ensure_vals<value_int>();
            double val = static_cast<double>(args.get_pos(0)->as_int());
            return mk_val<value_float>(val);
        }},
        {"tojson", tojson},
        {"string", tojson},
    };
    return builtins;
}


const func_builtins & value_float_t::get_builtins() const {
    static const func_builtins builtins = {
        {"default", default_value},
        {"abs", [](const func_args & args) -> value {
            args.ensure_vals<value_float>();
            double val = args.get_pos(0)->as_float();
            return mk_val<value_float>(val < 0.0 ? -val : val);
        }},
        {"int", [](const func_args & args) -> value {
            args.ensure_vals<value_float>();
            int64_t val = static_cast<int64_t>(args.get_pos(0)->as_float());
            return mk_val<value_int>(val);
        }},
        {"tojson", tojson},
        {"string", tojson},
    };
    return builtins;
}

static bool string_startswith(const std::string & str, const std::string & prefix) {
    if (str.length() < prefix.length()) return false;
    return str.compare(0, prefix.length(), prefix) == 0;
}

static bool string_endswith(const std::string & str, const std::string & suffix) {
    if (str.length() < suffix.length()) return false;
    return str.compare(str.length() - suffix.length(), suffix.length(), suffix) == 0;
}

const func_builtins & value_string_t::get_builtins() const {
    static const func_builtins builtins = {
        {"default", default_value},
        {"upper", [](const func_args & args) -> value {
            args.ensure_vals<value_string>();
            jinja::string str = args.get_pos(0)->as_string().uppercase();
            return mk_val<value_string>(str);
        }},
        {"lower", [](const func_args & args) -> value {
            args.ensure_vals<value_string>();
            jinja::string str = args.get_pos(0)->as_string().lowercase();
            return mk_val<value_string>(str);
        }},
        {"strip", [](const func_args & args) -> value {
            value val_input = args.get_pos(0);
            if (!is_val<value_string>(val_input)) {
                throw raised_exception("strip() first argument must be a string");
            }
            value val_chars = args.get_kwarg_or_pos("chars", 1);
            if (val_chars->is_undefined()) {
                return mk_val<value_string>(args.get_pos(0)->as_string().strip(true, true));
            } else {
                return mk_val<value_string>(args.get_pos(0)->as_string().strip(true, true, val_chars->as_string().str()));
            }
        }},
        {"rstrip", [](const func_args & args) -> value {
            args.ensure_vals<value_string>();
            value val_chars = args.get_kwarg_or_pos("chars", 1);
            if (val_chars->is_undefined()) {
                return mk_val<value_string>(args.get_pos(0)->as_string().strip(false, true));
            } else {
                return mk_val<value_string>(args.get_pos(0)->as_string().strip(false, true, val_chars->as_string().str()));
            }
        }},
        {"lstrip", [](const func_args & args) -> value {
            args.ensure_vals<value_string>();
            value val_chars = args.get_kwarg_or_pos("chars", 1);
            if (val_chars->is_undefined()) {
                return mk_val<value_string>(args.get_pos(0)->as_string().strip(true, false));
            } else {
                return mk_val<value_string>(args.get_pos(0)->as_string().strip(true, false, val_chars->as_string().str()));
            }
        }},
        {"title", [](const func_args & args) -> value {
            args.ensure_vals<value_string>();
            jinja::string str = args.get_pos(0)->as_string().titlecase();
            return mk_val<value_string>(str);
        }},
        {"capitalize", [](const func_args & args) -> value {
            args.ensure_vals<value_string>();
            jinja::string str = args.get_pos(0)->as_string().capitalize();
            return mk_val<value_string>(str);
        }},
        {"length", [](const func_args & args) -> value {
            args.ensure_vals<value_string>();
            jinja::string str = args.get_pos(0)->as_string();
            return mk_val<value_int>(str.length());
        }},
        {"startswith", [](const func_args & args) -> value {
            args.ensure_vals<value_string, value_string>();
            std::string str = args.get_pos(0)->as_string().str();
            std::string prefix = args.get_pos(1)->as_string().str();
            return mk_val<value_bool>(string_startswith(str, prefix));
        }},
        {"endswith", [](const func_args & args) -> value {
            args.ensure_vals<value_string, value_string>();
            std::string str = args.get_pos(0)->as_string().str();
            std::string suffix = args.get_pos(1)->as_string().str();
            return mk_val<value_bool>(string_endswith(str, suffix));
        }},
        {"split", [](const func_args & args) -> value {
            args.ensure_count(1, 3);
            value val_input = args.get_pos(0);
            if (!is_val<value_string>(val_input)) {
                throw raised_exception("split() first argument must be a string");
            }
            std::string str = val_input->as_string().str();
            // FIXME: Support non-specified delimiter (split on consecutive (no leading or trailing) whitespace)
            std::string delim = (args.count() > 1) ? args.get_pos(1)->as_string().str() : " ";
            int64_t maxsplit = (args.count() > 2) ? args.get_pos(2)->as_int() : -1;
            auto result = mk_val<value_array>();
            size_t pos = 0;
            std::string token;
            while ((pos = str.find(delim)) != std::string::npos && maxsplit != 0) {
                token = str.substr(0, pos);
                result->push_back(mk_val<value_string>(token));
                str.erase(0, pos + delim.length());
                --maxsplit;
            }
            auto res = mk_val<value_string>(str);
            res->val_str.mark_input_based_on(args.get_pos(0)->val_str);
            result->push_back(std::move(res));
            return result;
        }},
        {"rsplit", [](const func_args & args) -> value {
            args.ensure_count(1, 3);
            value val_input = args.get_pos(0);
            if (!is_val<value_string>(val_input)) {
                throw raised_exception("rsplit() first argument must be a string");
            }
            std::string str = val_input->as_string().str();
            // FIXME: Support non-specified delimiter (split on consecutive (no leading or trailing) whitespace)
            std::string delim = (args.count() > 1) ? args.get_pos(1)->as_string().str() : " ";
            int64_t maxsplit = (args.count() > 2) ? args.get_pos(2)->as_int() : -1;
            auto result = mk_val<value_array>();
            size_t pos = 0;
            std::string token;
            while ((pos = str.rfind(delim)) != std::string::npos && maxsplit != 0) {
                token = str.substr(pos + delim.length());
                result->push_back(mk_val<value_string>(token));
                str.erase(pos);
                --maxsplit;
            }
            auto res = mk_val<value_string>(str);
            res->val_str.mark_input_based_on(args.get_pos(0)->val_str);
            result->push_back(std::move(res));
            result->reverse();
            return result;
        }},
        {"replace", [](const func_args & args) -> value {
            args.ensure_vals<value_string, value_string, value_string, value_int>(true, true, true, false);
            std::string str = args.get_pos(0)->as_string().str();
            std::string old_str = args.get_pos(1)->as_string().str();
            std::string new_str = args.get_pos(2)->as_string().str();
            int64_t count = args.count() > 3 ? args.get_pos(3)->as_int() : -1;
            if (count > 0) {
                throw not_implemented_exception("String replace with count argument not implemented");
            }
            size_t pos = 0;
            while ((pos = str.find(old_str, pos)) != std::string::npos) {
                str.replace(pos, old_str.length(), new_str);
                pos += new_str.length();
            }
            auto res = mk_val<value_string>(str);
            res->val_str.mark_input_based_on(args.get_pos(0)->val_str);
            return res;
        }},
        {"int", [](const func_args & args) -> value {
            value val_input   = args.get_pos(0);
            value val_default = args.get_kwarg_or_pos("default", 1);
            value val_base    = args.get_kwarg_or_pos("base",    2);
            const int base = val_base->is_undefined() ? 10 : val_base->as_int();
            if (is_val<value_string>(val_input) == false) {
                throw raised_exception("int() first argument must be a string");
            }
            std::string str = val_input->as_string().str();
            try {
                return mk_val<value_int>(std::stoi(str, nullptr, base));
            } catch (...) {
                return mk_val<value_int>(val_default->is_undefined() ? 0 : val_default->as_int());
            }
        }},
        {"float", [](const func_args & args) -> value {
            args.ensure_vals<value_string>();
            value val_default = args.get_kwarg_or_pos("default", 1);
            std::string str = args.get_pos(0)->as_string().str();
            try {
                return mk_val<value_float>(std::stod(str));
            } catch (...) {
                return mk_val<value_float>(val_default->is_undefined() ? 0.0 : val_default->as_float());
            }
        }},
        {"string", [](const func_args & args) -> value {
            // no-op
            args.ensure_vals<value_string>();
            return mk_val<value_string>(args.get_pos(0)->as_string());
        }},
        {"default", [](const func_args & args) -> value {
            value input = args.get_pos(0);
            if (!is_val<value_string>(input)) {
                throw raised_exception("default() first argument must be a string");
            }
            value default_val = mk_val<value_string>("");
            if (args.count() > 1 && !args.get_pos(1)->is_undefined()) {
                default_val = args.get_pos(1);
            }
            value boolean_val = args.get_kwarg_or_pos("boolean", 2); // undefined == false
            if (input->is_undefined() || (boolean_val->as_bool() && !input->as_bool())) {
                return default_val;
            } else {
                return input;
            }
        }},
        {"slice", [](const func_args & args) -> value {
            args.ensure_count(1, 4);
            args.ensure_vals<value_string, value_int, value_int, value_int>(true, true, false, false);

            auto arg0 = args.get_pos(1);
            auto arg1 = args.get_pos(2, mk_val<value_undefined>());
            auto arg2 = args.get_pos(3, mk_val<value_undefined>());

            int64_t start, stop, step;
            if (args.count() == 1) {
                start = 0;
                stop = arg0->as_int();
                step = 1;
            } else if (args.count() == 2) {
                start = arg0->as_int();
                stop = arg1->as_int();
                step = 1;
            } else {
                start = arg0->as_int();
                stop = arg1->as_int();
                step = arg2->as_int();
            }
            if (step == 0) {
                throw raised_exception("slice step cannot be zero");
            }
            auto input = args.get_pos(0);
            auto sliced = slice(input->as_string().str(), start, stop, step);
            auto res = mk_val<value_string>(sliced);
            res->val_str.mark_input_based_on(input->as_string());
            return res;
        }},
        {"safe", [](const func_args & args) -> value {
            // no-op for now
            args.ensure_vals<value_string>();
            return args.get_pos(0);
        }},
        {"tojson", tojson},
        {"indent", [](const func_args &args) -> value {
            args.ensure_count(1, 4);
            value val_input  = args.get_pos(0);
            value val_width  = args.get_kwarg_or_pos("width", 1);
            const bool first = args.get_kwarg_or_pos("first", 2)->as_bool(); // undefined == false
            const bool blank = args.get_kwarg_or_pos("blank", 3)->as_bool(); // undefined == false
            if (!is_val<value_string>(val_input)) {
                throw raised_exception("indent() first argument must be a string");
            }
            std::string indent;
            if (is_val<value_int>(val_width)) {
                indent.assign(val_width->as_int(), ' ');
            } else if (is_val<value_string>(val_width)) {
                indent = val_width->as_string().str();
            } else {
                indent = "    ";
            }
            std::string indented;
            std::string input = val_input->as_string().str();
            std::istringstream iss = std::istringstream(input);
            std::string line;
            while (std::getline(iss, line)) {
                if (!indented.empty()) {
                    indented.push_back('\n');
                }
                if ((indented.empty() ? first : (!line.empty() || blank))) {
                    indented += indent;
                }
                indented += line;
            }
            if (!input.empty() && input.back() == '\n') {
                indented.push_back('\n');
                if (blank) {
                    indented += indent;
                }
            }

            auto res = mk_val<value_string>(indented);
            res->val_str.mark_input_based_on(val_input->as_string());
            return res;
        }},
        {"join", [](const func_args &) -> value {
            throw not_implemented_exception("String join builtin not implemented");
        }},
    };
    return builtins;
}


const func_builtins & value_bool_t::get_builtins() const {
    static const func_builtins builtins = {
        {"default", default_value},
        {"int", [](const func_args & args) -> value {
            args.ensure_vals<value_bool>();
            bool val = args.get_pos(0)->as_bool();
            return mk_val<value_int>(val ? 1 : 0);
        }},
        {"float", [](const func_args & args) -> value {
            args.ensure_vals<value_bool>();
            bool val = args.get_pos(0)->as_bool();
            return mk_val<value_float>(val ? 1.0 : 0.0);
        }},
        {"string", [](const func_args & args) -> value {
            args.ensure_vals<value_bool>();
            bool val = args.get_pos(0)->as_bool();
            return mk_val<value_string>(val ? "True" : "False");
        }},
        {"tojson", tojson},
    };
    return builtins;
}


const func_builtins & value_array_t::get_builtins() const {
    static const func_builtins builtins = {
        {"default", default_value},
        {"list", [](const func_args & args) -> value {
            args.ensure_vals<value_array>();
            const auto & arr = args.get_pos(0)->as_array();
            auto result = mk_val<value_array>();
            for (const auto& v : arr) {
                result->push_back(v);
            }
            return result;
        }},
        {"first", [](const func_args & args) -> value {
            args.ensure_vals<value_array>();
            const auto & arr = args.get_pos(0)->as_array();
            if (arr.empty()) {
                return mk_val<value_undefined>();
            }
            return arr[0];
        }},
        {"last", [](const func_args & args) -> value {
            args.ensure_vals<value_array>();
            const auto & arr = args.get_pos(0)->as_array();
            if (arr.empty()) {
                return mk_val<value_undefined>();
            }
            return arr[arr.size() - 1];
        }},
        {"length", [](const func_args & args) -> value {
            args.ensure_vals<value_array>();
            const auto & arr = args.get_pos(0)->as_array();
            return mk_val<value_int>(static_cast<int64_t>(arr.size()));
        }},
        {"slice", [](const func_args & args) -> value {
            args.ensure_count(1, 4);
            args.ensure_vals<value_array, value_int, value_int, value_int>(true, true, false, false);

            auto val  = args.get_pos(0);
            auto arg0 = args.get_pos(1);
            auto arg1 = args.get_pos(2, mk_val<value_undefined>());
            auto arg2 = args.get_pos(3, mk_val<value_undefined>());

            int64_t start, stop, step;
            if (args.count() == 1) {
                start = 0;
                stop = arg0->as_int();
                step = 1;
            } else if (args.count() == 2) {
                start = arg0->as_int();
                stop = arg1->as_int();
                step = 1;
            } else {
                start = arg0->as_int();
                stop = arg1->as_int();
                step = arg2->as_int();
            }
            if (step == 0) {
                throw raised_exception("slice step cannot be zero");
            }
            auto arr = slice(val->as_array(), start, stop, step);
            return is_val<value_tuple>(val) ? mk_val<value_tuple>(std::move(arr)) : mk_val<value_array>(std::move(arr));
        }},
        {"selectattr", selectattr<false>},
        {"select", selectattr<false>},
        {"rejectattr", selectattr<true>},
        {"reject", selectattr<true>},
        {"join", [](const func_args & args) -> value {
            args.ensure_count(1, 3);
            if (!is_val<value_array>(args.get_pos(0))) {
                throw raised_exception("join() first argument must be an array");
            }
            value val_delim = args.get_kwarg_or_pos("d",         1);
            value attribute = args.get_kwarg_or_pos("attribute", 2);
            const auto & arr = args.get_pos(0)->as_array();
            const bool attr_is_int = is_val<value_int>(attribute);
            if (!attribute->is_undefined() && !is_val<value_string>(attribute) && !attr_is_int) {
                throw raised_exception("join() attribute must be string or integer");
            }
            const int64_t attr_int = attr_is_int ? attribute->as_int() : 0;
            const std::string delim = val_delim->is_undefined() ? "" : val_delim->as_string().str();
            std::string result;
            for (size_t i = 0; i < arr.size(); ++i) {
                value val_arr = arr[i];
                if (!attribute->is_undefined()) {
                    if (attr_is_int && is_val<value_array>(val_arr)) {
                        val_arr = val_arr->at(attr_int);
                    } else if (!attr_is_int && is_val<value_object>(val_arr)) {
                        val_arr = val_arr->at(attribute);
                    }
                }
                if (!is_val<value_string>(val_arr) && !is_val<value_int>(val_arr) && !is_val<value_float>(val_arr)) {
                    throw raised_exception("join() can only join arrays of strings or numerics");
                }
                result += val_arr->as_string().str();
                if (i < arr.size() - 1) {
                    result += delim;
                }
            }
            return mk_val<value_string>(result);
        }},
        {"string", [](const func_args & args) -> value {
            args.ensure_vals<value_array>();
            if (args.ctx.is_get_stats) {
                // mark as used (recursively) for stats
                auto val_input = args.get_pos(0);
                value_t::stats_t::mark_used(const_cast<value&>(val_input), true);
            }
            return mk_val<value_string>(args.get_pos(0)->as_string());
        }},
        {"tojson", tojson},
        {"map", [](const func_args & args) -> value {
            args.ensure_count(2);
            if (!is_val<value_array>(args.get_pos(0))) {
                throw raised_exception("map: first argument must be an array");
            }
            if (!is_val<value_kwarg>(args.get_args().at(1))) {
                throw not_implemented_exception("map: filter-mapping not implemented");
            }
            value val       = args.get_pos(0);
            value attribute = args.get_kwarg_or_pos("attribute", 1);
            const bool attr_is_int = is_val<value_int>(attribute);
            if (!is_val<value_string>(attribute) && !attr_is_int) {
                throw raised_exception("map: attribute must be string or integer");
            }
            const int64_t attr_int = attr_is_int ? attribute->as_int() : 0;
            value default_val = args.get_kwarg("default", mk_val<value_undefined>());
            auto out = mk_val<value_array>();
            auto arr = val->as_array();
            for (const auto & item : arr) {
                value attr_val;
                if (attr_is_int) {
                    attr_val = is_val<value_array>(item) ? item->at(attr_int, default_val) : default_val;
                } else {
                    attr_val = is_val<value_object>(item) ? item->at(attribute, default_val) : default_val;
                }
                out->push_back(attr_val);
            }
            return is_val<value_tuple>(val) ? mk_val<value_tuple>(std::move(out->as_array())) : out;
        }},
        {"append", [](const func_args & args) -> value {
            args.ensure_count(2);
            if (!is_val<value_array>(args.get_pos(0))) {
                throw raised_exception("append: first argument must be an array");
            }
            const value_array_t * arr = cast_val<value_array>(args.get_pos(0));
            // need to use const_cast here to modify the array
            value_array_t * arr_editable = const_cast<value_array_t *>(arr);
            arr_editable->push_back(args.get_pos(1));
            return args.get_pos(0);
        }},
        {"pop", [](const func_args & args) -> value {
            args.ensure_count(1, 2);
            args.ensure_vals<value_array, value_int>(true, false);
            int64_t index = args.count() == 2 ? args.get_pos(1)->as_int() : -1;
            const value_array_t * arr = cast_val<value_array>(args.get_pos(0));
            // need to use const_cast here to modify the array
            value_array_t * arr_editable = const_cast<value_array_t *>(arr);
            return arr_editable->pop_at(index);
        }},
        {"sort", [](const func_args & args) -> value {
            args.ensure_count(1, 4);
            if (!is_val<value_array>(args.get_pos(0))) {
                throw raised_exception("sort: first argument must be an array");
            }
            value val         = args.get_pos(0);
            value val_reverse = args.get_kwarg_or_pos("reverse",        1);
            value val_case    = args.get_kwarg_or_pos("case_sensitive", 2);
            value attribute   = args.get_kwarg_or_pos("attribute",      3);
            // FIXME: sorting is currently always case sensitive
            //const bool case_sensitive = val_case->as_bool(); // undefined == false
            const bool reverse = val_reverse->as_bool(); // undefined == false
            const bool attr_is_int = is_val<value_int>(attribute);
            const int64_t attr_int = attr_is_int ? attribute->as_int() : 0;
            std::vector<value> arr = val->as_array(); // copy
            std::sort(arr.begin(), arr.end(),[&](const value & a, const value & b) {
                value val_a = a;
                value val_b = b;
                if (!attribute->is_undefined()) {
                    if (attr_is_int && is_val<value_array>(a) && is_val<value_array>(b)) {
                        val_a = a->at(attr_int);
                        val_b = b->at(attr_int);
                    } else if (!attr_is_int && is_val<value_object>(a) && is_val<value_object>(b)) {
                        val_a = a->at(attribute);
                        val_b = b->at(attribute);
                    } else {
                        throw raised_exception("sort: unsupported object attribute comparison between " + a->type() + " and " + b->type());
                    }
                }
                return value_compare(val_a, val_b, reverse ? value_compare_op::gt : value_compare_op::lt);
            });
            return is_val<value_tuple>(val) ? mk_val<value_tuple>(std::move(arr)) : mk_val<value_array>(std::move(arr));
        }},
        {"reverse", [](const func_args & args) -> value {
            args.ensure_vals<value_array>();
            value val = args.get_pos(0);
            std::vector<value> arr = val->as_array(); // copy
            std::reverse(arr.begin(), arr.end());
            return is_val<value_tuple>(val) ? mk_val<value_tuple>(std::move(arr)) : mk_val<value_array>(std::move(arr));
        }},
        {"unique", [](const func_args &) -> value {
            throw not_implemented_exception("Array unique builtin not implemented");
        }},
    };
    return builtins;
}


const func_builtins & value_object_t::get_builtins() const {
    if (!has_builtins) {
        static const func_builtins no_builtins = {};
        return no_builtins;
    }

    static const func_builtins builtins = {
        // {"default", default_value}, // cause issue with gpt-oss
        {"get", [](const func_args & args) -> value {
            args.ensure_count(2, 3);
            if (!is_val<value_object>(args.get_pos(0))) {
                throw raised_exception("get: first argument must be an object");
            }
            if (!is_val<value_string>(args.get_pos(1))) {
                throw raised_exception("get: second argument must be a string (key)");
            }
            value default_val = mk_val<value_none>();
            if (args.count() == 3) {
                default_val = args.get_pos(2);
            }
            const value obj = args.get_pos(0);
            const value key = args.get_pos(1);
            return obj->at(key, default_val);
        }},
        {"keys", [](const func_args & args) -> value {
            args.ensure_vals<value_object>();
            const auto & obj = args.get_pos(0)->as_ordered_object();
            auto result = mk_val<value_array>();
            for (const auto & pair : obj) {
                result->push_back(pair.first);
            }
            return result;
        }},
        {"values", [](const func_args & args) -> value {
            args.ensure_vals<value_object>();
            const auto & obj = args.get_pos(0)->as_ordered_object();
            auto result = mk_val<value_array>();
            for (const auto & pair : obj) {
                result->push_back(pair.second);
            }
            return result;
        }},
        {"items", [](const func_args & args) -> value {
            args.ensure_vals<value_object>();
            const auto & obj = args.get_pos(0)->as_ordered_object();
            auto result = mk_val<value_array>();
            for (const auto & pair : obj) {
                auto item = mk_val<value_tuple>(pair);
                result->push_back(std::move(item));
            }
            return result;
        }},
        {"tojson", tojson},
        {"string", [](const func_args & args) -> value {
            args.ensure_vals<value_object>();
            if (args.ctx.is_get_stats) {
                // mark as used (recursively) for stats
                auto val_input = args.get_pos(0);
                value_t::stats_t::mark_used(const_cast<value&>(val_input), true);
            }
            return mk_val<value_string>(args.get_pos(0)->as_string());
        }},
        {"length", [](const func_args & args) -> value {
            args.ensure_vals<value_object>();
            const auto & obj = args.get_pos(0)->as_ordered_object();
            return mk_val<value_int>(static_cast<int64_t>(obj.size()));
        }},
        {"tojson", [](const func_args & args) -> value {
            args.ensure_vals<value_object>();
            // use global to_json
            return global_builtins().at("tojson")(args);
        }},
        {"dictsort", [](const func_args & args) -> value {
            value val_input   = args.get_pos(0);
            value val_case    = args.get_kwarg_or_pos("case_sensitive", 1);
            value val_by      = args.get_kwarg_or_pos("by",             2);
            value val_reverse = args.get_kwarg_or_pos("reverse",        3);
            // FIXME: sorting is currently always case sensitive
            //const bool case_sensitive = val_case->as_bool(); // undefined == false
            const bool reverse = val_reverse->as_bool(); // undefined == false
            const bool by_value = is_val<value_string>(val_by) && val_by->as_string().str() == "value" ? true : false;
            auto result = mk_val<value_object>(val_input); // copy
            std::sort(result->val_obj.begin(), result->val_obj.end(), [&](const auto & a, const auto & b) {
                if (by_value) {
                    return value_compare(a.second, b.second, reverse ? value_compare_op::gt : value_compare_op::lt);
                } else {
                    return value_compare(a.first, b.first, reverse ? value_compare_op::gt : value_compare_op::lt);
                }
            });
            return result;
        }},
        {"join", [](const func_args &) -> value {
            throw not_implemented_exception("object join not implemented");
        }},
    };
    return builtins;
}

const func_builtins & value_none_t::get_builtins() const {
    static const func_builtins builtins = {
        {"default", default_value},
        {"tojson", tojson},
        {"string", [](const func_args &) -> value {
            return mk_val<value_string>("None");
        }},
        {"safe", [](const func_args &) -> value {
            return mk_val<value_string>("None");
        }},
        {"strip", [](const func_args &) -> value {
            return mk_val<value_string>("None");
        }},
        {"items", empty_value_fn<value_array>},
        {"map", empty_value_fn<value_array>},
        {"reject", empty_value_fn<value_array>},
        {"rejectattr", empty_value_fn<value_array>},
        {"select", empty_value_fn<value_array>},
        {"selectattr", empty_value_fn<value_array>},
        {"unique", empty_value_fn<value_array>},
    };
    return builtins;
}


const func_builtins & value_undefined_t::get_builtins() const {
    static const func_builtins builtins = {
        {"default", default_value},
        {"capitalize", empty_value_fn<value_string>},
        {"first", empty_value_fn<value_undefined>},
        {"items", empty_value_fn<value_array>},
        {"join", empty_value_fn<value_string>},
        {"last", empty_value_fn<value_undefined>},
        {"length", empty_value_fn<value_int>},
        {"list", empty_value_fn<value_array>},
        {"lower", empty_value_fn<value_string>},
        {"map", empty_value_fn<value_array>},
        {"max", empty_value_fn<value_undefined>},
        {"min", empty_value_fn<value_undefined>},
        {"reject", empty_value_fn<value_array>},
        {"rejectattr", empty_value_fn<value_array>},
        {"replace", empty_value_fn<value_string>},
        {"reverse", empty_value_fn<value_array>},
        {"safe", empty_value_fn<value_string>},
        {"select", empty_value_fn<value_array>},
        {"selectattr", empty_value_fn<value_array>},
        {"sort", empty_value_fn<value_array>},
        {"string", empty_value_fn<value_string>},
        {"strip", empty_value_fn<value_string>},
        {"sum", empty_value_fn<value_int>},
        {"title", empty_value_fn<value_string>},
        {"truncate", empty_value_fn<value_string>},
        {"unique", empty_value_fn<value_array>},
        {"upper", empty_value_fn<value_string>},
        {"wordcount", empty_value_fn<value_int>},
    };
    return builtins;
}


//////////////////////////////////


static value from_json(const nlohmann::ordered_json & j, bool mark_input) {
    if (j.is_null()) {
        return mk_val<value_none>();
    } else if (j.is_boolean()) {
        return mk_val<value_bool>(j.get<bool>());
    } else if (j.is_number_integer()) {
        return mk_val<value_int>(j.get<int64_t>());
    } else if (j.is_number_float()) {
        return mk_val<value_float>(j.get<double>());
    } else if (j.is_string()) {
        auto str = mk_val<value_string>(j.get<std::string>());
        if (mark_input) {
            str->mark_input();
        }
        return str;
    } else if (j.is_array()) {
        auto arr = mk_val<value_array>();
        for (const auto & item : j) {
            arr->push_back(from_json(item, mark_input));
        }
        return arr;
    } else if (j.is_object()) {
        auto obj = mk_val<value_object>();
        for (auto it = j.begin(); it != j.end(); ++it) {
            obj->insert(it.key(), from_json(it.value(), mark_input));
        }
        return obj;
    } else {
        throw std::runtime_error("Unsupported JSON value type");
    }
}

// compare operator for value_t
bool value_compare(const value & a, const value & b, value_compare_op op) {
    auto cmp = [&]() {
        // compare numeric types
        if ((is_val<value_int>(a) || is_val<value_float>(a)) &&
            (is_val<value_int>(b) || is_val<value_float>(b))){
            try {
                if (op == value_compare_op::eq) {
                    return a->as_float() == b->as_float();
                } else if (op == value_compare_op::ge) {
                    return a->as_float() >= b->as_float();
                } else if (op == value_compare_op::gt) {
                    return a->as_float() > b->as_float();
                } else if (op == value_compare_op::lt) {
                    return a->as_float() < b->as_float();
                } else if (op == value_compare_op::ne) {
                    return a->as_float() != b->as_float();
                } else {
                    throw std::runtime_error("Unsupported comparison operator for numeric types");
                }
            } catch (...) {}
        }
        // compare string and number
        // TODO: not sure if this is the right behavior
        if ((is_val<value_string>(b) && (is_val<value_int>(a) || is_val<value_float>(a))) ||
            (is_val<value_string>(a) && (is_val<value_int>(b) || is_val<value_float>(b))) ||
            (is_val<value_string>(a) && is_val<value_string>(b))) {
            try {
                if (op == value_compare_op::eq) {
                    return a->as_string().str() == b->as_string().str();
                } else if (op == value_compare_op::ge) {
                    return a->as_string().str() >= b->as_string().str();
                } else if (op == value_compare_op::gt) {
                    return a->as_string().str() > b->as_string().str();
                } else if (op == value_compare_op::lt) {
                    return a->as_string().str() < b->as_string().str();
                } else if (op == value_compare_op::ne) {
                    return a->as_string().str() != b->as_string().str();
                } else {
                    throw std::runtime_error("Unsupported comparison operator for string/number types");
                }
            } catch (...) {}
        }
        // compare boolean simple
        if (is_val<value_bool>(a) && is_val<value_bool>(b)) {
            if (op == value_compare_op::eq) {
                return a->as_bool() == b->as_bool();
            } else if (op == value_compare_op::ne) {
                return a->as_bool() != b->as_bool();
            } else {
                throw std::runtime_error("Unsupported comparison operator for bool type");
            }
        }
        // compare by type
        if (a->type() != b->type()) {
            return false;
        }
        return false;
    };
    auto result = cmp();
    JJ_DEBUG("Comparing types: %s and %s result=%d", a->type().c_str(), b->type().c_str(), result);
    return result;
}

template<>
void global_from_json(context & ctx, const nlohmann::ordered_json & json_obj, bool mark_input) {
    // printf("global_from_json: %s\n" , json_obj.dump(2).c_str());
    if (json_obj.is_null() || !json_obj.is_object()) {
        throw std::runtime_error("global_from_json: input JSON value must be an object");
    }
    for (auto it = json_obj.begin(); it != json_obj.end(); ++it) {
        JJ_DEBUG("global_from_json: setting key '%s'", it.key().c_str());
        ctx.set_val(it.key(), from_json(it.value(), mark_input));
    }
}

// recursively convert value to JSON string
// TODO: avoid circular references
static void value_to_json_internal(std::ostringstream & oss, const value & val, int curr_lvl, int indent, const std::string_view item_sep, const std::string_view key_sep) {
    auto indent_str = [indent, curr_lvl]() -> std::string {
        return (indent > 0) ? std::string(curr_lvl * indent, ' ') : "";
    };
    auto newline = [indent]() -> std::string {
        return (indent >= 0) ? "\n" : "";
    };

    if (is_val<value_none>(val) || val->is_undefined()) {
        oss << "null";
    } else if (is_val<value_bool>(val)) {
        oss << (val->as_bool() ? "true" : "false");
    } else if (is_val<value_int>(val)) {
        oss << val->as_int();
    } else if (is_val<value_float>(val)) {
        oss << val->as_float();
    } else if (is_val<value_string>(val)) {
        oss << "\"";
        for (char c : val->as_string().str()) {
            switch (c) {
                case '"': oss << "\\\""; break;
                case '\\': oss << "\\\\"; break;
                case '\b': oss << "\\b"; break;
                case '\f': oss << "\\f"; break;
                case '\n': oss << "\\n"; break;
                case '\r': oss << "\\r"; break;
                case '\t': oss << "\\t"; break;
                default:
                    if (static_cast<unsigned char>(c) < 0x20) {
                        char buf[7];
                        snprintf(buf, sizeof(buf), "\\u%04x", static_cast<unsigned char>(c));
                        oss << buf;
                    } else {
                        oss << c;
                    }
            }
        }
        oss << "\"";
    } else if (is_val<value_array>(val)) {
        const auto & arr = val->as_array();
        oss << "[";
        if (!arr.empty()) {
            oss << newline();
            for (size_t i = 0; i < arr.size(); ++i) {
                oss << indent_str() << (indent > 0 ? std::string(indent, ' ') : "");
                value_to_json_internal(oss, arr[i], curr_lvl + 1, indent, item_sep, key_sep);
                if (i < arr.size() - 1) {
                    oss << item_sep;
                }
                oss << newline();
            }
            oss << indent_str();
        }
        oss << "]";
    } else if (is_val<value_object>(val)) {
        const auto & obj = val->as_ordered_object(); // IMPORTANT: need to keep exact order
        oss << "{";
        if (!obj.empty()) {
            oss << newline();
            size_t i = 0;
            for (const auto & pair : obj) {
                oss << indent_str() << (indent > 0 ? std::string(indent, ' ') : "");
                value_to_json_internal(oss, mk_val<value_string>(pair.first->as_string().str()), curr_lvl + 1, indent, item_sep, key_sep);
                oss << key_sep;
                value_to_json_internal(oss, pair.second, curr_lvl + 1, indent, item_sep, key_sep);
                if (i < obj.size() - 1) {
                    oss << item_sep;
                }
                oss << newline();
                ++i;
            }
            oss << indent_str();
        }
        oss << "}";
    } else {
        oss << "null";
    }
}

std::string value_to_json(const value & val, int indent, const std::string_view item_sep, const std::string_view key_sep) {
    std::ostringstream oss;
    value_to_json_internal(oss, val, 0, indent, item_sep, key_sep);
    JJ_DEBUG("value_to_json: result=%s", oss.str().c_str());
    return oss.str();
}

// TODO: avoid circular references
std::string value_to_string_repr(const value & val) {
    if (is_val<value_string>(val)) {
        const std::string val_str = val->as_string().str();

        if (val_str.find('\'') != std::string::npos) {
            return value_to_json(val);
        } else {
            return "'" + val_str + "'";
        }
    } else {
        return val->as_repr();
    }
}

// stats utility
void value_t::stats_t::mark_used(value & val, bool deep) {
    val->stats.used = true;
    if (deep) {
        if (is_val<value_array>(val)) {
            for (auto & item : val->val_arr) {
                mark_used(item, deep);
            }
        } else if (is_val<value_object>(val)) {
            for (auto & pair : val->val_obj) {
                mark_used(pair.first, deep);
                mark_used(pair.second, deep);
            }
        }
    }
}

} // namespace jinja
