/*
 * Copyright (c) 2020-2023, Andreas Kling <andreas@ladybird.org>
 * Copyright (c) 2020-2023, Linus Groh <linusg@serenityos.org>
 * Copyright (c) 2020-2022, Ali Mohammad Pur <mpfard@serenityos.org>
 * Copyright (c) 2025, Ryszard Goc <ryszardgoc@gmail.com>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <AK/NeverDestroyed.h>
#include <AK/Platform.h>
#include <AK/ScopeGuard.h>
#include <AK/StringBuilder.h>
#include <AK/Utf16String.h>
#include <LibCore/ArgsParser.h>
#include <LibCore/ConfigFile.h>
#include <LibCore/StandardPaths.h>
#include <LibCrypto/Hash/SHA2.h>
#include <LibJS/Bytecode/Debug.h>
#include <LibJS/Console.h>
#include <LibJS/Contrib/Test262/GlobalObject.h>
#include <LibJS/Print.h>
#include <LibJS/Runtime/ConsoleObject.h>
#include <LibJS/Runtime/DeclarativeEnvironment.h>
#include <LibJS/Runtime/GlobalEnvironment.h>
#include <LibJS/Runtime/JSONObject.h>
#include <LibJS/Runtime/Reference.h>
#include <LibJS/Runtime/StringPrototype.h>
#include <LibJS/Runtime/VM.h>
#include <LibJS/Runtime/ValueInlines.h>
#include <LibJS/RustFFI.h>
#include <LibJS/Script.h>
#include <LibJS/SourceCode.h>
#include <LibJS/SourceTextModule.h>
#include <LibJS/Token.h>
#include <LibMain/Main.h>
#include <LibTextCodec/Decoder.h>

#if !defined(AK_OS_WINDOWS) && !defined(AK_OS_ANDROID)
#    include <editline/readline.h>
#endif
#include <stdlib.h>
#include <string.h>

// FIXME: https://github.com/LadybirdBrowser/ladybird/issues/2412
//    We should be able to destroy the VM on process exit.
NeverDestroyed<RefPtr<JS::VM>> g_vm_storage;
JS::VM* g_vm;
Vector<String> g_repl_statements;
GC::Root<JS::Value> g_last_value = GC::make_root(JS::js_undefined());

class ReplObject final : public JS::GlobalObject {
    JS_OBJECT(ReplObject, JS::GlobalObject);
    GC_DECLARE_ALLOCATOR(ReplObject);

public:
    ReplObject(JS::Realm& realm)
        : GlobalObject(realm)
    {
    }
    virtual void initialize(JS::Realm&) override;
    virtual ~ReplObject() override = default;

private:
    JS_DECLARE_NATIVE_FUNCTION(exit_interpreter);
    JS_DECLARE_NATIVE_FUNCTION(repl_help);
    JS_DECLARE_NATIVE_FUNCTION(save_to_file);
    JS_DECLARE_NATIVE_FUNCTION(load_ini);
    JS_DECLARE_NATIVE_FUNCTION(load_json);
    JS_DECLARE_NATIVE_FUNCTION(last_value_getter);
    JS_DECLARE_NATIVE_FUNCTION(print);
    JS_DECLARE_NATIVE_FUNCTION(gc);
};

GC_DEFINE_ALLOCATOR(ReplObject);

class ScriptObject final : public JS::GlobalObject {
    JS_OBJECT(ScriptObject, JS::GlobalObject);
    GC_DECLARE_ALLOCATOR(ScriptObject);

public:
    ScriptObject(JS::Realm& realm)
        : JS::GlobalObject(realm)
    {
    }
    virtual void initialize(JS::Realm&) override;
    virtual ~ScriptObject() override = default;

private:
    JS_DECLARE_NATIVE_FUNCTION(load_ini);
    JS_DECLARE_NATIVE_FUNCTION(load_json);
    JS_DECLARE_NATIVE_FUNCTION(print);
    JS_DECLARE_NATIVE_FUNCTION(gc);
};

GC_DEFINE_ALLOCATOR(ScriptObject);

static bool s_dump_ast = false;
static bool s_as_module = false;
static bool s_print_last_result = false;
static bool s_strip_ansi = false;
static bool s_raw_strings = false;
static bool s_disable_source_location_hints = false;
#if !defined(AK_OS_WINDOWS) && !defined(AK_OS_ANDROID)
static JS::Realm* s_repl_realm;
static JS::GlobalEnvironment* s_repl_global_environment;
#endif
static String s_history_path = String {};
[[maybe_unused]] static int s_repl_line_level = 0;
[[maybe_unused]] static bool s_keep_running_repl = true;
static int s_exit_code = 0;

static ErrorOr<void> print_inline(JS::Value value, Stream& stream)
{
    JS::PrintContext print_context { .vm = *g_vm, .stream = &stream, .strip_ansi = s_strip_ansi, .raw_strings = s_raw_strings };
    return JS::print(value, print_context);
}

enum class PrintTarget {
    StandardError,
    StandardOutput,
};

static ErrorOr<NonnullOwnPtr<Stream>> flushed_print_stream(PrintTarget target)
{
    if (target == PrintTarget::StandardOutput) {
        (void)fflush(stdout);
        return Core::File::standard_output();
    }

    (void)fflush(stderr);
    return Core::File::standard_error();
}

enum class PrintEnd {
    Newline,
    None
};

static ErrorOr<void> print(JS::Value value, PrintTarget target = PrintTarget::StandardOutput, PrintEnd end = PrintEnd::Newline)
{
    auto stream = TRY(flushed_print_stream(target));

    TRY(print_inline(value, *stream));

    if (end == PrintEnd::Newline)
        TRY(stream->write_until_depleted("\n"sv));

    return {};
}

static ErrorOr<void> print_all_arguments(JS::VM const& vm, PrintTarget target = PrintTarget::StandardOutput, PrintEnd end = PrintEnd::Newline)
{
    auto stream = TRY(flushed_print_stream(target));

    for (size_t i = 0; i < vm.argument_count(); i++) {
        TRY(print_inline(vm.argument(i), *stream));

        if (i < vm.argument_count() - 1) {
            TRY(stream->write_until_depleted(" "sv));
        }
    }

    if (end == PrintEnd::Newline)
        TRY(stream->write_until_depleted("\n"sv));

    return {};
}

[[maybe_unused]] static ErrorOr<String> prompt_for_level(int level)
{
    static StringBuilder prompt_builder;
    prompt_builder.clear();
    prompt_builder.append("> "sv);

    for (auto i = 0; i < level; ++i)
        prompt_builder.append("    "sv);

    return prompt_builder.to_string();
}

static ErrorOr<void> write_to_file(String const& path)
{
    auto file = TRY(Core::File::open(path, Core::File::OpenMode::Write, 0666));
    for (size_t i = 0; i < g_repl_statements.size(); i++) {
        auto line = g_repl_statements[i].bytes();
        if (line.size() > 0 && i != g_repl_statements.size() - 1) {
            TRY(file->write_until_depleted(line));
        }
        if (i != g_repl_statements.size() - 1) {
            TRY(file->write_value('\n'));
        }
    }
    file->close();
    return {};
}

static ErrorOr<bool> parse_and_run(JS::Realm& realm, StringView source, StringView source_name, bool parse_only = false)
{
    auto& vm = realm.vm();

    JS::ThrowCompletionOr<JS::Value> result { JS::js_undefined() };
    auto utf16_source = Utf16String::from_utf8(source);

    if (!s_as_module) {
        auto script_or_error = JS::Script::parse(utf16_source.utf16_view(), realm, source_name);
        if (script_or_error.is_error()) {
            auto error = script_or_error.error()[0];
            auto hint = error.source_location_hint(utf16_source);
            if (!hint.is_empty())
                outln("{}", hint);

            auto error_string = error.to_utf16_string();
            outln("{}", error_string);
            result = vm.throw_completion<JS::SyntaxError>(move(error_string));
        } else {
            auto script = script_or_error.release_value();

            if (!parse_only)
                result = vm.run(*script);
        }
    } else {
        auto module_or_error = JS::SourceTextModule::parse(utf16_source.utf16_view(), realm, source_name);
        if (module_or_error.is_error()) {
            auto error = module_or_error.error()[0];
            auto hint = error.source_location_hint(utf16_source);
            if (!hint.is_empty())
                outln("{}", hint);

            auto error_string = error.to_utf16_string();
            outln("{}", error_string);
            result = vm.throw_completion<JS::SyntaxError>(move(error_string));
        } else {
            auto module = module_or_error.release_value();
            if (!parse_only)
                result = vm.run(*module);
        }
    }

    auto handle_exception = [&](JS::Value thrown_value) -> ErrorOr<void> {
        warnln("Uncaught exception: ");
        TRY(print(thrown_value, PrintTarget::StandardError));

        if (auto error = thrown_value.template as_if<JS::Error>())
            warnln("{}", error->stack_string(JS::CompactTraceback::Yes));
        return {};
    };

    if (!result.is_error())
        g_last_value = GC::make_root(result.value());

    if (result.is_error()) {
        TRY(handle_exception(result.release_error().value()));
        return false;
    }

    if (s_print_last_result) {
        TRY(print(result.value()));
    }

    return true;
}

static JS::ThrowCompletionOr<JS::Value> load_ini_impl(JS::VM& vm)
{
    auto& realm = *vm.current_realm();

    auto filename = TRY(vm.argument(0).to_utf16_string(vm)).to_utf8_but_should_be_ported_to_utf16().to_byte_string();
    auto file_or_error = Core::File::open(filename, Core::File::OpenMode::Read);
    if (file_or_error.is_error())
        return vm.throw_completion<JS::Error>(Utf16String::formatted("Failed to open '{}': {}", filename, file_or_error.error()));

    auto config_file_or_error = Core::ConfigFile::open(filename, file_or_error.release_value());
    if (config_file_or_error.is_error())
        return vm.throw_completion<JS::Error>(Utf16String::formatted("Failed to read '{}': {}", filename, config_file_or_error.error()));
    auto config_file = config_file_or_error.release_value();
    auto object = JS::Object::create(realm, realm.intrinsics().object_prototype());
    for (auto const& group : config_file->groups()) {
        auto group_object = JS::Object::create(realm, realm.intrinsics().object_prototype());
        for (auto const& key : config_file->keys(group)) {
            auto entry = config_file->read_entry(group, key);
            group_object->define_direct_property(Utf16String::from_utf8(key), JS::PrimitiveString::create(vm, Utf16String::from_utf8(entry)), JS::Attribute::Enumerable | JS::Attribute::Configurable | JS::Attribute::Writable);
        }
        object->define_direct_property(Utf16String::from_utf8(group), group_object, JS::Attribute::Enumerable | JS::Attribute::Configurable | JS::Attribute::Writable);
    }
    return object;
}

static JS::ThrowCompletionOr<JS::Value> load_json_impl(JS::VM& vm)
{
    auto filename = TRY(vm.argument(0).to_utf16_string(vm)).to_utf8_but_should_be_ported_to_utf16();
    auto file_or_error = Core::File::open(filename, Core::File::OpenMode::Read);
    if (file_or_error.is_error())
        return vm.throw_completion<JS::Error>(Utf16String::formatted("Failed to open '{}': {}", filename, file_or_error.error()));

    auto file_contents_or_error = file_or_error.value()->read_until_eof();
    if (file_contents_or_error.is_error())
        return vm.throw_completion<JS::Error>(Utf16String::formatted("Failed to read '{}': {}", filename, file_contents_or_error.error()));

    auto file_contents = file_contents_or_error.release_value();
    auto json_text = Utf16String::try_from_utf8(StringView { file_contents.bytes() });
    if (json_text.is_error())
        return vm.throw_completion<JS::SyntaxError>(JS::ErrorType::JsonMalformed);

    return JS::JSONObject::parse_json(vm, json_text.release_value());
}

void ReplObject::initialize(JS::Realm& realm)
{
    Base::initialize(realm);

    define_direct_property("global"_utf16_fly_string, this, JS::Attribute::Enumerable);
    u8 attr = JS::Attribute::Configurable | JS::Attribute::Writable | JS::Attribute::Enumerable;
    define_native_function(realm, "exit"_utf16_fly_string, exit_interpreter, 0, attr);
    define_native_function(realm, "help"_utf16_fly_string, repl_help, 0, attr);
    define_native_function(realm, "save"_utf16_fly_string, save_to_file, 1, attr);
    define_native_function(realm, "loadINI"_utf16_fly_string, load_ini, 1, attr);
    define_native_function(realm, "loadJSON"_utf16_fly_string, load_json, 1, attr);
    define_native_function(realm, "print"_utf16_fly_string, print, 1, attr);
    define_native_function(realm, "gc"_utf16_fly_string, gc, 0, attr);

    define_native_accessor(
        realm,
        "_"_utf16_fly_string,
        [](JS::VM&) {
            return g_last_value.value();
        },
        [](JS::VM& vm) -> JS::ThrowCompletionOr<JS::Value> {
            auto& global_object = vm.get_global_object();
            VERIFY(is<ReplObject>(global_object));
            outln("Disable writing last value to '_'");

            // We must delete first otherwise this setter gets called recursively.
            TRY(global_object.internal_delete(vm.names._));

            auto value = vm.argument(0);
            TRY(global_object.internal_set(vm.names._, value, &global_object));
            return value;
        },
        attr);
}

JS_DEFINE_NATIVE_FUNCTION(ReplObject::save_to_file)
{
    if (!vm.argument_count())
        return JS::Value(false);
    auto const save_path = TRY(vm.argument(0).to_utf16_string(vm)).to_utf8_but_should_be_ported_to_utf16();
    if (!write_to_file(save_path).is_error()) {
        return JS::Value(true);
    }
    return JS::Value(false);
}

JS_DEFINE_NATIVE_FUNCTION(ReplObject::exit_interpreter)
{
    if (vm.argument_count() != 0)
        s_exit_code = TRY(vm.argument(0).to_number(vm)).as_double();

    s_keep_running_repl = false;
    return JS::js_undefined();
}

JS_DEFINE_NATIVE_FUNCTION(ReplObject::repl_help)
{
    warnln("REPL commands:");
    warnln("    exit(code): exit the REPL with specified code. Defaults to 0.");
    warnln("    help(): display this menu");
    warnln("    loadINI(file): load the given file as INI.");
    warnln("    loadJSON(file): load the given file as JSON.");
    warnln("    print(value): pretty-print the given JS value.");
    warnln("    save(file): write REPL input history to the given file. For example: save(\"foo.txt\")");
    return JS::js_undefined();
}

JS_DEFINE_NATIVE_FUNCTION(ReplObject::load_ini)
{
    return load_ini_impl(vm);
}

JS_DEFINE_NATIVE_FUNCTION(ReplObject::load_json)
{
    return load_json_impl(vm);
}

JS_DEFINE_NATIVE_FUNCTION(ReplObject::print)
{
    auto result = print_all_arguments(vm);
    if (result.is_error())
        return g_vm->throw_completion<JS::InternalError>(Utf16String::formatted("Failed to print value(s): {}", result.error()));

    return JS::js_undefined();
}

JS_DEFINE_NATIVE_FUNCTION(ReplObject::gc)
{
    vm.heap().collect_garbage();
    return JS::js_undefined();
}

void ScriptObject::initialize(JS::Realm& realm)
{
    Base::initialize(realm);

    define_direct_property("global"_utf16_fly_string, this, JS::Attribute::Enumerable);
    u8 attr = JS::Attribute::Configurable | JS::Attribute::Writable | JS::Attribute::Enumerable;
    define_native_function(realm, "loadINI"_utf16_fly_string, load_ini, 1, attr);
    define_native_function(realm, "loadJSON"_utf16_fly_string, load_json, 1, attr);
    define_native_function(realm, "print"_utf16_fly_string, print, 1, attr);
    define_native_function(realm, "gc"_utf16_fly_string, gc, 0, attr);
}

JS_DEFINE_NATIVE_FUNCTION(ScriptObject::load_ini)
{
    return load_ini_impl(vm);
}

JS_DEFINE_NATIVE_FUNCTION(ScriptObject::load_json)
{
    return load_json_impl(vm);
}

JS_DEFINE_NATIVE_FUNCTION(ScriptObject::print)
{
    auto result = print_all_arguments(vm);
    if (result.is_error())
        return g_vm->throw_completion<JS::InternalError>(Utf16String::formatted("Failed to print value(s): {}", result.error()));

    return JS::js_undefined();
}

JS_DEFINE_NATIVE_FUNCTION(ScriptObject::gc)
{
    vm.heap().collect_garbage();
    return JS::js_undefined();
}

class ReplConsoleClient final : public JS::ConsoleClient {
    GC_CELL(ReplConsoleClient, JS::ConsoleClient);

public:
    ReplConsoleClient(JS::Console& console)
        : ConsoleClient(console)
    {
    }

    virtual void clear() override
    {
        out("\033[3J\033[H\033[2J");
        m_group_stack_depth = 0;
        fflush(stdout);
    }

    virtual void end_group() override
    {
        if (m_group_stack_depth > 0)
            m_group_stack_depth--;
    }

    // 2.3. Printer(logLevel, args[, options]), https://console.spec.whatwg.org/#printer
    virtual JS::ThrowCompletionOr<JS::Value> printer(JS::Console::LogLevel log_level, PrinterArguments arguments) override
    {
        auto indent = TRY_OR_THROW_OOM(*g_vm, String::repeated(' ', m_group_stack_depth * 2));

        if (log_level == JS::Console::LogLevel::Trace) {
            auto trace = arguments.get<JS::Console::Trace>();
            StringBuilder builder;
            if (!trace.label.is_empty())
                builder.appendff("{}\033[36;1m{}\033[0m\n", indent, trace.label);

            for (auto& frame : trace.stack)
                builder.appendff("{}-> {}\n", indent, frame.function_name);

            outln("{}", builder.string_view());
            return JS::js_undefined();
        }

        if (log_level == JS::Console::LogLevel::Group || log_level == JS::Console::LogLevel::GroupCollapsed) {
            auto group = arguments.get<JS::Console::Group>();
            outln("{}\033[36;1m{}\033[0m", indent, group.label);
            m_group_stack_depth++;
            return JS::js_undefined();
        }

        auto output = TRY(generically_format_values(arguments.get<GC::RootVector<JS::Value>>()));

        switch (log_level) {
        case JS::Console::LogLevel::Debug:
            outln("{}\033[36;1m{}\033[0m", indent, output);
            break;
        case JS::Console::LogLevel::Error:
        case JS::Console::LogLevel::Assert:
            outln("{}\033[31;1m{}\033[0m", indent, output);
            break;
        case JS::Console::LogLevel::Info:
            outln("{}(i) {}", indent, output);
            break;
        case JS::Console::LogLevel::Log:
            outln("{}{}", indent, output);
            break;
        case JS::Console::LogLevel::Warn:
        case JS::Console::LogLevel::CountReset:
            outln("{}\033[33;1m{}\033[0m", indent, output);
            break;
        default:
            outln("{}{}", indent, output);
            break;
        }
        return JS::js_undefined();
    }

private:
    int m_group_stack_depth { 0 };
};

#if !defined(AK_OS_WINDOWS) && !defined(AK_OS_ANDROID)
static Vector<ByteString> complete_repl_line(StringView line)
{
    auto& realm = *s_repl_realm;
    auto& global_environment = *s_repl_global_environment;
    auto source_code = JS::SourceCode::create({}, Utf16String::from_utf8(line));
    auto const& code_view = source_code->code_view();

    enum {
        Initial,
        CompleteVariable,
        CompleteNullProperty,
        CompleteProperty,
    } mode { Initial };

    Utf16FlyString variable_name;
    Utf16FlyString property_name;
    bool last_token_has_trivia = false;

    struct CompleteState {
        decltype(mode)* current_mode;
        Utf16FlyString* variable_name;
        Utf16FlyString* property_name;
        bool* last_token_has_trivia;
        Utf16View const* code_view;
    } complete_state { &mode, &variable_name, &property_name, &last_token_has_trivia, &code_view };

    // We're only going to complete either
    //    - <N>
    //        where N is part of the name of a variable
    //    - <N>.<P>
    //        where N is the complete name of a variable and
    //        P is part of the name of one of its properties
    JS::FFI::rust_tokenize(source_code->utf16_data(), source_code->length_in_code_units(), &complete_state,
        [](void* ctx, JS::FFI::FFIToken const* tok) {
            auto& s = *static_cast<CompleteState*>(ctx);
            auto type = static_cast<JS::TokenType>(tok->token_type);
            auto category = static_cast<JS::TokenCategory>(tok->category);
            if (type == JS::TokenType::Eof) {
                *s.last_token_has_trivia = tok->trivia_length > 0;
                return;
            }

            auto token_value = [&]() {
                return Utf16FlyString::from_utf16(s.code_view->substring_view(tok->offset, tok->length));
            };
            bool is_identifier_name = type != JS::TokenType::PrivateIdentifier
                && (category == JS::TokenCategory::Identifier || category == JS::TokenCategory::Keyword || category == JS::TokenCategory::ControlKeyword);

            switch (*s.current_mode) {
            case CompleteVariable:
                if (type == JS::TokenType::Period)
                    *s.current_mode = CompleteNullProperty;
                else
                    *s.current_mode = Initial;
                break;
            case CompleteNullProperty:
                if (is_identifier_name) {
                    *s.current_mode = CompleteProperty;
                    *s.property_name = token_value();
                } else {
                    *s.current_mode = Initial;
                }
                break;
            case CompleteProperty:
            case Initial:
                if (type == JS::TokenType::Identifier) {
                    *s.current_mode = CompleteVariable;
                    *s.variable_name = token_value();
                } else {
                    *s.current_mode = Initial;
                }
                break;
            }
        });

    if (mode == CompleteNullProperty) {
        mode = CompleteProperty;
        property_name = Utf16FlyString {};
        last_token_has_trivia = false; // <name> <dot> [tab] is sensible to complete.
    }

    if (mode == Initial || last_token_has_trivia)
        return {}; // we do not know how to complete this

    Vector<ByteString> results;

    Function<void(JS::Shape const&, Utf16FlyString const&)> list_all_properties = [&results, &list_all_properties](JS::Shape const& shape, Utf16FlyString const& property_pattern) {
        shape.for_each_property_in_insertion_order([&](auto const& property_key, auto const&) {
            if (!property_key.is_string())
                return;

            auto key = property_key.as_string().to_utf16_string();

            if (key.starts_with(property_pattern.view())) {
                auto completion = key.to_byte_string();
                if (!results.contains_slow(completion)) // hide duplicates
                    results.append(move(completion));
            }
        });
        if (auto const* prototype = shape.prototype()) {
            list_all_properties(prototype->shape(), property_pattern);
        }
    };

    switch (mode) {
    case CompleteProperty: {
        auto reference_or_error = g_vm->resolve_binding(variable_name, JS::Strict::No, &global_environment);
        if (reference_or_error.is_error())
            return {};
        auto value_or_error = reference_or_error.value().get_value(*g_vm);
        if (value_or_error.is_error())
            return {};
        auto variable = value_or_error.value();
        VERIFY(!variable.is_special_empty_value());

        if (auto object = variable.template as_if<JS::Object>()) {
            auto const& shape = object->shape();
            list_all_properties(shape, property_name);
            for (auto& result : results) {
                StringBuilder builder;
                builder.append(MUST(variable_name.view().to_byte_string()));
                builder.append('.');
                builder.append(result);
                result = builder.to_byte_string();
            }
        }
        break;
    }
    case CompleteVariable: {
        auto const& variable = realm.global_object();
        list_all_properties(variable.shape(), variable_name);

        for (auto const& name : global_environment.declarative_record().bindings()) {
            if (name.view().starts_with(variable_name.view()))
                results.empend(MUST(name.view().to_byte_string()));
        }

        break;
    }
    default:
        VERIFY_NOT_REACHED();
    }

    return results;
}

static char** complete_repl_line_for_readline(char const*, int, int)
{
    if (!rl_line_buffer)
        return nullptr;

    rl_attempted_completion_over = 1;

    auto completions = complete_repl_line(StringView { rl_line_buffer, strlen(rl_line_buffer) });
    if (completions.is_empty())
        return nullptr;

    auto common_prefix = StringView { completions[0] };
    for (size_t i = 1; i < completions.size(); ++i) {
        size_t prefix_length = 0;
        auto completion = StringView { completions[i] };
        while (prefix_length < common_prefix.length()
            && prefix_length < completion.length()
            && common_prefix[prefix_length] == completion[prefix_length]) {
            ++prefix_length;
        }
        common_prefix = common_prefix.substring_view(0, prefix_length);
    }

    auto** matches = static_cast<char**>(calloc(completions.size() + 2, sizeof(char*)));
    if (!matches)
        return nullptr;

    matches[0] = strndup(common_prefix.characters_without_null_termination(), common_prefix.length());
    if (!matches[0]) {
        free(matches);
        return nullptr;
    }

    for (size_t i = 0; i < completions.size(); ++i) {
        matches[i + 1] = strdup(completions[i].characters());
        if (!matches[i + 1]) {
            for (size_t j = 0; j <= i; ++j)
                free(matches[j]);
            free(matches);
            return nullptr;
        }
    }

    return matches;
}

static ErrorOr<String> read_next_piece()
{
    StringBuilder piece;

    auto line_level_delta_for_next_line { 0 };

    do {
        auto prompt = TRY(prompt_for_level(s_repl_line_level)).to_byte_string();
        auto* raw_line = readline(prompt.characters());

        line_level_delta_for_next_line = 0;

        if (!raw_line) {
            s_keep_running_repl = false;
            return String {};
        }
        ArmedScopeGuard free_raw_line = [&] {
            free(raw_line);
        };

        auto line = TRY(String::from_utf8(StringView { raw_line, strlen(raw_line) }));
        if (!line.is_empty())
            add_history(line.to_byte_string().characters());

        piece.append(line);
        piece.append('\n');

        auto source_code = JS::SourceCode::create({}, Utf16String::from_utf8(line));

        enum {
            NotInLabelOrObjectKey,
            InLabelOrObjectKeyIdentifier,
            InLabelOrObjectKey
        } label_state { NotInLabelOrObjectKey };

        struct BracketState {
            decltype(label_state)* label;
            int* level;
        } bracket_state { &label_state, &s_repl_line_level };

        JS::FFI::rust_tokenize(source_code->utf16_data(), source_code->length_in_code_units(), &bracket_state,
            [](void* ctx, JS::FFI::FFIToken const* tok) {
                auto& state = *static_cast<BracketState*>(ctx);
                auto type = static_cast<JS::TokenType>(tok->token_type);
                switch (type) {
                case JS::TokenType::BracketOpen:
                case JS::TokenType::CurlyOpen:
                case JS::TokenType::ParenOpen:
                    *state.label = NotInLabelOrObjectKey;
                    (*state.level)++;
                    break;
                case JS::TokenType::BracketClose:
                case JS::TokenType::CurlyClose:
                case JS::TokenType::ParenClose:
                    *state.label = NotInLabelOrObjectKey;
                    (*state.level)--;
                    break;
                case JS::TokenType::Identifier:
                case JS::TokenType::StringLiteral:
                    if (*state.label == NotInLabelOrObjectKey)
                        *state.label = InLabelOrObjectKeyIdentifier;
                    else
                        *state.label = NotInLabelOrObjectKey;
                    break;
                case JS::TokenType::Colon:
                    if (*state.label == InLabelOrObjectKeyIdentifier)
                        *state.label = InLabelOrObjectKey;
                    else
                        *state.label = NotInLabelOrObjectKey;
                    break;
                default:
                    break;
                }
            });

        if (label_state == InLabelOrObjectKey) {
            // If there's a label or object literal key at the end of this line,
            // prompt for more lines but do not change the line level.
            line_level_delta_for_next_line += 1;
        }
    } while (s_repl_line_level + line_level_delta_for_next_line > 0);

    return piece.to_string();
}

static ErrorOr<void> repl(JS::Realm& realm)
{
    while (s_keep_running_repl) {
        auto const piece = TRY(read_next_piece());
        if (Utf16String::from_utf8(piece).trim(JS::whitespace_characters).is_empty())
            continue;

        g_repl_statements.append(piece);
        TRY(parse_and_run(realm, piece, "REPL"sv));
    }
    return {};
}

static ErrorOr<int> run_repl(bool gc_on_every_allocation, [[maybe_unused]] bool syntax_highlight)
{
    s_print_last_result = true;

    auto root_execution_context = JS::create_simple_execution_context<ReplObject>(*g_vm);
    auto& realm = *root_execution_context->realm;

    auto& console_object = *realm.intrinsics().console_object();
    ReplConsoleClient console_client(console_object.console());
    console_object.console().set_client(console_client);
    g_vm->heap().set_should_collect_on_every_allocation(gc_on_every_allocation);

    auto& global_environment = realm.global_environment();
    s_repl_realm = &realm;
    s_repl_global_environment = &global_environment;

    read_history(s_history_path.to_byte_string().characters());
    rl_attempted_completion_function = complete_repl_line_for_readline;

    TRY(repl(realm));
    write_history(s_history_path.to_byte_string().characters());
    return s_exit_code;
}

#endif

ErrorOr<int> ladybird_main(Main::Arguments arguments)
{
    bool gc_on_every_allocation = false;
    bool disable_syntax_highlight = false;
    bool disable_debug_printing = false;
    bool use_test262_global = false;
    bool parse_only = false;
    StringView evaluate_script;
    Vector<StringView> script_paths;

    Core::ArgsParser args_parser;
    args_parser.set_general_help("This is a JavaScript interpreter.");
    args_parser.add_option(parse_only, "Parse only", "parse-only", 'p');
    args_parser.add_option(s_dump_ast, "Dump the AST", "dump-ast", 'A');
    args_parser.add_option(JS::Bytecode::g_dump_bytecode, "Dump the bytecode", "dump-bytecode", 'd');
    args_parser.add_option(s_as_module, "Treat as module", "as-module", 'm');
    args_parser.add_option(s_print_last_result, "Print last result", "print-last-result", 'l');
    args_parser.add_option(s_strip_ansi, "Disable ANSI colors", "disable-ansi-colors", 'i');
    args_parser.add_option(s_disable_source_location_hints, "Disable source location hints", "disable-source-location-hints", 'h');
    args_parser.add_option(gc_on_every_allocation, "GC on every allocation", "gc-on-every-allocation", 'g');
    args_parser.add_option(s_raw_strings, "Display strings without quotes or escape sequences", "raw-strings", 'r');
    args_parser.add_option(disable_syntax_highlight, "Disable live syntax highlighting", "no-syntax-highlight", 's');
    args_parser.add_option(disable_debug_printing, "Disable debug output", "disable-debug-output", {});
    args_parser.add_option(evaluate_script, "Evaluate argument as a script", "evaluate", 'c', "script");
    args_parser.add_option(use_test262_global, "Use test262 global ($262)", "use-test262-global", {});
    args_parser.add_positional_argument(script_paths, "Path to script files", "scripts", Core::ArgsParser::Required::No);
    args_parser.parse(arguments);

    [[maybe_unused]] bool syntax_highlight = !disable_syntax_highlight;

    JS::g_dump_ast = s_dump_ast;
    JS::g_dump_ast_use_color = !s_strip_ansi;

    AK::set_debug_enabled(!disable_debug_printing);
    s_history_path = TRY(String::formatted("{}/.js-history", Core::StandardPaths::home_directory()));

    g_vm_storage.get() = JS::VM::create();
    g_vm = g_vm_storage->ptr();
    g_vm->set_dynamic_imports_allowed(true);

    if (!disable_debug_printing) {
        // NOTE: These will print out both warnings when using something like Promise.reject().catch(...) -
        // which is, as far as I can tell, correct - a promise is created, rejected without handler, and a
        // handler then attached to it. The Node.js REPL doesn't warn in this case, so it's something we
        // might want to revisit at a later point and disable warnings for promises created this way.
        g_vm->on_promise_unhandled_rejection = [](auto& promise) {
            warn("WARNING: A promise was rejected without any handlers");
            warn(" (result: ");
            (void)print(promise.result(), PrintTarget::StandardError, PrintEnd::None);
            warnln(")");
        };
        g_vm->on_promise_rejection_handled = [](auto& promise) {
            warn("WARNING: A handler was added to an already rejected promise");
            warn(" (result: ");
            (void)print(promise.result(), PrintTarget::StandardError, PrintEnd::None);
            warnln(")");
        };
    }

    // FIXME: Figure out some way to interrupt the interpreter now that vm.exception() is gone.

    if (evaluate_script.is_empty() && script_paths.is_empty()) {
#if defined(AK_OS_WINDOWS) || defined(AK_OS_ANDROID)
        dbgln("REPL functionality is not supported on this platform");
        VERIFY_NOT_REACHED();
#else
        return run_repl(gc_on_every_allocation, syntax_highlight);
#endif
    } else {
        OwnPtr<JS::ExecutionContext> root_execution_context;
        if (use_test262_global)
            root_execution_context = JS::create_simple_execution_context<JS::Test262::GlobalObject>(*g_vm);
        else
            root_execution_context = JS::create_simple_execution_context<ScriptObject>(*g_vm);

        auto& realm = *root_execution_context->realm;
        auto& console_object = *realm.intrinsics().console_object();
        ReplConsoleClient console_client(console_object.console());
        console_object.console().set_client(console_client);
        g_vm->heap().set_should_collect_on_every_allocation(gc_on_every_allocation);

        StringBuilder builder;
        StringView source_name;

        if (evaluate_script.is_empty()) {
            if (script_paths.size() > 1)
                warnln("Warning: Multiple files supplied, this will concatenate the sources and resolve modules as if it was the first file");

            for (auto& path : script_paths) {
                auto file = TRY(Core::File::open(path, Core::File::OpenMode::Read));
                auto file_contents = TRY(file->read_until_eof());
                auto source = StringView { file_contents };

                if (Utf8View { file_contents }.validate()) {
                    builder.append(source);
                } else {
                    auto decoder = TextCodec::decoder_for("windows-1252"sv);
                    VERIFY(decoder.has_value());

                    auto utf8_source = TRY(TextCodec::convert_input_to_utf8_using_given_decoder_unless_there_is_a_byte_order_mark(*decoder, source));
                    builder.append(utf8_source);
                }
            }

            source_name = script_paths[0];
        } else {
            builder.append(evaluate_script);
            source_name = "eval"sv;
        }

        // We resolve modules as if it is the first file

        if (!TRY(parse_and_run(realm, builder.string_view(), source_name, parse_only)))
            return 1;
    }

    return s_exit_code;
}
