/*
 * Copyright (c) 2020-2024, Andreas Kling <andreas@ladybird.org>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <AK/LexicalPath.h>
#include <LibCore/ArgsParser.h>
#include <LibCore/EventLoop.h>
#include <LibCore/LocalServer.h>
#include <LibCore/Process.h>
#include <LibCore/Resource.h>
#include <LibCore/System.h>
#include <LibCrypto/OpenSSLForward.h>
#include <LibGfx/Font/FontDatabase.h>
#include <LibGfx/Font/PathFontProvider.h>
#include <LibGfx/SkiaBackendContext.h>
#include <LibIPC/ConnectionFromClient.h>
#include <LibIPC/TransportHandle.h>
#include <LibMain/Main.h>
#include <LibRequests/RequestClient.h>
#include <LibUnicode/TimeZone.h>
#include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/Fetch/Fetching/Fetching.h>
#include <LibWeb/HTML/UniversalGlobalScope.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/Internals/Internals.h>
#include <LibWeb/Loader/ContentFilter.h>
#include <LibWeb/Loader/GeneratedPagesLoader.h>
#include <LibWeb/Loader/ResourceLoader.h>
#include <LibWeb/Painting/BackingStoreManager.h>
#include <LibWeb/Painting/PaintableBox.h>
#include <LibWeb/Platform/EventLoopPlugin.h>
#include <LibWeb/Platform/FontPlugin.h>
#include <LibWeb/WebIDL/Tracing.h>
#include <LibWebView/Plugins/ImageCodecPlugin.h>
#include <LibWebView/SiteIsolation.h>
#include <LibWebView/Utilities.h>
#include <WebContent/ConnectionFromClient.h>
#include <WebContent/PageClient.h>
#include <WebContent/WebDriverConnection.h>

#include <openssl/thread.h>

#if defined(AK_OS_MACOS)
#    include <LibIPC/Transport.h>
#    include <LibIPC/TransportBootstrapMach.h>
#else
#    include <LibIPC/SingleServer.h>
#endif

#if defined(AK_OS_WINDOWS)
#    include <objbase.h>
#endif

#include <SDL3/SDL_init.h>

#if !defined(AK_OS_WINDOWS)
#    include <signal.h>
static void crash_signal_handler(int signo)
{
    char const* name;
    switch (signo) {
    case SIGSEGV:
        name = "SIGSEGV";
        break;
    case SIGBUS:
        name = "SIGBUS";
        break;
    case SIGFPE:
        name = "SIGFPE";
        break;
    case SIGABRT:
        name = "SIGABRT";
        break;
    case SIGILL:
        name = "SIGILL";
        break;
    default:
        name = "unknown";
        break;
    }
    warnln("\n\033[31;1mCRASH\033[0m: Received signal {} ({})", name, signo);
    dump_backtrace(2, 100);
    exit(128 + signo);
}

static void install_crash_signal_handlers()
{
    struct sigaction sa;
    sa.sa_handler = crash_signal_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESETHAND;
    sigaction(SIGSEGV, &sa, nullptr);
    sigaction(SIGBUS, &sa, nullptr);
    sigaction(SIGFPE, &sa, nullptr);
    sigaction(SIGABRT, &sa, nullptr);
    sigaction(SIGILL, &sa, nullptr);
}
#endif

static ErrorOr<void> load_content_filters(StringView config_path);

static ErrorOr<void> connect_to_resource_loader(GC::Heap& heap, IPC::TransportHandle const& handle);
static ErrorOr<void> connect_to_image_decoder(IPC::TransportHandle const& handle);

ErrorOr<int> ladybird_main(Main::Arguments arguments)
{
    AK::set_rich_debug_enabled(true);

#if !defined(AK_OS_WINDOWS)
    install_crash_signal_handlers();
#endif

#if defined(AK_OS_WINDOWS)
    // NOTE: We need this here otherwise SDL inits COM in the APARTMENTTHREADED model which we don't want as we need to
    // make calls across threads which would otherwise have a high overhead. It is safe for all the objects we use.
    HRESULT hr = CoInitializeEx(0, COINIT_MULTITHREADED);
    VERIFY(SUCCEEDED(hr));
    ScopeGuard uninitialize_com = []() { CoUninitialize(); };
#endif
    // SDL is used for the Gamepad API.
    if (!SDL_Init(SDL_INIT_GAMEPAD)) {
        dbgln("Failed to initialize SDL3: {}", SDL_GetError());
        return -1;
    }

    Core::EventLoop event_loop;

    WebView::platform_init();

    Web::Platform::EventLoopPlugin::install(*new Web::Platform::EventLoopPlugin);

    auto config_path = ByteString::formatted("{}/ladybird/default-config", WebView::s_ladybird_resource_root);
    StringView mach_server_name {};
    Vector<ByteString> certificates;
    bool enable_test_mode = false;
    bool expose_experimental_interfaces = false;
    bool expose_internals_object = false;
    bool wait_for_debugger = false;
    bool log_all_js_exceptions = false;
    bool disable_site_isolation = false;
    bool enable_idl_tracing = false;
    bool enable_http_memory_cache = false;
    bool force_cpu_painting = false;
    bool force_fontconfig = false;
    bool collect_garbage_on_every_allocation = false;
    bool is_headless = false;
    bool disable_scrollbar_painting = false;
    bool disable_async_scrolling = false;
    StringView echo_server_port_string_view {};
    StringView default_time_zone {};
    StringView style_invalidation_counter_dump_interval {};
    bool file_origins_are_tuple_origins = false;

    Core::ArgsParser args_parser;
    args_parser.add_option(config_path, "Ladybird configuration path", "config-path", 0, "config_path");
    args_parser.add_option(enable_test_mode, "Enable test mode", "test-mode");
    args_parser.add_option(expose_experimental_interfaces, "Expose experimental IDL interfaces", "expose-experimental-interfaces");
    args_parser.add_option(expose_internals_object, "Expose internals object", "expose-internals-object");
    args_parser.add_option(certificates, "Path to a certificate file", "certificate", 'C', "certificate");
    args_parser.add_option(wait_for_debugger, "Wait for debugger", "wait-for-debugger");
    args_parser.add_option(mach_server_name, "Mach server name", "mach-server-name", 0, "mach_server_name");
    args_parser.add_option(log_all_js_exceptions, "Log all JavaScript exceptions", "log-all-js-exceptions");
    args_parser.add_option(disable_site_isolation, "Disable site isolation", "disable-site-isolation");
    args_parser.add_option(enable_idl_tracing, "Enable IDL tracing", "enable-idl-tracing");
    args_parser.add_option(enable_http_memory_cache, "Enable HTTP cache", "enable-http-memory-cache");
    args_parser.add_option(force_cpu_painting, "Force CPU painting", "force-cpu-painting");
    args_parser.add_option(force_fontconfig, "Force using fontconfig for font loading", "force-fontconfig");
    args_parser.add_option(collect_garbage_on_every_allocation, "Collect garbage after every JS heap allocation", "collect-garbage-on-every-allocation");
    args_parser.add_option(disable_scrollbar_painting, "Don't paint horizontal or vertical viewport scrollbars", "disable-scrollbar-painting");
    args_parser.add_option(disable_async_scrolling, "Disable async scrolling", "disable-async-scrolling");
    args_parser.add_option(echo_server_port_string_view, "Echo server port used in test internals", "echo-server-port", 0, "echo_server_port");
    args_parser.add_option(is_headless, "Report that the browser is running in headless mode", "headless");
    args_parser.add_option(default_time_zone, "Default time zone", "default-time-zone", 0, "time-zone-id");
    args_parser.add_option(style_invalidation_counter_dump_interval, "Dump style invalidation counters after every N style invalidations", "dump-style-invalidation-counters", 0, "N");
    args_parser.add_option(file_origins_are_tuple_origins, "Treat file:// URLs as having tuple origins", "tuple-file-origins");

    args_parser.parse(arguments);

    if (wait_for_debugger) {
        Core::Process::wait_for_debugger_and_break();
    }

    if (!default_time_zone.is_empty()) {
        if (auto result = Unicode::set_current_time_zone(default_time_zone); result.is_error())
            dbgln("Failed to set default time zone: {}", result.error());
    }

    if (!style_invalidation_counter_dump_interval.is_empty()) {
        auto interval = style_invalidation_counter_dump_interval.to_number<u64>();
        if (!interval.has_value() || *interval == 0)
            VERIFY_NOT_REACHED();
        Web::DOM::Document::set_style_invalidation_counter_dump_interval(*interval);
    }

    if (file_origins_are_tuple_origins)
        URL::set_file_scheme_urls_have_tuple_origins();

    auto& font_provider = static_cast<Gfx::PathFontProvider&>(Gfx::FontDatabase::the().install_system_font_provider(make<Gfx::PathFontProvider>()));
    if (force_fontconfig) {
        font_provider.set_name_but_fixme_should_create_custom_system_font_provider("FontConfig"_string);
    }
    font_provider.load_all_fonts_from_uri("resource://fonts"sv);

    // Always use the CPU backend for tests, as the GPU backend is not deterministic
    if (force_cpu_painting) {
        WebContent::PageClient::set_use_skia_painter(WebContent::PageClient::UseSkiaPainter::CPUBackend);
    } else {
        Gfx::SkiaBackendContext::initialize_gpu_backend();
        WebContent::PageClient::set_use_skia_painter(WebContent::PageClient::UseSkiaPainter::GPUBackendIfAvailable);
    }

    WebContent::PageClient::set_is_headless(is_headless);

    if (disable_site_isolation)
        WebView::disable_site_isolation();

    if (enable_http_memory_cache)
        Web::Fetch::Fetching::set_http_memory_cache_enabled(true);

    Web::Painting::set_paint_viewport_scrollbars(!disable_scrollbar_painting);
    WebContent::PageClient::set_async_scrolling_enabled(!disable_async_scrolling);

    if (!echo_server_port_string_view.is_empty()) {
        if (auto maybe_echo_server_port = echo_server_port_string_view.to_number<u16>(); maybe_echo_server_port.has_value())
            Web::Internals::Internals::set_echo_server_port(maybe_echo_server_port.value());
        else
            VERIFY_NOT_REACHED();
    }

    OPENSSL_TRY(OSSL_set_max_threads(nullptr, Core::System::hardware_concurrency()));

    Web::HTML::Window::set_enable_test_mode(enable_test_mode);
    Web::HTML::Window::set_internals_object_exposed(expose_internals_object);
    Web::HTML::UniversalGlobalScopeMixin::set_experimental_interfaces_exposed(expose_experimental_interfaces);

    Web::Platform::FontPlugin::install(*new Web::Platform::FontPlugin(enable_test_mode, &font_provider));

    Web::Bindings::initialize_main_thread_vm(Web::Bindings::AgentType::SimilarOriginWindow);

    if (collect_garbage_on_every_allocation)
        Web::Bindings::main_thread_vm().heap().set_should_collect_on_every_allocation(true);

    if (log_all_js_exceptions) {
        JS::set_log_all_js_exceptions(true);
    }

    if (enable_idl_tracing) {
        Web::WebIDL::set_enable_idl_tracing(true);
    }

    auto maybe_content_filter_error = load_content_filters(config_path);
    if (maybe_content_filter_error.is_error())
        dbgln("Failed to load content filters: {}", maybe_content_filter_error.error());

#if defined(AK_OS_MACOS)
    auto browser_port = TRY(Core::MachPort::look_up_from_bootstrap_server(ByteString { mach_server_name }));
    auto transport_ports = TRY(IPC::bootstrap_transport_from_server_port(browser_port));
    auto webcontent_client = WebContent::ConnectionFromClient::construct(
        make<IPC::Transport>(move(transport_ports.receive_right), move(transport_ports.send_right)));
#else
    auto webcontent_client = TRY(IPC::take_over_accepted_client_from_system_server<WebContent::ConnectionFromClient>(mach_server_name));
#endif

    auto& heap = Web::Bindings::main_thread_vm().heap();
    webcontent_client->on_request_server_connection = [&heap](auto const& handle) {
        if (auto result = connect_to_resource_loader(heap, handle); result.is_error())
            dbgln("Failed to connect to resource loader: {}", result.error());
    };
    webcontent_client->on_image_decoder_connection = [](auto const& handle) {
        if (auto result = connect_to_image_decoder(handle); result.is_error())
            dbgln("Failed to connect to image decoder: {}", result.error());
    };

    return event_loop.exec();
}

static ErrorOr<void> load_content_filters(StringView config_path)
{
    auto buffer = TRY(ByteBuffer::create_uninitialized(4096));

    auto file = TRY(Core::File::open(ByteString::formatted("{}/BrowserContentFilters.txt", config_path), Core::File::OpenMode::Read));
    auto ad_filter_list = TRY(Core::InputBufferedFile::create(move(file)));

    Vector<String> patterns;

    while (TRY(ad_filter_list->can_read_line())) {
        auto line = TRY(ad_filter_list->read_line(buffer));
        if (line.is_empty())
            continue;

        auto pattern = TRY(String::from_utf8(line));
        TRY(patterns.try_append(move(pattern)));
    }

    auto& content_filter = Web::ContentFilter::the();
    TRY(content_filter.set_patterns(patterns));

    return {};
}

ErrorOr<void> connect_to_resource_loader(GC::Heap& heap, IPC::TransportHandle const& handle)
{
    auto transport = TRY(handle.create_transport());
    auto request_client = TRY(try_make_ref_counted<Requests::RequestClient>(move(transport)));
#ifdef AK_OS_WINDOWS
    auto response = request_client->send_sync<Messages::RequestServer::InitTransport>(Core::System::getpid());
    request_client->transport().set_peer_pid(response->peer_pid());
#endif
    if (Web::ResourceLoader::is_initialized())
        Web::ResourceLoader::the().set_client(move(request_client));
    else
        Web::ResourceLoader::initialize(heap, move(request_client));
    return {};
}

ErrorOr<void> connect_to_image_decoder(IPC::TransportHandle const& handle)
{
    auto transport = TRY(handle.create_transport());
    auto new_client = TRY(try_make_ref_counted<ImageDecoderClient::Client>(move(transport)));
#ifdef AK_OS_WINDOWS
    auto response = new_client->send_sync<Messages::ImageDecoderServer::InitTransport>(Core::System::getpid());
    new_client->transport().set_peer_pid(response->peer_pid());
#endif
    if (Web::Platform::ImageCodecPlugin::is_initialized())
        static_cast<WebView::ImageCodecPlugin&>(Web::Platform::ImageCodecPlugin::the()).set_client(move(new_client));
    else
        Web::Platform::ImageCodecPlugin::install(*new WebView::ImageCodecPlugin(move(new_client)));
    return {};
}
