/*
 * Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <AK/GenericShorthands.h>
#include <AK/JsonObject.h>
#include <AK/JsonValue.h>
#include <AK/MemoryStream.h>
#include <LibJS/Print.h>
#include <LibJS/Runtime/BigInt.h>
#include <LibJS/Runtime/ErrorData.h>
#include <LibJS/Runtime/Realm.h>
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
#include <LibWeb/HTML/Window.h>
#include <WebContent/ConsoleGlobalEnvironmentExtensions.h>
#include <WebContent/DevToolsConsoleClient.h>
#include <WebContent/PageClient.h>

namespace WebContent {

GC_DEFINE_ALLOCATOR(DevToolsConsoleClient);

GC::Ref<DevToolsConsoleClient> DevToolsConsoleClient::create(JS::Realm& realm, JS::Console& console, PageClient& client)
{
    auto& window = as<Web::HTML::Window>(realm.global_object());
    auto console_global_environment_extensions = realm.create<ConsoleGlobalEnvironmentExtensions>(realm, window);

    return realm.heap().allocate<DevToolsConsoleClient>(realm, console, client, console_global_environment_extensions);
}

DevToolsConsoleClient::DevToolsConsoleClient(JS::Realm& realm, JS::Console& console, PageClient& client, ConsoleGlobalEnvironmentExtensions& console_global_environment_extensions)
    : WebContentConsoleClient(realm, console, client, console_global_environment_extensions)
{
}

DevToolsConsoleClient::~DevToolsConsoleClient() = default;

// https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#grips
static JsonValue serialize_js_value(JS::Realm& realm, JS::Value value)
{
    auto& vm = realm.vm();

    auto serialize_type = [](StringView type) {
        JsonObject serialized;
        serialized.set("type"sv, type);
        return serialized;
    };

    if (value.is_undefined())
        return serialize_type("undefined"sv);

    if (value.is_null())
        return serialize_type("null"sv);

    if (value.is_boolean())
        return value.as_bool();

    if (value.is_string())
        return value.as_string().utf8_string();

    if (value.is_number()) {
        if (value.is_nan())
            return serialize_type("NaN"sv);
        if (value.is_positive_infinity())
            return serialize_type("Infinity"sv);
        if (value.is_negative_infinity())
            return serialize_type("-Infinity"sv);
        if (value.is_negative_zero())
            return serialize_type("-0"sv);
        return value.as_double();
    }

    if (value.is_bigint()) {
        auto serialized = serialize_type("BigInt"sv);
        serialized.set("text"sv, MUST(value.as_bigint().big_integer().to_base(10)));
        return serialized;
    }

    if (value.is_symbol())
        return value.as_symbol().descriptive_string().to_utf8();

    // FIXME: Handle serialization of object grips. For now, we stringify the object.
    if (value.is_object()) {
        Web::HTML::TemporaryExecutionContext execution_context { realm };
        AllocatingMemoryStream stream;

        JS::PrintContext context { vm, stream, true };
        MUST(JS::print(value, context));

        return MUST(String::from_stream(stream, stream.used_buffer_size()));
    }

    return {};
}

void DevToolsConsoleClient::handle_result(JS::Value result)
{
    m_client->did_execute_js_console_input(serialize_js_value(m_realm, result));
}

void DevToolsConsoleClient::report_exception(String const& name, String const& message, JS::ErrorData const& error_data, bool in_promise)
{
    Vector<WebView::StackFrame> trace;
    trace.ensure_capacity(error_data.traceback().size());

    for (auto const& frame : error_data.traceback()) {
        auto const& source_range = frame.source_range();
        WebView::StackFrame stack_frame;

        if (!frame.function_name.is_empty())
            stack_frame.function = frame.function_name.to_utf8();

        if (!source_range.filename().is_empty() || source_range.start.line != 0 || source_range.start.column != 0) {
            stack_frame.file = String::from_utf8_with_replacement_character(source_range.filename());
            stack_frame.line = source_range.start.line;
            stack_frame.column = source_range.start.column;
        }

        if (stack_frame.function.has_value() || stack_frame.file.has_value())
            trace.unchecked_append(move(stack_frame));
    }

    send_console_output({
        .timestamp = UnixDateTime::now(),
        .output = WebView::ConsoleError {
            .name = name,
            .message = message,
            .trace = move(trace),
            .inside_promise = in_promise,
        },
    });
}

void DevToolsConsoleClient::send_console_output(WebView::ConsoleOutput console_output)
{
    m_client->did_output_js_console_message(move(console_output));
}

// 2.3. Printer(logLevel, args[, options]), https://console.spec.whatwg.org/#printer
JS::ThrowCompletionOr<JS::Value> DevToolsConsoleClient::printer(JS::Console::LogLevel log_level, PrinterArguments arguments)
{
    if (log_level == JS::Console::LogLevel::Trace) {
        auto const& trace = arguments.get<JS::Console::Trace>();

        m_console->output_debug_message(log_level, trace.label);

        Vector<WebView::StackFrame> stack_frames;
        stack_frames.ensure_capacity(trace.stack.size());

        for (auto const& frame : trace.stack) {
            stack_frames.unchecked_append(WebView::StackFrame {
                .function = frame.function_name,
                .file = frame.source_file,
                .line = frame.line,
                .column = frame.column,
            });
        }

        send_console_output({
            .timestamp = UnixDateTime::now(),
            .output = WebView::ConsoleTrace {
                .label = trace.label,
                .stack = move(stack_frames),
            },
        });

        return JS::js_undefined();
    }

    // FIXME: Implement these.
    if (first_is_one_of(log_level, JS::Console::LogLevel::Table, JS::Console::LogLevel::Group, JS::Console::LogLevel::GroupCollapsed))
        return JS::js_undefined();

    auto const& argument_values = arguments.get<GC::RootVector<JS::Value>>();

    auto output = TRY(generically_format_values(argument_values));
    m_console->output_debug_message(log_level, output);

    Vector<JsonValue> serialized_arguments;
    serialized_arguments.ensure_capacity(argument_values.size());

    for (auto value : argument_values)
        serialized_arguments.unchecked_append(serialize_js_value(m_console->realm(), value));

    send_console_output({
        .timestamp = UnixDateTime::now(),
        .output = WebView::ConsoleLog {
            .level = log_level,
            .arguments = move(serialized_arguments),
        },
    });

    return JS::js_undefined();
}

}
