/*
 * Copyright (c) 2023, Linus Groh <linusg@serenityos.org>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <AK/Debug.h>
#include <AK/Error.h>
#include <AK/NeverDestroyed.h>
#include <AK/NumericLimits.h>
#include <AK/ScopeGuard.h>
#include <AK/String.h>
#include <AK/Time.h>
#include <LibCore/EventLoop.h>
#include <LibCore/StandardPaths.h>
#include <LibCore/Timer.h>
#include <LibGfx/ImageFormats/PNGWriter.h>
#include <LibGfx/SharedImageBuffer.h>
#include <LibURL/Parser.h>
#include <LibWeb/CSS/SystemColor.h>
#include <LibWeb/Crypto/Crypto.h>
#include <LibWeb/Infra/Strings.h>
#include <LibWeb/WebDriver/Error.h>
#include <LibWebView/Application.h>
#include <LibWebView/BookmarkStore.h>
#include <LibWebView/ErrorHTML.h>
#include <LibWebView/HelperProcess.h>
#include <LibWebView/HistoryDebug.h>
#include <LibWebView/HistoryStore.h>
#include <LibWebView/Menu.h>
#include <LibWebView/SiteIsolation.h>
#include <LibWebView/URL.h>
#include <LibWebView/UserAgent.h>
#include <LibWebView/ViewImplementation.h>

namespace WebView {

static HashMap<u64, ViewImplementation*>& all_views()
{
    static NeverDestroyed<HashMap<u64, ViewImplementation*>> views;
    return *views;
}
static u64 s_view_count = 1; // This has to start at 1 for Firefox DevTools.

void ViewImplementation::for_each_view(Function<IterationDecision(ViewImplementation&)> callback)
{
    for (auto& view : all_views()) {
        if (callback(*view.value) == IterationDecision::Break)
            break;
    }
}

Optional<ViewImplementation&> ViewImplementation::find_view_by_id(u64 id)
{
    if (auto view = all_views().get(id); view.has_value())
        return *view.value();
    return {};
}

ViewImplementation::ViewImplementation()
    : m_document_cookie_version_buffer(Core::create_shared_version_buffer())
    , m_view_id(s_view_count++)
{
    all_views().set(m_view_id, this);

    initialize_context_menus();

    m_repeated_crash_timer = Core::Timer::create_single_shot(1000, [this] {
        // Reset the "crashing a lot" counter after 1 second in case we just
        // happen to be visiting crashy websites a lot.
        this->m_crash_count = 0;
    });

    on_request_file = [this](auto const& path, auto request_id) {
        auto file = Core::File::open(path, Core::File::OpenMode::Read);

        if (file.is_error())
            client().async_handle_file_return(page_id(), file.error().code(), {}, request_id);
        else
            client().async_handle_file_return(page_id(), 0, IPC::File::adopt_file(file.release_value()), request_id);
    };
}

ViewImplementation::~ViewImplementation()
{
    all_views().remove(m_view_id);

    if (m_client_state.client)
        m_client_state.client->unregister_view(m_client_state.page_index);
}

WebContentClient& ViewImplementation::client()
{
    VERIFY(m_client_state.client);
    return *m_client_state.client;
}

WebContentClient const& ViewImplementation::client() const
{
    VERIFY(m_client_state.client);
    return *m_client_state.client;
}

u64 ViewImplementation::page_id() const
{
    VERIFY(m_client_state.client);
    return m_client_state.page_index;
}

void ViewImplementation::set_url(URL::URL url)
{
    if (m_url == url)
        return;

    auto previous_host = current_host();
    m_url = move(url);
    update_bookmark_action();

    if (current_host() != previous_host)
        apply_zoom_for_current_host();
}

void ViewImplementation::set_favicon(Badge<WebContentClient>, Gfx::Bitmap const& favicon)
{
    m_favicon_base64_png.clear();

    if (auto favicon_png = Gfx::PNGWriter::encode(favicon); !favicon_png.is_error()) {
        if (auto favicon_base64_png = encode_base64(favicon_png.value().bytes()); !favicon_base64_png.is_error())
            m_favicon_base64_png = favicon_base64_png.release_value();
    }

    if (m_favicon_base64_png.has_value()) {
        Application::bookmark_store().update_favicon(m_url, *m_favicon_base64_png);
        if (!m_should_suppress_history_for_current_load)
            Application::history_store().update_favicon(m_url, *m_favicon_base64_png);
    }

    if (on_favicon_change)
        on_favicon_change(favicon);
}

void ViewImplementation::create_new_process_for_cross_site_navigation(URL::URL const& url, Variant<Empty, String, Web::HTML::POSTResource> document_resource, Web::Bindings::NavigationHistoryBehavior history_handling)
{
    dump_session_history("before-process-swap"sv);
    m_webdriver_pending_navigation_url = url;

    if (m_client_state.has_usable_bitmap) {
        // Keep showing the old page until the new WebContent process paints its first frame.
        m_backup_shared_image_buffer = move(m_client_state.front_bitmap.shared_image_buffer);
        m_backup_bitmap_size = m_client_state.front_bitmap.last_painted_size;
    }

    if (m_client_state.client) {
        m_client_state.client->unregister_view(m_client_state.page_index);
    }

    initialize_client();
    VERIFY(m_client_state.client);

    if (on_web_content_process_change_for_cross_site_navigation)
        on_web_content_process_change_for_cross_site_navigation();

    handle_resize();

    auto ui_session_history_already_points_to_url = false;
    if (should_manage_session_history_in_ui_process()) {
        if (auto const* current_entry = m_session_history.current_entry(); current_entry && current_entry->url == url)
            ui_session_history_already_points_to_url = true;

        if (m_pending_session_history_traversal.has_value() && m_pending_session_history_traversal->will_replace_web_content_process)
            m_pending_session_history_traversal->stage = PendingSessionHistoryTraversal::Stage::ReplacingWebContentProcess;
        if (m_pending_session_history_navigation.has_value())
            m_pending_session_history_navigation->web_content_restore_mode = PendingSessionHistoryNavigation::WebContentRestoreMode::RestoreFromUIProcess;
        m_current_web_content_session_history_matches_mirror = false;
        m_session_history.forget_web_content_state();
        m_pending_web_content_session_history_seed.waiting_for_ack = false;
        m_pending_web_content_session_history_seed.should_send_entries = true;
        m_pending_web_content_session_history_seed.ignore_updates_until_seed = true;
    }

    auto seed_replacement_process_before_load_if_possible = [&] {
        if (!should_manage_session_history_in_ui_process())
            return false;
        if (!m_pending_web_content_session_history_seed.should_send_entries)
            return false;
        if (m_session_history_entry_url_loading_from_ui_process.has_value())
            return false;
        if (m_session_history.current_step_to_restore_after_loading_top_level_entry().has_value())
            return false;
        seed_web_content_session_history_from_ui_process();
        return true;
    };

    if (ui_session_history_already_points_to_url) {
        m_should_suppress_history_for_current_load = false;
        m_should_suppress_history_for_next_load = false;
        if (!m_session_history_entry_url_loading_from_ui_process.has_value())
            m_pending_web_content_session_history_seed.step_after_loading_top_level_entry = m_session_history.current_step_to_restore_after_loading_top_level_entry();
        set_url(url);
        auto seeded_replacement_process_before_load = seed_replacement_process_before_load_if_possible();
        auto web_content_history_handling = seeded_replacement_process_before_load ? Web::Bindings::NavigationHistoryBehavior::Replace : history_handling;
        dump_session_history("process-swap-load-existing-ui-entry"sv);
        client().async_load_url_with_document_resource(page_id(), url, document_resource, web_content_history_handling);
    } else {
        m_should_suppress_history_for_current_load = false;
        m_should_suppress_history_for_next_load = false;
        if (should_manage_session_history_in_ui_process() && !m_session_history_entry_url_loading_from_ui_process.has_value()) {
            if (m_session_history.current_entry()) {
                m_pending_session_history_navigation = PendingSessionHistoryNavigation {
                    url,
                    m_session_history,
                    PendingSessionHistoryNavigation::WebContentRestoreMode::RestoreFromUIProcess,
                };
            } else {
                m_pending_session_history_navigation.clear();
            }
            if (history_handling == Web::Bindings::NavigationHistoryBehavior::Replace)
                m_session_history.replace_current_entry(url, document_resource);
            else
                m_session_history.navigate(url, document_resource);
            m_current_web_content_session_history_matches_mirror = false;
            update_navigation_action_state();
        }
        if (!m_session_history_entry_url_loading_from_ui_process.has_value())
            m_pending_web_content_session_history_seed.step_after_loading_top_level_entry = m_session_history.current_step_to_restore_after_loading_top_level_entry();
        set_url(url);
        auto seeded_replacement_process_before_load = seed_replacement_process_before_load_if_possible();
        auto web_content_history_handling = seeded_replacement_process_before_load ? Web::Bindings::NavigationHistoryBehavior::Replace : history_handling;
        dump_session_history("load"sv);
        client().async_load_url_with_document_resource(page_id(), url, document_resource, web_content_history_handling);
    }
    dump_session_history("after-process-swap-load"sv);
}

void ViewImplementation::server_did_paint(Badge<WebContentClient>, i32 bitmap_id, Gfx::IntSize size)
{
    bool did_swap_bitmap = false;
    if (m_client_state.back_bitmap.id == bitmap_id) {
        m_client_state.has_usable_bitmap = true;
        m_client_state.back_bitmap.last_painted_size = size.to_type<Web::DevicePixels>();
        swap(m_client_state.back_bitmap, m_client_state.front_bitmap);
        m_backup_shared_image_buffer = nullptr;
        did_swap_bitmap = true;
    }

    dbgln_if(COMPOSITOR_DEBUG, "[Compositor] UI received presented bitmap {} for page {} size={}x{} did_swap={} front={} back={}",
        bitmap_id, page_id(), size.width(), size.height(), did_swap_bitmap, m_client_state.front_bitmap.id, m_client_state.back_bitmap.id);

    client().notify_presented_bitmap_ready_to_paint(page_id(), bitmap_id);

    if (did_swap_bitmap && on_ready_to_paint)
        on_ready_to_paint();
}

void ViewImplementation::set_window_position(Gfx::IntPoint position)
{
    client().async_set_window_position(m_client_state.page_index, position.to_type<Web::DevicePixels>());
}

void ViewImplementation::set_window_size(Gfx::IntSize size)
{
    client().async_set_window_size(m_client_state.page_index, size.to_type<Web::DevicePixels>());
}

void ViewImplementation::did_update_window_rect()
{
    client().async_did_update_window_rect(m_client_state.page_index);
}

void ViewImplementation::set_system_visibility_state(Web::HTML::VisibilityState visibility_state)
{
    if (m_system_visibility_state == visibility_state)
        return;

    m_system_visibility_state = visibility_state;
    client().async_set_system_visibility_state(m_client_state.page_index, m_system_visibility_state);
}

void ViewImplementation::load(URL::URL const& url, Web::Bindings::NavigationHistoryBehavior history_handling)
{
    m_is_showing_crash_page = false;
    m_should_suppress_history_for_current_load = false;
    m_should_suppress_history_for_next_load = false;
    auto should_defer_ui_process_history_update = false;
    if (!m_session_history_entry_url_loading_from_ui_process.has_value())
        abandon_pending_web_content_session_history_seed();
    if (should_manage_session_history_in_ui_process() && !m_session_history_entry_url_loading_from_ui_process.has_value()) {
        m_pending_session_history_traversal.clear();
        auto const* current_entry = m_session_history.current_entry();
        auto is_javascript_navigation = url.scheme() == "javascript"sv;
        should_defer_ui_process_history_update = is_javascript_navigation;
        if (current_entry && !is_javascript_navigation)
            m_pending_session_history_navigation = PendingSessionHistoryNavigation { url, m_session_history };
        else
            m_pending_session_history_navigation.clear();

        if (!is_javascript_navigation) {
            auto ui_process_history_handling = history_handling;
            if (ui_process_history_handling == Web::Bindings::NavigationHistoryBehavior::Auto) {
                // https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigate
                // If url equals navigable's active document's URL, and
                // initiatorOriginSnapshot is same origin with targetNavigable's
                // active document's origin, then set historyHandling to "replace".
                if (current_entry && current_entry->url == url)
                    ui_process_history_handling = Web::Bindings::NavigationHistoryBehavior::Replace;
                else
                    ui_process_history_handling = Web::Bindings::NavigationHistoryBehavior::Push;
            }

            if (ui_process_history_handling == Web::Bindings::NavigationHistoryBehavior::Replace)
                m_session_history.replace_current_entry(url, Empty {});
            else
                m_session_history.navigate(url);
            m_current_web_content_session_history_matches_mirror = false;
            update_navigation_action_state();
        }
    }
    if (!should_defer_ui_process_history_update)
        set_url(url);
    dump_session_history("load"sv);
    client().async_load_url(page_id(), url, history_handling);
}

void ViewImplementation::load_html(StringView html)
{
    m_is_showing_crash_page = false;
    m_should_suppress_history_for_current_load = false;
    m_should_suppress_history_for_next_load = false;
    abandon_pending_web_content_session_history_seed();
    if (should_manage_session_history_in_ui_process()) {
        m_current_web_content_session_history_matches_mirror = false;
        m_session_history.forget_web_content_state();
    }
    client().async_load_html(page_id(), html);
}

void ViewImplementation::load_crash_page_html(StringView html, URL::URL const& crashed_url)
{
    m_is_showing_crash_page = true;
    m_should_suppress_history_for_current_load = true;
    m_should_suppress_history_for_next_load = true;
    abandon_pending_web_content_session_history_seed();
    if (should_manage_session_history_in_ui_process()) {
        m_current_web_content_session_history_matches_mirror = false;
        m_session_history.forget_web_content_state();
    }
    set_url(crashed_url);
    client().async_load_html_with_url(page_id(), html, crashed_url);
}

void ViewImplementation::load_navigation_error_page(StringView text)
{
    auto message = MUST(String::formatted("Failed to load \"{}\"", text));

    StringBuilder builder;
    builder.appendff(ERROR_HTML_HEADER, ""sv, ERROR_SVG, message);
    builder.append("<p>If you were trying to enter a search query, please enable search in <a href=\"about:settings#search\">settings</a>.</p>"sv);
    builder.append(ERROR_HTML_FOOTER);
    load_html(builder.string_view());
}

void ViewImplementation::reload()
{
    if (should_manage_session_history_in_ui_process() && m_is_showing_crash_page) {
        m_is_showing_crash_page = false;
        m_should_suppress_history_for_current_load = false;
        m_should_suppress_history_for_next_load = false;
        prepare_to_seed_web_content_session_history_from_ui_process();
        restore_current_session_history_entry_from_ui_process();
        return;
    }

    m_is_showing_crash_page = false;
    m_should_suppress_history_for_current_load = false;
    m_should_suppress_history_for_next_load = false;
    abandon_pending_web_content_session_history_seed();
    if (should_manage_session_history_in_ui_process()) {
        m_session_history.mark_current_entry_reload_pending();
        m_current_web_content_session_history_matches_mirror = false;
        update_navigation_action_state();
        dump_session_history("reload-mark-current-entry-reload-pending"sv);
    }
    client().async_reload(page_id());
}

ViewImplementation::HistoryTraversalOutcome ViewImplementation::traverse_the_history_by_delta(
    int delta,
    CheckForCancelation check_for_cancelation,
    Function<void(HistoryTraversalOutcome)> on_cancelation_check_complete)
{
    if (!should_manage_session_history_in_ui_process()) {
        m_should_suppress_history_for_current_load = false;
        m_should_suppress_history_for_next_load = false;
        m_webdriver_pending_navigation_completes_with_session_history_update = false;
        client().async_traverse_the_history_by_delta(page_id(), delta);
        return {
            .status = HistoryTraversalStatus::Started,
            .will_change_top_level_entry = true,
        };
    }

    // https://html.spec.whatwg.org/multipage/browsing-the-web.html#traverse-the-history-by-a-delta
    // Let allSteps be the result of getting all used history steps for traversable.
    // Let currentStepIndex be the index of traversable's current session history step within allSteps.
    // Let targetStepIndex be currentStepIndex plus delta.
    //
    // AD-HOC: Browser UI calls do not pass sourceDocument. Since the UI process owns the top-level session history
    //         mirror, it resolves targetStepIndex before asking WebContent to apply or precheck the resulting
    //         traverse history step.
    auto target = m_session_history.traversal_target_for_delta(delta);
    if (!target.has_value()) {
        dump_session_history("traverse-no-entry"sv);
        return { .status = HistoryTraversalStatus::NoEntry };
    }

    m_should_suppress_history_for_current_load = false;
    m_should_suppress_history_for_next_load = false;

    auto will_replace_web_content_process = !is_url_suitable_for_same_process_navigation(m_url, target->target_top_level_entry->url);
    auto pending_traversal = PendingSessionHistoryTraversal {
        .target_step = target->target_step,
        .target_step_index = target->target_step_index,
        .will_change_top_level_entry = target->changes_top_level_entry,
        .will_replace_web_content_process = will_replace_web_content_process,
        .on_cancelation_check_complete = nullptr,
    };

    auto web_content_can_apply_traversal = !m_pending_web_content_session_history_seed.should_send_entries
        && !m_pending_web_content_session_history_seed.ignore_updates_until_seed
        && !m_pending_web_content_session_history_seed.waiting_for_ack
        && !m_session_history_entry_url_loading_from_ui_process.has_value()
        && !m_pending_web_content_session_history_seed.step_after_loading_top_level_entry.has_value()
        && m_session_history.web_content_can_traverse_to(*target);

    // If WebContent already has enough state to apply the traverse history step,
    // let it run the spec algorithm directly.
    if (web_content_can_apply_traversal && !will_replace_web_content_process) {
        m_pending_session_history_traversal = move(pending_traversal);
        m_webdriver_pending_navigation_url = target->target_top_level_entry->url;
        if (auto const* current_entry = m_session_history.current_entry()) {
            m_webdriver_pending_navigation_completes_with_session_history_update = current_entry->document_state.id != 0
                && current_entry->document_state.id == target->target_top_level_entry->document_state.id;
        } else {
            m_webdriver_pending_navigation_completes_with_session_history_update = false;
        }
        dump_session_history("traverse-delegate-to-webcontent"sv);
        client().async_traverse_the_history_to_step(page_id(), target->target_step);
        return {
            .status = HistoryTraversalStatus::Started,
            .will_replace_web_content_process = will_replace_web_content_process,
            .will_change_top_level_entry = target->changes_top_level_entry,
        };
    }

    // https://html.spec.whatwg.org/multipage/browsing-the-web.html#apply-the-history-step
    // If checkForCancelation is true, and the result of checking if unloading is canceled given
    // navigablesCrossingDocuments, traversable, targetStep, and userInvolvement is not "continue", then return that
    // result.
    //
    // AD-HOC: If WebContent cannot apply the step itself, the UI still asks it to run that precheck before moving
    //         the UI-owned history mirror and loading or reseeding WebContent from that state. A renderer-initiated
    //         traversal can report that this already happened, but only trust that precheck if WebContent can
    //         traverse to the same target step in the UI-owned history mirror.
    auto needs_cancelation_check = check_for_cancelation == CheckForCancelation::Yes
        || (check_for_cancelation == CheckForCancelation::IfWebContentCannotTraverseTarget && !web_content_can_apply_traversal);
    if (needs_cancelation_check) {
        pending_traversal.stage = PendingSessionHistoryTraversal::Stage::CheckingCancelation;
        pending_traversal.cancelation_check_request_id = m_next_traverse_history_step_cancelation_check_request_id++;
        pending_traversal.on_cancelation_check_complete = move(on_cancelation_check_complete);
        auto request_id = pending_traversal.cancelation_check_request_id;
        m_pending_session_history_traversal = move(pending_traversal);
        client().async_check_if_traverse_history_step_is_canceled(page_id(), request_id, target->target_step);
        dump_session_history("traverse-fallback-check-cancelation"sv);
        return {
            .status = HistoryTraversalStatus::Started,
            .will_replace_web_content_process = will_replace_web_content_process,
            .will_change_top_level_entry = target->changes_top_level_entry,
            .waiting_for_cancelation_check = true,
        };
    }

    pending_traversal.stage = PendingSessionHistoryTraversal::Stage::LoadingEntryFromUIProcess;
    m_pending_session_history_traversal = move(pending_traversal);
    load_session_history_traversal_target_from_ui_process(*target, "traverse-fallback-load"sv);
    return {
        .status = HistoryTraversalStatus::Started,
        .will_replace_web_content_process = will_replace_web_content_process,
        .will_change_top_level_entry = target->changes_top_level_entry,
    };
}

Vector<ViewImplementation::SessionHistoryTraversalMenuItem> ViewImplementation::session_history_traversal_menu_items(int direction) const
{
    VERIFY(direction == -1 || direction == 1);

    auto current_used_step_index = m_session_history.current_used_step_index();
    if (!current_used_step_index.has_value())
        return {};

    Vector<SessionHistoryTraversalMenuItem> items;
    auto append_item = [&](size_t target_step_index, TraversableSessionHistory::Entry const& target_entry) {
        auto history_entry = Application::history_store().entry_for_url(target_entry.url);
        auto url = target_entry.url.serialize();
        auto title = history_entry.has_value() && history_entry->title.has_value() && !history_entry->title->is_empty()
            ? move(*history_entry->title)
            : url;
        items.append({
            static_cast<int>(target_step_index) - static_cast<int>(*current_used_step_index),
            move(title),
            move(url),
            history_entry.has_value() ? move(history_entry->favicon_base64_png) : Optional<String> {},
        });
    };

    if (direction < 0) {
        for (size_t target_step_index = *current_used_step_index; target_step_index > 0; --target_step_index) {
            auto target_step = m_session_history.step_at(target_step_index - 1);
            if (!target_step.has_value())
                continue;
            auto const* target_entry = m_session_history.top_level_entry_for_step(*target_step);
            if (!target_entry)
                continue;
            append_item(target_step_index - 1, *target_entry);
        }
    } else {
        for (size_t target_step_index = *current_used_step_index + 1; target_step_index < m_session_history.used_step_count(); ++target_step_index) {
            auto target_step = m_session_history.step_at(target_step_index);
            if (!target_step.has_value())
                continue;
            auto const* target_entry = m_session_history.top_level_entry_for_step(*target_step);
            if (!target_entry)
                continue;
            append_item(target_step_index, *target_entry);
        }
    }

    return items;
}

void ViewImplementation::zoom_in()
{
    if (m_zoom_level >= ZOOM_MAX_LEVEL)
        return;
    m_zoom_level = round_to<int>((m_zoom_level + ZOOM_STEP) * 100) / 100.0;
    update_zoom();
    Application::settings().set_zoom_for_host(current_host(), m_zoom_level);
}

void ViewImplementation::zoom_out()
{
    if (m_zoom_level <= ZOOM_MIN_LEVEL)
        return;
    m_zoom_level = round_to<int>((m_zoom_level - ZOOM_STEP) * 100) / 100.0;
    update_zoom();
    Application::settings().set_zoom_for_host(current_host(), m_zoom_level);
}

void ViewImplementation::set_zoom(double zoom_level)
{
    m_zoom_level = max(ZOOM_MIN_LEVEL, min(zoom_level, ZOOM_MAX_LEVEL));
    update_zoom();
}

void ViewImplementation::reset_zoom()
{
    m_zoom_level = 1.0;
    update_zoom();
    client().async_reset_zoom(m_client_state.page_index);
    Application::settings().set_zoom_for_host(current_host(), m_zoom_level);
}

void ViewImplementation::enqueue_input_event(Web::InputEvent event)
{
    auto* mouse_event = event.get_pointer<Web::MouseEvent>();
    auto* pinch_event = event.get_pointer<Web::PinchEvent>();
    if (mouse_event && mouse_event->type == Web::MouseEvent::Type::MouseWheel) {
        mouse_event->wheel_delta_x /= zoom_level();
        mouse_event->wheel_delta_y /= zoom_level();
    }

    if (Application::web_content_options().enable_async_scrolling == EnableAsyncScrolling::Yes
        && m_client_state.has_usable_bitmap
        && mouse_event) {
        if (mouse_event->type == Web::MouseEvent::Type::MouseWheel) {
            auto wheel_delta_x = mouse_event->wheel_delta_x;
            auto wheel_delta_y = mouse_event->wheel_delta_y;
            if (mouse_event->modifiers & Web::UIEvents::KeyModifier::Mod_Shift)
                swap(wheel_delta_x, wheel_delta_y);

            auto device_pixels_per_css_pixel = static_cast<float>(device_pixel_ratio() * zoom_level());
            auto position = Gfx::FloatPoint {
                static_cast<float>(mouse_event->position.x().value()),
                static_cast<float>(mouse_event->position.y().value()),
            };
            auto delta_in_device_pixels = Gfx::FloatPoint { wheel_delta_x, wheel_delta_y }.scaled(device_pixels_per_css_pixel);
            dbgln_if(COMPOSITOR_DEBUG, "[Compositor] UI attempting compositor wheel bypass for page {} at {},{} device delta {},{}",
                m_client_state.page_index, position.x(), position.y(), delta_in_device_pixels.x(), delta_in_device_pixels.y());
            if (client().send_async_scroll_to_compositor(m_client_state.page_index, position, delta_in_device_pixels))
                mouse_event->async_scroll_performed_default_action = true;
            dbgln_if(COMPOSITOR_DEBUG, "[Compositor] UI compositor wheel bypass result for page {}: {}",
                m_client_state.page_index, mouse_event->async_scroll_performed_default_action ? "accepted"sv : "rejected"sv);
        } else if (client().handle_mouse_event_in_compositor(m_client_state.page_index, *mouse_event)) {
            dbgln_if(COMPOSITOR_DEBUG, "[Compositor] UI compositor handled mouse event for page {} at {},{}",
                m_client_state.page_index, mouse_event->position.x().value(), mouse_event->position.y().value());
            return;
        }
    }
    if (Application::web_content_options().enable_async_scrolling == EnableAsyncScrolling::Yes
        && m_client_state.has_usable_bitmap
        && pinch_event) {
        dbgln_if(COMPOSITOR_DEBUG, "[Compositor] UI attempting compositor pinch bypass for page {} at {},{} scale delta {}",
            m_client_state.page_index, pinch_event->position.x().value(), pinch_event->position.y().value(), pinch_event->scale_delta);
        auto handled = client().handle_pinch_event_in_compositor(m_client_state.page_index, *pinch_event);
        dbgln_if(COMPOSITOR_DEBUG, "[Compositor] UI compositor pinch bypass result for page {}: {}",
            m_client_state.page_index, handled ? "accepted"sv : "rejected"sv);
    }

    // Send the next event over to the WebContent to be handled by JS. We'll later get a message to say whether JS
    // prevented the default event behavior, at which point we either discard or handle that event, and then try to
    // process the next one.
    m_pending_input_events.enqueue(move(event));

    m_pending_input_events.tail().visit(
        [this](Web::KeyEvent const& event) {
            client().async_key_event(m_client_state.page_index, event.clone_without_browser_data());
        },
        [this](Web::MouseEvent const& event) {
            client().dispatch_mouse_event_to_web_content(m_client_state.page_index, event);
        },
        [this](Web::DragEvent& event) {
            auto cloned_event = event.clone_without_browser_data();
            cloned_event.files = move(event.files);

            client().async_drag_event(m_client_state.page_index, cloned_event);
        },
        [this](Web::PinchEvent const& event) {
            client().async_pinch_event(m_client_state.page_index, event);
        });
}

void ViewImplementation::did_finish_handling_input_event(Badge<WebContentClient>, Web::EventResult event_result)
{
    auto event = m_pending_input_events.dequeue();

    if (event_result == Web::EventResult::Handled)
        return;

    // Here we handle events that were not consumed or cancelled by the WebContent. Propagate the event back
    // to the concrete view implementation.
    event.visit(
        [this](Web::KeyEvent const& event) {
            if (on_finish_handling_key_event)
                on_finish_handling_key_event(event);
        },
        [this](Web::DragEvent const& event) {
            if (on_finish_handling_drag_event)
                on_finish_handling_drag_event(event);
        },
        [](auto const&) {});
}

void ViewImplementation::set_preferred_color_scheme(Web::CSS::PreferredColorScheme color_scheme)
{
    m_preferred_color_scheme = color_scheme;
    set_page_background_color(preferred_canvas_background_color());

    client().async_set_preferred_color_scheme(page_id(), color_scheme);
}

void ViewImplementation::set_preferred_contrast(Web::CSS::PreferredContrast contrast)
{
    client().async_set_preferred_contrast(page_id(), contrast);
}

void ViewImplementation::set_preferred_motion(Web::CSS::PreferredMotion motion)
{
    client().async_set_preferred_motion(page_id(), motion);
}

void ViewImplementation::notify_cookies_changed(HashTable<String> const& changed_domains, ReadonlySpan<HTTP::Cookie::Cookie> page_cookies, ReadonlySpan<HTTP::Cookie::Cookie> host_cookies)
{
    for (auto const& domain : changed_domains) {
        if (auto document_index = m_document_cookie_version_indices.get(domain); document_index.has_value())
            Core::increment_shared_version(m_document_cookie_version_buffer, *document_index);
    }

    if (!page_cookies.is_empty())
        client().async_cookies_changed(page_id(), page_cookies);
    if (m_on_host_cookie_change)
        m_on_host_cookie_change(Vector<HTTP::Cookie::Cookie> { host_cookies });
}

void ViewImplementation::listen_for_host_cookie_changes(DevTools::DevToolsDelegate::OnHostCookieChange on_cookie_change)
{
    m_on_host_cookie_change = move(on_cookie_change);
}

void ViewImplementation::stop_listening_for_host_cookie_changes()
{
    m_on_host_cookie_change = nullptr;
}

void ViewImplementation::notify_storage_changed(DevTools::DevToolsDelegate::StorageChange change)
{
    for (auto& listener : m_storage_change_listeners)
        listener.value(change);
}

u64 ViewImplementation::add_storage_change_listener(DevTools::DevToolsDelegate::OnStorageChange on_storage_change)
{
    auto listener_id = m_next_storage_change_listener_id++;
    m_storage_change_listeners.set(listener_id, move(on_storage_change));
    return listener_id;
}

void ViewImplementation::remove_storage_change_listener(u64 listener_id)
{
    m_storage_change_listeners.remove(listener_id);
}

ErrorOr<Core::SharedVersionIndex> ViewImplementation::ensure_document_cookie_version_index(Badge<WebContentClient>, String const& domain)
{
    return m_document_cookie_version_indices.try_ensure(domain, [&]() -> ErrorOr<Core::SharedVersionIndex> {
        Core::SharedVersionIndex document_index = m_document_cookie_version_indices.size();

        if (!Core::initialize_shared_version(m_document_cookie_version_buffer, document_index)) {
            dbgln("Reached maximum document cookie version count for {}, cannot create new version for {}", m_url, domain);
            return Error::from_string_literal("Reached maximum document cookie version count");
        }

        return document_index;
    });
}

Optional<Core::SharedVersion> ViewImplementation::document_cookie_version(URL::URL const& url) const
{
    auto domain = HTTP::Cookie::canonicalize_domain(url);
    if (!domain.has_value())
        return {};

    auto document_index = m_document_cookie_version_indices.get(*domain);
    if (!document_index.has_value())
        return {};

    return Core::get_shared_version(m_document_cookie_version_buffer, *document_index);
}

ByteString ViewImplementation::selected_text()
{
    return client().get_selected_text(page_id());
}

ByteString ViewImplementation::cut_selected_text()
{
    return client().cut_selected_text(page_id());
}

Optional<String> ViewImplementation::selected_text_with_whitespace_collapsed()
{
    auto selected_text = MUST(Web::Infra::strip_and_collapse_whitespace(this->selected_text()));
    if (selected_text.is_empty())
        return OptionalNone {};
    return selected_text;
}

void ViewImplementation::select_all()
{
    client().async_select_all(page_id());
}

void ViewImplementation::find_in_page(String const& query, CaseSensitivity case_sensitivity)
{
    client().async_find_in_page(page_id(), query, case_sensitivity);
}

void ViewImplementation::find_in_page_next_match()
{
    client().async_find_in_page_next_match(page_id());
}

void ViewImplementation::find_in_page_previous_match()
{
    client().async_find_in_page_previous_match(page_id());
}

void ViewImplementation::get_source()
{
    client().async_get_source(page_id());
}

void ViewImplementation::inspect_dom_tree()
{
    client().async_inspect_dom_tree(page_id());
}

void ViewImplementation::inspect_storage(Web::StorageAPI::StorageEndpointType storage_endpoint, u64 request_id)
{
    client().async_inspect_storage(page_id(), storage_endpoint, request_id);
}

Optional<StorageSetResult> ViewImplementation::set_session_storage_item(String const& key, String const& value)
{
    return client().set_session_storage_item(page_id(), key, value);
}

Optional<String> ViewImplementation::remove_session_storage_item(String const& key)
{
    return client().remove_session_storage_item(page_id(), key);
}

bool ViewImplementation::clear_session_storage()
{
    return client().clear_session_storage(page_id());
}

void ViewImplementation::inspect_accessibility_tree()
{
    client().async_inspect_accessibility_tree(page_id());
}

void ViewImplementation::get_hovered_node_id()
{
    client().async_get_hovered_node_id(page_id());
}

void ViewImplementation::start_node_picker(DevTools::DevToolsDelegate::OnNodePickerEvent on_node_picker_event)
{
    m_node_picker_active = true;
    m_node_picker_hovered_node_id.clear();
    m_pending_node_picker_requests.clear();
    m_on_node_picker_event = move(on_node_picker_event);
}

void ViewImplementation::stop_node_picker()
{
    if (!m_node_picker_active)
        return;

    clear_node_picker();
    m_node_picker_active = false;
    m_pending_node_picker_requests.clear();
    m_on_node_picker_event = nullptr;
}

void ViewImplementation::clear_node_picker()
{
    m_node_picker_hovered_node_id.clear();
    clear_highlighted_dom_node();
}

void ViewImplementation::node_picker_hover(Web::DevicePixelPoint position)
{
    request_node_picker_hit_test(NodePickerRequestType::Hovered, position);
}

void ViewImplementation::node_picker_pick(Web::DevicePixelPoint position)
{
    request_node_picker_hit_test(NodePickerRequestType::Picked, position);
}

void ViewImplementation::node_picker_preview(Web::DevicePixelPoint position)
{
    request_node_picker_hit_test(NodePickerRequestType::Previewed, position);
}

void ViewImplementation::node_picker_cancel()
{
    if (!m_node_picker_active)
        return;

    if (m_on_node_picker_event) {
        m_on_node_picker_event({
            .type = DevTools::DevToolsDelegate::NodePickerEvent::Type::Canceled,
            .node_id = {},
        });
    }
}

void ViewImplementation::request_node_picker_hit_test(NodePickerRequestType type, Web::DevicePixelPoint position)
{
    if (!m_node_picker_active)
        return;

    auto request_id = m_next_node_picker_request_id++;
    m_pending_node_picker_requests.set(request_id, type);
    client().async_get_node_id_at_position(page_id(), request_id, position);
}

void ViewImplementation::did_receive_node_picker_hit_test(u64 request_id, Web::UniqueNodeID node_id)
{
    auto request_type = m_pending_node_picker_requests.take(request_id);
    if (!request_type.has_value() || !m_node_picker_active)
        return;

    if (*request_type == NodePickerRequestType::Hovered) {
        if (node_id == m_node_picker_hovered_node_id.value_or(0))
            return;

        m_node_picker_hovered_node_id = node_id;
        if (node_id == 0) {
            clear_node_picker();
            return;
        }
    } else if (node_id == 0) {
        return;
    }

    if (!m_on_node_picker_event)
        return;

    DevTools::DevToolsDelegate::NodePickerEvent::Type event_type;
    switch (*request_type) {
    case NodePickerRequestType::Hovered:
        event_type = DevTools::DevToolsDelegate::NodePickerEvent::Type::Hovered;
        break;
    case NodePickerRequestType::Picked:
        event_type = DevTools::DevToolsDelegate::NodePickerEvent::Type::Picked;
        break;
    case NodePickerRequestType::Previewed:
        event_type = DevTools::DevToolsDelegate::NodePickerEvent::Type::Previewed;
        break;
    }

    m_on_node_picker_event({
        .type = event_type,
        .node_id = node_id,
    });
}

void ViewImplementation::inspect_dom_node(Web::UniqueNodeID node_id, DOMNodeProperties::Type property_type, Optional<Web::CSS::PseudoElement> pseudo_element, JsonValue options)
{
    client().async_inspect_dom_node(page_id(), property_type, node_id, pseudo_element, move(options));
}

void ViewImplementation::inspect_grid_layouts(Web::UniqueNodeID root_node_id)
{
    client().async_inspect_grid_layouts(page_id(), root_node_id);
}

void ViewImplementation::inspect_current_grid(Web::UniqueNodeID node_id)
{
    client().async_inspect_current_grid(page_id(), node_id);
}

void ViewImplementation::inspect_current_flexbox(Web::UniqueNodeID node_id, bool only_look_at_parents)
{
    client().async_inspect_current_flexbox(page_id(), node_id, only_look_at_parents);
}

void ViewImplementation::clear_inspected_dom_node()
{
    client().async_clear_inspected_dom_node(page_id());
}

void ViewImplementation::highlight_dom_node(Web::UniqueNodeID node_id, Optional<Web::CSS::PseudoElement> pseudo_element)
{
    client().async_highlight_dom_node(page_id(), node_id, pseudo_element);
}

void ViewImplementation::clear_highlighted_dom_node()
{
    highlight_dom_node(0, {});
}

void ViewImplementation::highlight_flexbox(Web::UniqueNodeID node_id, JsonValue options)
{
    client().async_highlight_flexbox(page_id(), node_id, move(options));
}

void ViewImplementation::clear_flexbox_highlight(Web::UniqueNodeID node_id)
{
    client().async_clear_flexbox_highlight(page_id(), node_id);
}

void ViewImplementation::highlight_grid(Web::UniqueNodeID node_id, JsonValue options)
{
    client().async_highlight_grid(page_id(), node_id, move(options));
}

void ViewImplementation::clear_grid_highlight(Web::UniqueNodeID node_id)
{
    client().async_clear_grid_highlight(page_id(), node_id);
}

void ViewImplementation::set_listen_for_dom_mutations(bool listen_for_dom_mutations)
{
    client().async_set_listen_for_dom_mutations(page_id(), listen_for_dom_mutations);
}

void ViewImplementation::did_connect_devtools_client()
{
    m_devtools_connected = true;
    client().async_did_connect_devtools_client(page_id());
}

void ViewImplementation::did_disconnect_devtools_client()
{
    m_devtools_connected = false;
    client().async_did_disconnect_devtools_client(page_id());
}

void ViewImplementation::get_dom_node_inner_html(Web::UniqueNodeID node_id)
{
    client().async_get_dom_node_inner_html(page_id(), node_id);
}

void ViewImplementation::get_dom_node_outer_html(Web::UniqueNodeID node_id)
{
    client().async_get_dom_node_outer_html(page_id(), node_id);
}

void ViewImplementation::set_dom_node_outer_html(Web::UniqueNodeID node_id, String const& html)
{
    client().async_set_dom_node_outer_html(page_id(), node_id, html);
}

void ViewImplementation::set_dom_node_text(Web::UniqueNodeID node_id, String const& text)
{
    client().async_set_dom_node_text(page_id(), node_id, text);
}

void ViewImplementation::set_dom_node_tag(Web::UniqueNodeID node_id, String const& name)
{
    client().async_set_dom_node_tag(page_id(), node_id, name);
}

void ViewImplementation::add_dom_node_attributes(Web::UniqueNodeID node_id, ReadonlySpan<Attribute> attributes)
{
    client().async_add_dom_node_attributes(page_id(), node_id, attributes);
}

void ViewImplementation::replace_dom_node_attribute(Web::UniqueNodeID node_id, String const& name, ReadonlySpan<Attribute> replacement_attributes)
{
    client().async_replace_dom_node_attribute(page_id(), node_id, name, replacement_attributes);
}

void ViewImplementation::create_child_element(Web::UniqueNodeID node_id)
{
    client().async_create_child_element(page_id(), node_id);
}

void ViewImplementation::create_child_text_node(Web::UniqueNodeID node_id)
{
    client().async_create_child_text_node(page_id(), node_id);
}

void ViewImplementation::insert_dom_node_before(Web::UniqueNodeID node_id, Web::UniqueNodeID parent_node_id, Optional<Web::UniqueNodeID> sibling_node_id)
{
    client().async_insert_dom_node_before(page_id(), node_id, parent_node_id, sibling_node_id);
}

void ViewImplementation::clone_dom_node(Web::UniqueNodeID node_id)
{
    client().async_clone_dom_node(page_id(), node_id);
}

void ViewImplementation::remove_dom_node(Web::UniqueNodeID node_id)
{
    client().async_remove_dom_node(page_id(), node_id);
}

void ViewImplementation::list_style_sheets()
{
    client().async_list_style_sheets(page_id());
}

void ViewImplementation::request_style_sheet_source(Web::CSS::StyleSheetIdentifier const& identifier)
{
    client().async_request_style_sheet_source(page_id(), identifier);
}

void ViewImplementation::debug_request(ByteString const& request, ByteString const& argument)
{
    if (request == "dump-session-history"sv)
        dump_session_history("debug-request"sv, SessionHistoryDumpMode::Always);

    client().async_debug_request(page_id(), request, argument);
}

void ViewImplementation::set_content_blockers(Core::AnonymousBuffer const& patterns)
{
    client().async_set_content_blockers(page_id(), patterns);
}

void ViewImplementation::run_javascript(String const& js_source)
{
    client().async_run_javascript(page_id(), js_source);
}

void ViewImplementation::js_console_input(String const& js_source)
{
    client().async_js_console_input(page_id(), js_source);
}

void ViewImplementation::exit_fullscreen()
{
    client().async_exit_fullscreen(page_id());
}

void ViewImplementation::set_is_fullscreen(Web::ViewportIsFullscreen is_fullscreen)
{
    if (m_is_fullscreen == is_fullscreen)
        return;
    m_is_fullscreen = is_fullscreen;

    handle_resize();
    did_update_window_rect();
}

void ViewImplementation::alert_closed()
{
    client().async_alert_closed(page_id());
}

void ViewImplementation::confirm_closed(bool accepted)
{
    client().async_confirm_closed(page_id(), accepted);
}

void ViewImplementation::prompt_closed(Optional<String> const& response)
{
    client().async_prompt_closed(page_id(), response);
}

void ViewImplementation::color_picker_update(Optional<Color> picked_color, Web::HTML::ColorPickerUpdateState state)
{
    client().async_color_picker_update(page_id(), picked_color, state);
}

void ViewImplementation::file_picker_closed(Vector<Web::HTML::SelectedFile> selected_files)
{
    client().async_file_picker_closed(page_id(), move(selected_files));
}

void ViewImplementation::select_dropdown_closed(Optional<u32> const& selected_item_id)
{
    client().async_select_dropdown_closed(page_id(), selected_item_id);
}

void ViewImplementation::paste_text_from_clipboard()
{
    client().async_paste(page_id(), Application::the().clipboard_text());
}

void ViewImplementation::set_marked_text_from_input_method(Utf16String const& text)
{
    client().async_set_marked_text_from_input_method(page_id(), text);
}

void ViewImplementation::commit_text_from_input_method(Utf16String const& text)
{
    client().async_commit_text_from_input_method(page_id(), text);
}

void ViewImplementation::unmark_text_from_input_method()
{
    client().async_unmark_text_from_input_method(page_id());
}

Optional<Web::DevicePixelRect> ViewImplementation::get_input_caret_rect()
{
    // Returns the most-recent caret position pushed by WebContent (see set_input_caret_rect). Deliberately makes no
    // synchronous IPC request: This is read from inside AppKit text-input callbacks — where blocking can re-enter the
    // run loop and deadlock the input method.
    return m_input_caret_rect;
}

void ViewImplementation::set_input_caret_rect(Badge<WebContentClient>, Optional<Web::DevicePixelRect> rect)
{
    m_input_caret_rect = rect;
}

void ViewImplementation::retrieved_clipboard_entries(u64 request_id, ReadonlySpan<Web::Clipboard::SystemClipboardItem> items)
{
    client().async_retrieved_clipboard_entries(page_id(), request_id, items);
}

void ViewImplementation::toggle_page_mute_state()
{
    m_mute_state = Web::HTML::invert_mute_state(m_mute_state);
    client().async_toggle_page_mute_state(page_id());
}

void ViewImplementation::did_change_audio_play_state(Badge<WebContentClient>, Web::HTML::AudioPlayState play_state)
{
    bool state_changed = false;

    switch (play_state) {
    case Web::HTML::AudioPlayState::Paused:
        if (--m_number_of_elements_playing_audio == 0) {
            m_audio_play_state = play_state;
            state_changed = true;
        }
        break;

    case Web::HTML::AudioPlayState::Playing:
        if (m_number_of_elements_playing_audio++ == 0) {
            m_audio_play_state = play_state;
            state_changed = true;
        }
        break;
    }

    if (state_changed && on_audio_play_state_changed)
        on_audio_play_state_changed(m_audio_play_state);
}

void ViewImplementation::did_update_navigation_buttons_state(Badge<WebContentClient>, bool back_enabled, bool forward_enabled)
{
    VERIFY(!should_manage_session_history_in_ui_process());

    m_navigate_back_action->set_enabled(back_enabled);
    m_navigate_forward_action->set_enabled(forward_enabled);
    dump_session_history("did-update-navigation-buttons-state-using-webcontent"sv);
}

static Optional<size_t> current_top_level_history_entry_index_for_step(Vector<Web::HTML::SessionHistoryEntryDescriptor> const&, Optional<i32> current_step);

TraversableSessionHistory::UpdateResult ViewImplementation::update_session_history_from_web_content(Vector<Web::HTML::SessionHistoryEntryDescriptor> entries, Vector<i32> used_steps, size_t current_used_step_index, bool pending_step_after_fallback_load_was_restored, bool seed_web_content_on_invalid_snapshot)
{
    auto update_result = m_session_history.update_from_web_content(move(entries), move(used_steps), current_used_step_index);
    m_current_web_content_session_history_matches_mirror = update_result == TraversableSessionHistory::UpdateResult::CompleteSnapshot
        && m_session_history.web_content_history_matches_mirror();
    if (update_result != TraversableSessionHistory::UpdateResult::InvalidSnapshot) {
        // NB: A complete WebContent snapshot means the UI-owned navigation settled, including redirected navigations
        //     whose final URL differs from the original pending navigation URL. A partial snapshot only updates the UI
        //     mirror with the current WebContent-visible subset.
        if (update_result == TraversableSessionHistory::UpdateResult::CompleteSnapshot)
            m_pending_session_history_navigation.clear();
        if (auto* current_entry = m_session_history.current_entry()) {
            auto current_url = current_entry->url;
            auto const url_changed = m_url != current_url;
            set_url(current_url);
            if (url_changed && on_url_change)
                on_url_change(m_url);
            if (m_webdriver_pending_navigation_url.has_value() && *m_webdriver_pending_navigation_url != current_url)
                m_webdriver_pending_navigation_url = current_url;
            if (m_webdriver_pending_navigation_completes_with_session_history_update)
                complete_webdriver_pending_navigation_if_url_matches(m_url);
        }
        if (pending_step_after_fallback_load_was_restored)
            m_pending_web_content_session_history_seed.step_after_loading_top_level_entry.clear();
    } else if (seed_web_content_on_invalid_snapshot) {
        if (auto const* current_entry = m_session_history.current_entry(); current_entry && current_entry->url == m_url) {
            prepare_to_seed_web_content_session_history_from_ui_process();
            seed_web_content_session_history_from_ui_process();
        }
    }

    update_navigation_action_state();
    return update_result;
}

bool ViewImplementation::adopt_web_content_session_history_after_rejected_seed(Vector<Web::HTML::SessionHistoryEntryDescriptor> entries, Vector<i32> used_steps, size_t current_used_step_index)
{
    if (entries.is_empty())
        return false;

    auto entries_from_web_content = entries;
    auto used_steps_from_web_content = used_steps;
    auto update_result = update_session_history_from_web_content(move(entries), move(used_steps), current_used_step_index, false, false);
    if (update_result == TraversableSessionHistory::UpdateResult::InvalidSnapshot && current_used_step_index < used_steps_from_web_content.size()) {
        auto current_top_level_entry_index = current_top_level_history_entry_index_for_step(entries_from_web_content, used_steps_from_web_content[current_used_step_index]);
        if (current_top_level_entry_index.has_value() && entries_from_web_content[*current_top_level_entry_index].url == m_url) {
            m_session_history.clear();
            update_result = update_session_history_from_web_content(move(entries_from_web_content), move(used_steps_from_web_content), current_used_step_index, false, false);
        }
    }
    if (update_result == TraversableSessionHistory::UpdateResult::InvalidSnapshot)
        return false;

    m_pending_web_content_session_history_seed.clear();
    m_pending_session_history_traversal.clear();
    return true;
}

void ViewImplementation::did_update_session_history(Badge<WebContentClient>, Vector<Web::HTML::SessionHistoryEntryDescriptor> entries, Vector<i32> used_steps, size_t current_used_step_index)
{
    if (!should_manage_session_history_in_ui_process())
        return;

    if (history_debug_enabled()) {
        dbgln("[History] UI received WebContent session history snapshot page={} pid={} current_used_step={} entries={} used_steps={}",
            page_id(),
            client().pid(),
            current_used_step_index,
            history_log_entries(entries),
            history_log_steps(used_steps, current_used_step_index));
    }
    if (m_pending_web_content_session_history_seed.waiting_for_ack) {
        dump_session_history("ignored-session-history-before-ui-seed-ack"sv);
        update_navigation_action_state();
        return;
    }
    auto pending_step_after_fallback_load_was_restored = false;
    if (m_pending_web_content_session_history_seed.step_after_loading_top_level_entry.has_value()) {
        if (current_used_step_index >= used_steps.size() || used_steps[current_used_step_index] != *m_pending_web_content_session_history_seed.step_after_loading_top_level_entry) {
            dump_session_history("ignored-partial-session-history-before-fallback-seed"sv);
            update_navigation_action_state();
            return;
        }
        pending_step_after_fallback_load_was_restored = true;
    }
    if (m_pending_web_content_session_history_seed.ignore_updates_until_seed) {
        dump_session_history("ignored-session-history-before-ui-seed"sv);
        update_navigation_action_state();
        return;
    }
    update_session_history_from_web_content(move(entries), move(used_steps), current_used_step_index, pending_step_after_fallback_load_was_restored, true);
    dump_session_history("did-update-session-history"sv);
}

void ViewImplementation::did_update_session_history_for_testing(Badge<WebContentClient>, Vector<Web::HTML::SessionHistoryEntryDescriptor> entries, Vector<i32> used_steps, size_t current_used_step_index)
{
    if (!should_manage_session_history_in_ui_process())
        return;

    // NB: dumpUIProcessSessionHistory() first sends WebContent's current snapshot to the UI process, then returns
    //     the UI mirror. If a stale seed ack is still pending, normal async snapshots are intentionally ignored, so
    //     use the same convergence path as a rejected seed ack to make this testing hook deterministic.
    if (m_pending_web_content_session_history_seed.waiting_for_ack) {
        if (adopt_web_content_session_history_after_rejected_seed(move(entries), move(used_steps), current_used_step_index))
            dump_session_history("did-update-session-history-for-testing-after-rejected-seed"sv);
        else
            dump_session_history("ignored-session-history-for-testing-before-ui-seed-ack"sv);
        return;
    }

    update_session_history_from_web_content(move(entries), move(used_steps), current_used_step_index, false, true);
    dump_session_history("did-update-session-history-for-testing"sv);
}

void ViewImplementation::did_change_needs_beforeunload_check(Badge<WebContentClient>, bool needs_beforeunload_check)
{
    m_needs_beforeunload_check = needs_beforeunload_check;
}

void ViewImplementation::did_change_background_color(Badge<WebContentClient>, Gfx::Color color)
{
    set_page_background_color(color);
}

void ViewImplementation::set_page_background_color_to_system_canvas(bool dark)
{
    auto color_scheme = dark ? Web::CSS::PreferredColorScheme::Dark : Web::CSS::PreferredColorScheme::Light;
    m_system_canvas_background_color = Web::CSS::SystemColor::canvas(color_scheme);
    set_page_background_color(preferred_canvas_background_color());
}

void ViewImplementation::set_page_background_color(Gfx::Color color)
{
    m_page_background_color = color;
    if (on_page_background_color_change)
        on_page_background_color_change(m_page_background_color);
}

Gfx::Color ViewImplementation::preferred_canvas_background_color() const
{
    if (m_preferred_color_scheme == Web::CSS::PreferredColorScheme::Dark)
        return Web::CSS::SystemColor::canvas(Web::CSS::PreferredColorScheme::Dark);
    if (m_preferred_color_scheme == Web::CSS::PreferredColorScheme::Light)
        return Web::CSS::SystemColor::canvas(Web::CSS::PreferredColorScheme::Light);
    return m_system_canvas_background_color;
}

void ViewImplementation::did_allocate_backing_stores(Badge<WebContentClient>, i32 front_bitmap_id, Gfx::SharedImage front_backing_store, i32 back_bitmap_id, Gfx::SharedImage back_backing_store)
{
    dbgln_if(COMPOSITOR_DEBUG, "[Compositor] UI installing backing stores for page {} front={} back={} had_usable_bitmap={}",
        page_id(), front_bitmap_id, back_bitmap_id, m_client_state.has_usable_bitmap);
    if (m_client_state.has_usable_bitmap) {
        // NOTE: We keep the outgoing front bitmap as a backup so we have something to paint until we get a new one.
        m_backup_shared_image_buffer = move(m_client_state.front_bitmap.shared_image_buffer);
        m_backup_bitmap_size = m_client_state.front_bitmap.last_painted_size;
    }
    m_client_state.has_usable_bitmap = false;
    m_client_state.front_bitmap.id = front_bitmap_id;
    m_client_state.back_bitmap.id = back_bitmap_id;
    m_client_state.front_bitmap.shared_image_buffer = make<Gfx::SharedImageBuffer>(Gfx::SharedImageBuffer::import_from_shared_image(move(front_backing_store)));
    m_client_state.back_bitmap.shared_image_buffer = make<Gfx::SharedImageBuffer>(Gfx::SharedImageBuffer::import_from_shared_image(move(back_backing_store)));
}

void ViewImplementation::update_zoom()
{
    if (m_zoom_level != 1.0) {
        m_reset_zoom_action->set_text(MUST(String::formatted("{}%", round_to<int>(m_zoom_level * 100))));
        m_reset_zoom_action->set_visible(true);
    } else {
        m_reset_zoom_action->set_visible(false);
    }

    client().async_set_zoom_level(m_client_state.page_index, m_zoom_level);
}

String ViewImplementation::current_host() const
{
    if (!m_url.host().has_value())
        return {};
    return m_url.serialized_host();
}

void ViewImplementation::apply_zoom_for_current_host()
{
    auto& settings = Application::settings();
    auto zoom_level = settings.zoom_for_host(current_host()).value_or(settings.default_zoom_level_factor());
    if (zoom_level == m_zoom_level)
        return;
    m_zoom_level = zoom_level;
    update_zoom();
}

void ViewImplementation::handle_resize()
{
    client().async_set_viewport(page_id(), viewport_size(), m_device_pixel_ratio, m_is_fullscreen);
    Application::the().update_compositor_viewport(client().compositor_context_id_for_page(page_id()), viewport_size().to_type<int>(), Web::Compositor::WindowResizingInProgress::Yes);
}

void ViewImplementation::initialize_client(CreateNewClient create_new_client)
{
    m_needs_beforeunload_check = true;

    if (create_new_client == CreateNewClient::Yes) {
        auto client_handle = m_client_state.client_handle;
        m_client_state = {};
        m_client_state.client_handle = move(client_handle);

        // FIXME: Fail to open the tab, rather than crashing the whole application if this fails.
        m_client_state.client = Application::the().launch_web_content_process(*this).release_value_but_fixme_should_propagate_errors();
    } else {
        m_client_state.client->register_view(m_client_state.page_index, *this);
    }

    if (m_client_state.client_handle.is_empty())
        m_client_state.client_handle = Web::Crypto::generate_random_uuid();
    client().async_set_window_handle(m_client_state.page_index, m_client_state.client_handle);
    client().async_set_zoom_level(m_client_state.page_index, m_zoom_level);
    client().async_set_viewport(m_client_state.page_index, viewport_size(), m_device_pixel_ratio, m_is_fullscreen);
    client().async_set_maximum_frames_per_second(m_client_state.page_index, m_maximum_frames_per_second);
    client().async_set_system_visibility_state(m_client_state.page_index, m_system_visibility_state);
    auto compositor_context_id = client().compositor_context_id_for_page(m_client_state.page_index);
    Application::the().update_compositor_viewport(compositor_context_id, viewport_size().to_type<int>());
    client().async_set_document_cookie_version_buffer(m_client_state.page_index, m_document_cookie_version_buffer);

    if (auto webdriver_endpoint = Application::browser_options().webdriver_endpoint; webdriver_endpoint.has_value())
        client().async_connect_to_webdriver(m_client_state.page_index, *webdriver_endpoint);

    Application::the().apply_view_options({}, *this);

    default_zoom_level_factor_changed();
    languages_changed();
    browsing_behavior_changed();
    autoplay_settings_changed();
    global_privacy_control_changed();

    // If DevTools is connected, notify the new WebContent process.
    if (m_devtools_connected)
        client().async_did_connect_devtools_client(page_id());
}

void ViewImplementation::did_start_navigation(URL::URL const& url, Variant<Empty, String, Web::HTML::POSTResource> document_resource, bool is_redirect, Web::Bindings::NavigationHistoryBehavior history_handling)
{
    if (!should_manage_session_history_in_ui_process())
        return;

    if (m_should_suppress_history_for_next_load || m_should_suppress_history_for_current_load)
        return;

    if (m_session_history_entry_url_loading_from_ui_process.has_value()) {
        if (*m_session_history_entry_url_loading_from_ui_process != url) {
            // NB: An earlier UI-process history fallback load can start after a newer back/forward request has
            //     superseded it. Chromium, WebKit, and Firefox all give pending history loads an identity so stale
            //     traversals cannot consume state belonging to the current one. Keep the UI-owned history authoritative
            //     here and wait for the load matching the latest requested entry.
            dump_session_history("ignored-stale-ui-history-load-start"sv);
            return;
        }

        auto should_keep_preseeded_web_content_history = m_pending_web_content_session_history_seed.waiting_for_ack || m_session_history.web_content_uses_ui_step_coordinates();
        m_session_history_entry_url_loading_from_ui_process.clear();
        if (!should_keep_preseeded_web_content_history) {
            m_current_web_content_session_history_matches_mirror = false;
            m_session_history.forget_web_content_state();
        }
        dump_session_history("did-start-navigation-from-ui-history-load"sv);
        return;
    }

    if (m_pending_web_content_session_history_seed.should_send_entries || m_pending_web_content_session_history_seed.ignore_updates_until_seed || m_pending_web_content_session_history_seed.waiting_for_ack) {
        if (auto const* current_entry = m_session_history.current_entry(); current_entry && current_entry->url != url) {
            dump_session_history("ignored-navigation-start-before-ui-history-seed"sv);
            return;
        }
    }

    if (m_is_showing_crash_page) {
        m_is_showing_crash_page = false;
        if (auto const* current_entry = m_session_history.current_entry(); current_entry && current_entry->url == url) {
            prepare_to_seed_web_content_session_history_from_ui_process();
            dump_session_history("did-start-navigation-from-crash-page"sv);
            return;
        }
    }

    if (is_redirect) {
        m_session_history.replace_current_entry_url(url);
        if (m_pending_session_history_navigation.has_value())
            m_pending_session_history_navigation->url = url;
        if (m_webdriver_pending_navigation_url.has_value())
            m_webdriver_pending_navigation_url = url;
        m_current_web_content_session_history_matches_mirror = false;
        update_navigation_action_state();
        dump_session_history("did-start-navigation-redirect"sv);
        return;
    }

    if (auto const* current_entry = m_session_history.current_entry(); current_entry && current_entry->url == url) {
        if (m_pending_session_history_navigation.has_value() && m_pending_session_history_navigation->url == url)
            return;

        if (history_handling == Web::Bindings::NavigationHistoryBehavior::Push && m_current_web_content_session_history_matches_mirror)
            m_pending_session_history_navigation = PendingSessionHistoryNavigation { url, m_session_history };
        else
            m_pending_session_history_navigation.clear();

        if (history_handling == Web::Bindings::NavigationHistoryBehavior::Replace) {
            m_session_history.replace_current_entry(url, move(document_resource));
            m_current_web_content_session_history_matches_mirror = false;
            update_navigation_action_state();
            dump_session_history("did-start-navigation-replace-current-url"sv);
        } else if (history_handling == Web::Bindings::NavigationHistoryBehavior::Push) {
            m_session_history.navigate(url, move(document_resource));
            m_current_web_content_session_history_matches_mirror = false;
            update_navigation_action_state();
            dump_session_history("did-start-navigation-push-current-url"sv);
        }
        return;
    }

    if (m_session_history.current_entry())
        m_pending_session_history_navigation = PendingSessionHistoryNavigation { url, m_session_history };
    else
        m_pending_session_history_navigation.clear();
    if (history_handling == Web::Bindings::NavigationHistoryBehavior::Replace)
        m_session_history.replace_current_entry(url, move(document_resource));
    else
        m_session_history.navigate(url, move(document_resource));
    m_current_web_content_session_history_matches_mirror = false;
    update_navigation_action_state();
    dump_session_history("did-start-navigation"sv);
}

void ViewImplementation::did_cancel_navigation(URL::URL const& url)
{
    if (m_pending_session_history_navigation.has_value() && m_pending_session_history_navigation->url == url) {
        restore_pending_session_history_navigation("did-cancel-navigation"sv);
        return;
    }

    if (m_session_history_entry_url_loading_from_ui_process.has_value() && *m_session_history_entry_url_loading_from_ui_process == url) {
        m_session_history_entry_url_loading_from_ui_process.clear();
        abandon_pending_web_content_session_history_seed();
        m_current_web_content_session_history_matches_mirror = false;
        m_session_history.forget_web_content_state();
        update_navigation_action_state();
        dump_session_history("did-cancel-ui-history-load"sv);
        return;
    }

    if (m_webdriver_pending_navigation_url.has_value()) {
        m_session_history.clear_current_entry_reload_pending();
        m_webdriver_pending_navigation_url = m_url;
        m_webdriver_pending_navigation_completes_with_session_history_update = false;
        complete_webdriver_pending_navigation_if_url_matches(m_url);
    }

    dump_session_history("did-cancel-navigation-ignored"sv);
}

void ViewImplementation::did_finish_navigation(URL::URL const& url)
{
    if (m_webdriver_pending_navigation_url.has_value() && *m_webdriver_pending_navigation_url == url && !m_webdriver_pending_navigation_completes_with_session_history_update)
        complete_webdriver_pending_navigation_if_url_matches(url);

    if (m_pending_session_history_navigation.has_value() && m_pending_session_history_navigation->url == url)
        m_pending_session_history_navigation.clear();

    if (should_manage_session_history_in_ui_process() && m_pending_web_content_session_history_seed.should_send_entries) {
        if (auto const* current_entry = m_session_history.current_entry(); current_entry && current_entry->url == url) {
            m_session_history.clear_current_entry_reload_pending();
            auto allow_current_entry_reconstruction = m_pending_web_content_session_history_seed.should_reseed_after_current_history_load
                ? AllowCurrentEntryReconstruction::Yes
                : AllowCurrentEntryReconstruction::No;
            m_pending_web_content_session_history_seed.should_reseed_after_current_history_load = false;
            seed_web_content_session_history_from_ui_process(allow_current_entry_reconstruction);
        } else {
            // NB: The first finish notification from a fresh WebContent process can still report about:blank before the
            //     traversed-to entry is ready. Keep the pending seed state intact so partial snapshots remain ignored
            //     until we can seed the full UI-owned history.
            dump_session_history("skip-seed-webcontent-session-history"sv);
        }
    }
}

bool ViewImplementation::restore_pending_session_history_navigation(StringView reason)
{
    if (!m_pending_session_history_navigation.has_value())
        return false;

    auto web_content_restore_mode = m_pending_session_history_navigation->web_content_restore_mode;
    m_session_history = move(m_pending_session_history_navigation->previous_session_history);
    m_pending_session_history_navigation.clear();
    m_pending_session_history_traversal.clear();

    if (auto* current_entry = m_session_history.current_entry()) {
        auto current_url = current_entry->url;
        auto const url_changed = m_url != current_url;
        set_url(current_url);
        if (url_changed && on_url_change)
            on_url_change(m_url);

        if (web_content_restore_mode == PendingSessionHistoryNavigation::WebContentRestoreMode::RestoreFromUIProcess) {
            prepare_to_seed_web_content_session_history_from_ui_process();
            m_should_suppress_history_for_current_load = false;
            m_should_suppress_history_for_next_load = false;
            m_webdriver_pending_navigation_url = current_url;
            m_webdriver_pending_navigation_completes_with_session_history_update = true;
            load_current_session_history_entry_from_ui_process();
        } else {
            m_session_history_entry_url_loading_from_ui_process.clear();
            abandon_pending_web_content_session_history_seed();
            m_current_web_content_session_history_matches_mirror = m_session_history.web_content_history_matches_mirror();
            m_webdriver_pending_navigation_url.clear();
            m_webdriver_pending_navigation_completes_with_session_history_update = false;
            complete_webdriver_pending_navigation_if_url_matches(current_url);
        }
    } else {
        m_current_web_content_session_history_matches_mirror = false;
    }

    update_navigation_action_state();
    dump_session_history(reason);
    return true;
}

void ViewImplementation::abandon_pending_web_content_session_history_seed()
{
    m_session_history_entry_url_loading_from_ui_process.clear();
    m_pending_web_content_session_history_seed.clear();
}

StringView ViewImplementation::pending_session_history_navigation_web_content_restore_mode_to_string(PendingSessionHistoryNavigation::WebContentRestoreMode mode)
{
    switch (mode) {
    case PendingSessionHistoryNavigation::WebContentRestoreMode::PreserveCurrentProcessState:
        return "preserve-current-process-state"sv;
    case PendingSessionHistoryNavigation::WebContentRestoreMode::RestoreFromUIProcess:
        return "restore-from-ui-process"sv;
    }
    VERIFY_NOT_REACHED();
}

StringView ViewImplementation::pending_session_history_traversal_stage_to_string(PendingSessionHistoryTraversal::Stage stage)
{
    switch (stage) {
    case PendingSessionHistoryTraversal::Stage::ApplyingInWebContent:
        return "applying-in-webcontent"sv;
    case PendingSessionHistoryTraversal::Stage::CheckingCancelation:
        return "checking-cancelation"sv;
    case PendingSessionHistoryTraversal::Stage::LoadingEntryFromUIProcess:
        return "loading-entry-from-ui-process"sv;
    case PendingSessionHistoryTraversal::Stage::ReplacingWebContentProcess:
        return "replacing-webcontent-process"sv;
    case PendingSessionHistoryTraversal::Stage::RestoringNestedStepAfterSeed:
        return "restoring-nested-step-after-seed"sv;
    }
    VERIFY_NOT_REACHED();
}

static Optional<size_t> current_top_level_history_entry_index_for_step(Vector<Web::HTML::SessionHistoryEntryDescriptor> const& entries, Optional<i32> current_step)
{
    if (!current_step.has_value())
        return {};

    Optional<size_t> current_entry_index;
    for (size_t i = 0; i < entries.size(); ++i) {
        if (entries[i].step > *current_step)
            break;
        current_entry_index = i;
    }
    return current_entry_index;
}

void ViewImplementation::did_start_webdriver_navigation(Badge<WebContentClient>, URL::URL const& url)
{
    m_webdriver_pending_navigation_url = url;
    m_webdriver_pending_navigation_completes_with_session_history_update = false;
}

void ViewImplementation::wait_for_webdriver_navigation_completion(Badge<WebContentClient>, Optional<u64> page_load_timeout, Function<void(Web::WebDriver::Response)> on_complete)
{
    if (!m_webdriver_pending_navigation_url.has_value()) {
        on_complete(JsonValue {});
        return;
    }

    auto request_id = m_next_webdriver_navigation_completion_request_id++;
    auto request = make<WebDriverNavigationCompletionRequest>();
    request->on_complete = move(on_complete);
    m_pending_webdriver_navigation_completion_requests.set(request_id, move(request));

    auto listener_id = add_navigation_listener({
        .on_load_start = nullptr,
        .on_load_finish = [this](URL::URL const& url) {
            complete_webdriver_pending_navigation_if_url_matches(url);
        },
    });
    m_pending_webdriver_navigation_completion_requests.get(request_id).value()->navigation_listener_id = listener_id;

    if (page_load_timeout.has_value()) {
        auto timer_interval = *page_load_timeout > NumericLimits<int>::max() ? NumericLimits<int>::max() : static_cast<int>(*page_load_timeout);
        auto timer = Core::Timer::create_single_shot(timer_interval, [this, request_id] {
            complete_webdriver_navigation_completion(request_id, Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::Timeout, "Navigation timed out"sv));
        });
        timer->start();
        m_pending_webdriver_navigation_completion_requests.get(request_id).value()->timer = move(timer);
    }
}

void ViewImplementation::complete_webdriver_navigation_completion(u64 request_id, Web::WebDriver::Response response)
{
    auto maybe_request = m_pending_webdriver_navigation_completion_requests.take(request_id);
    if (!maybe_request.has_value())
        return;

    auto request = maybe_request.release_value();
    remove_navigation_listener(request->navigation_listener_id);
    if (request->timer)
        request->timer->stop();
    request->on_complete(move(response));
}

void ViewImplementation::complete_webdriver_pending_navigation_if_url_matches(URL::URL const& url)
{
    if (m_webdriver_pending_navigation_url.has_value() && *m_webdriver_pending_navigation_url != url)
        return;

    m_webdriver_pending_navigation_url.clear();
    m_webdriver_pending_navigation_completes_with_session_history_update = false;

    Vector<u64> request_ids;
    request_ids.ensure_capacity(m_pending_webdriver_navigation_completion_requests.size());
    for (auto const& request : m_pending_webdriver_navigation_completion_requests)
        request_ids.unchecked_append(request.key);

    auto view_id = m_view_id;
    for (auto request_id : request_ids) {
        Core::EventLoop::current().deferred_invoke([view_id, request_id] {
            auto view = ViewImplementation::find_view_by_id(view_id);
            if (!view.has_value())
                return;
            view->complete_webdriver_navigation_completion(request_id, JsonValue {});
        });
    }
}

JsonValue ViewImplementation::webdriver_session_history() const
{
    JsonObject serialized;
    serialized.set("currentURL"sv, m_url.serialize());
    serialized.set("webContentProcessID"sv, client().pid());
    serialized.set("backButtonEnabled"sv, m_navigate_back_action->enabled());
    serialized.set("forwardButtonEnabled"sv, m_navigate_forward_action->enabled());
    serialized.set("webContentHistoryMatchesUI"sv, m_current_web_content_session_history_matches_mirror);
    serialized.set("loadingSessionHistoryEntryFromUI"sv, m_session_history_entry_url_loading_from_ui_process.has_value());
    serialized.set("waitingToSeedWebContent"sv, m_pending_web_content_session_history_seed.should_send_entries);
    serialized.set("waitingForWebContentSeedAck"sv, m_pending_web_content_session_history_seed.waiting_for_ack);
    serialized.set("ignoringWebContentUpdatesUntilSeed"sv, m_pending_web_content_session_history_seed.ignore_updates_until_seed);
    serialized.set("reseedAfterCurrentHistoryLoad"sv, m_pending_web_content_session_history_seed.should_reseed_after_current_history_load);
    serialized.set("hasOnlyTopLevelUsedSteps"sv, m_session_history.has_only_top_level_used_steps());
    serialized.set("webContentUsesUIStepCoordinates"sv, m_session_history.web_content_uses_ui_step_coordinates());
    auto web_content_known_entries = m_session_history.web_content_known_entries();
    auto web_content_known_used_steps = m_session_history.web_content_known_used_steps();
    auto web_content_current_step = m_session_history.web_content_current_step();
    Optional<size_t> web_content_current_step_index;
    if (web_content_current_step.has_value())
        web_content_current_step_index = web_content_known_used_steps.find_first_index(*web_content_current_step);
    serialized.set("webContentKnownEntries"sv, history_json_entries(web_content_known_entries, current_top_level_history_entry_index_for_step(web_content_known_entries, web_content_current_step)));
    serialized.set("webContentKnownUsedSteps"sv, history_json_steps(web_content_known_used_steps, web_content_current_step_index));
    if (web_content_current_step.has_value())
        serialized.set("webContentCurrentStep"sv, *web_content_current_step);
    else
        serialized.set("webContentCurrentStep"sv, JsonValue {});

    if (auto current_used_step_index = m_session_history.current_used_step_index(); current_used_step_index.has_value())
        serialized.set("currentUsedStepIndex"sv, *current_used_step_index);
    else
        serialized.set("currentUsedStepIndex"sv, JsonValue {});

    if (auto pending_step = m_pending_web_content_session_history_seed.step_after_loading_top_level_entry; pending_step.has_value())
        serialized.set("pendingWebContentHistoryStepAfterFallbackLoad"sv, *pending_step);
    else
        serialized.set("pendingWebContentHistoryStepAfterFallbackLoad"sv, JsonValue {});

    if (m_pending_session_history_navigation.has_value()) {
        JsonObject pending_navigation;
        pending_navigation.set("url"sv, m_pending_session_history_navigation->url.serialize());
        pending_navigation.set("webContentRestoreMode"sv, pending_session_history_navigation_web_content_restore_mode_to_string(m_pending_session_history_navigation->web_content_restore_mode));
        if (auto const* previous_current_entry = m_pending_session_history_navigation->previous_session_history.current_entry())
            pending_navigation.set("previousCurrentURL"sv, previous_current_entry->url.serialize());
        else
            pending_navigation.set("previousCurrentURL"sv, JsonValue {});
        serialized.set("pendingSessionHistoryNavigation"sv, move(pending_navigation));
    } else {
        serialized.set("pendingSessionHistoryNavigation"sv, JsonValue {});
    }

    if (m_pending_session_history_traversal.has_value()) {
        JsonObject pending_traversal;
        pending_traversal.set("targetStep"sv, m_pending_session_history_traversal->target_step);
        pending_traversal.set("targetStepIndex"sv, m_pending_session_history_traversal->target_step_index);
        pending_traversal.set("willChangeTopLevelEntry"sv, m_pending_session_history_traversal->will_change_top_level_entry);
        pending_traversal.set("willReplaceWebContentProcess"sv, m_pending_session_history_traversal->will_replace_web_content_process);
        pending_traversal.set("stage"sv, pending_session_history_traversal_stage_to_string(m_pending_session_history_traversal->stage));
        serialized.set("pendingSessionHistoryTraversal"sv, move(pending_traversal));
    } else {
        serialized.set("pendingSessionHistoryTraversal"sv, JsonValue {});
    }

    serialized.set("entries"sv, history_json_entries(m_session_history));
    serialized.set("usedSteps"sv, history_json_steps(m_session_history));
    return serialized;
}

String ViewImplementation::ui_process_session_history_for_testing(Badge<WebContentClient>) const
{
    return webdriver_session_history().serialized();
}

void ViewImplementation::update_navigation_action_state()
{
    m_navigate_back_action->set_enabled(m_session_history.can_go_back());
    m_navigate_forward_action->set_enabled(m_session_history.can_go_forward());
}

void ViewImplementation::seed_web_content_session_history_from_ui_process(AllowCurrentEntryReconstruction allow_current_entry_reconstruction)
{
    auto current_top_level_entry_index = m_session_history.current_top_level_entry_index();
    if (!current_top_level_entry_index.has_value()) {
        abandon_pending_web_content_session_history_seed();
        m_current_web_content_session_history_matches_mirror = false;
        m_session_history.forget_web_content_state();
        update_navigation_action_state();
        dump_session_history("skip-webcontent-session-history-seed-without-current-entry"sv);
        return;
    }

    auto entries = m_session_history.entries();
    if (entries.is_empty()) {
        abandon_pending_web_content_session_history_seed();
        m_current_web_content_session_history_matches_mirror = false;
        m_session_history.forget_web_content_state();
        update_navigation_action_state();
        dump_session_history("skip-webcontent-session-history-seed-without-entries"sv);
        return;
    }

    if (history_debug_enabled()) {
        dbgln("[History] UI seeds WebContent session history page={} pid={} current={} entries={}",
            page_id(),
            client().pid(),
            *current_top_level_entry_index,
            history_log_entries(entries, current_top_level_entry_index));
    }

    // NB: A fallback traversal or crash recovery can restore the current top-level document before WebContent has the
    //     UI-owned session history that surrounds it. Allow WebContent to reconstruct that current entry only while the
    //     UI process is actively restoring that authoritative history, including the follow-up child navigable step for
    //     nested history. After crash recovery there might not be a pending traversal object anymore, but a pending
    //     nested step still means the top-level document is only a staging point for restoring the current history
    //     step.
    auto is_restoring_traversal_target = m_pending_session_history_traversal.has_value()
        && (m_pending_session_history_traversal->stage == PendingSessionHistoryTraversal::Stage::LoadingEntryFromUIProcess
            || m_pending_session_history_traversal->stage == PendingSessionHistoryTraversal::Stage::ReplacingWebContentProcess
            || m_pending_session_history_traversal->stage == PendingSessionHistoryTraversal::Stage::RestoringNestedStepAfterSeed);
    auto allow_reconstructing_current_entry = is_restoring_traversal_target
        || m_pending_web_content_session_history_seed.step_after_loading_top_level_entry.has_value()
        || allow_current_entry_reconstruction == AllowCurrentEntryReconstruction::Yes;
    client().async_set_top_level_session_history(page_id(), move(entries), *current_top_level_entry_index, allow_reconstructing_current_entry);
    m_pending_web_content_session_history_seed.waiting_for_ack = true;
    m_pending_web_content_session_history_seed.should_send_entries = false;
    update_navigation_action_state();
    dump_session_history("sent-webcontent-session-history-seed"sv);
}

void ViewImplementation::prepare_to_seed_web_content_session_history_from_ui_process()
{
    m_current_web_content_session_history_matches_mirror = false;
    m_session_history.forget_web_content_state();
    m_pending_session_history_navigation.clear();
    m_pending_web_content_session_history_seed.clear();
    // NB: A fresh or repaired WebContent process reaches the current top-level session history entry after loading or
    //     reseeding m_url. If the traversable's current session history step is nested, finish restoration by
    //     traversing to that step after seeding the top-level entries.
    m_pending_web_content_session_history_seed.step_after_loading_top_level_entry = m_session_history.current_step_to_restore_after_loading_top_level_entry();
    m_pending_web_content_session_history_seed.should_send_entries = true;
    m_pending_web_content_session_history_seed.ignore_updates_until_seed = true;
}

void ViewImplementation::restore_current_session_history_entry_from_ui_process()
{
    m_webdriver_pending_navigation_url = m_url;
    if (!m_pending_web_content_session_history_seed.step_after_loading_top_level_entry.has_value()) {
        m_pending_web_content_session_history_seed.should_reseed_after_current_history_load = true;
        seed_web_content_session_history_from_ui_process();
    }
    load_current_session_history_entry_from_ui_process();
}

void ViewImplementation::load_current_session_history_entry_from_ui_process()
{
    auto const* current_entry = m_session_history.current_entry();
    if (!current_entry) {
        m_session_history_entry_url_loading_from_ui_process = m_url;
        client().async_load_url(page_id(), m_url, Web::Bindings::NavigationHistoryBehavior::Auto);
        return;
    }

    m_session_history_entry_url_loading_from_ui_process = current_entry->url;
    auto history_handling = m_pending_web_content_session_history_seed.waiting_for_ack || m_session_history.web_content_uses_ui_step_coordinates()
        ? Web::Bindings::NavigationHistoryBehavior::Replace
        : Web::Bindings::NavigationHistoryBehavior::Auto;
    client().async_load_url_with_document_resource(
        page_id(),
        current_entry->url,
        current_entry->document_state.resource,
        history_handling);
}

void ViewImplementation::load_session_history_traversal_target_from_ui_process(TraversableSessionHistory::TraversalTarget const& target, StringView dump_reason)
{
    if (!m_pending_session_history_traversal.has_value() || m_pending_session_history_traversal->target_step != target.target_step) {
        m_pending_session_history_traversal = PendingSessionHistoryTraversal {
            .target_step = target.target_step,
            .target_step_index = target.target_step_index,
            .will_change_top_level_entry = target.changes_top_level_entry,
            .will_replace_web_content_process = !is_url_suitable_for_same_process_navigation(m_url, target.target_top_level_entry->url),
            .stage = PendingSessionHistoryTraversal::Stage::LoadingEntryFromUIProcess,
            .on_cancelation_check_complete = nullptr,
        };
    } else {
        m_pending_session_history_traversal->stage = PendingSessionHistoryTraversal::Stage::LoadingEntryFromUIProcess;
    }

    auto previous_session_history = m_session_history;
    m_session_history.traverse_to(target.target_step_index);
    update_navigation_action_state();

    prepare_to_seed_web_content_session_history_from_ui_process();
    m_pending_session_history_navigation = PendingSessionHistoryNavigation {
        target.target_top_level_entry->url,
        move(previous_session_history),
    };
    m_webdriver_pending_navigation_url = target.target_top_level_entry->url;
    // NB: A UI-process fallback traversal is only fully observable once the replacement WebContent process has
    //     accepted the UI-owned history seed. Completing WebDriver at load finish would let tests, and callers doing
    //     immediate history inspection, observe the fresh process before it has consumed the authoritative history.
    m_webdriver_pending_navigation_completes_with_session_history_update = true;
    set_url(target.target_top_level_entry->url);
    dump_session_history(dump_reason);
    load_current_session_history_entry_from_ui_process();
}

NonnullRefPtr<Core::Promise<Empty>> ViewImplementation::reset_session_history_for_testing()
{
    m_pending_session_history_reset_for_testing = Core::Promise<Empty>::construct();
    client().async_reset_session_history_for_testing(page_id());
    return *m_pending_session_history_reset_for_testing;
}

void ViewImplementation::did_set_top_level_session_history(Badge<WebContentClient>, bool accepted, Vector<Web::HTML::SessionHistoryEntryDescriptor> entries, Vector<i32> used_steps, size_t current_used_step_index)
{
    if (!should_manage_session_history_in_ui_process())
        return;

    if (history_debug_enabled()) {
        dbgln("[History] UI received WebContent session history seed ack page={} pid={} accepted={} current_used_step={} entries={} used_steps={}",
            page_id(),
            client().pid(),
            accepted,
            current_used_step_index,
            history_log_entries(entries),
            history_log_steps(used_steps, current_used_step_index));
    }

    if (!m_pending_web_content_session_history_seed.waiting_for_ack) {
        dump_session_history("ignored-webcontent-session-history-seed-ack"sv);
        return;
    }

    if (!accepted) {
        // NB: WebContent can reject a stale UI-process seed without changing its live session history. In that case
        //     the ack carries WebContent's current snapshot, so converge the UI mirror from the same path used by
        //     normal WebContent history updates. If the snapshot itself is not usable, fall back to forgetting the
        //     WebContent state below and wait for the next repair point.
        if (adopt_web_content_session_history_after_rejected_seed(move(entries), move(used_steps), current_used_step_index)) {
            dump_session_history("webcontent-session-history-seed-rejected-with-current-snapshot"sv);
            return;
        }

        abandon_pending_web_content_session_history_seed();
        m_current_web_content_session_history_matches_mirror = false;
        m_session_history.forget_web_content_state();
        m_pending_session_history_traversal.clear();
        update_navigation_action_state();
        dump_session_history("webcontent-session-history-seed-rejected"sv);
        return;
    }

    if (!m_session_history.did_seed_web_content_from_ui_process(move(entries), move(used_steps), current_used_step_index)) {
        if (m_pending_web_content_session_history_seed.should_reseed_after_current_history_load) {
            m_pending_web_content_session_history_seed.waiting_for_ack = false;
            m_pending_web_content_session_history_seed.should_send_entries = true;
            m_pending_web_content_session_history_seed.ignore_updates_until_seed = true;
            m_current_web_content_session_history_matches_mirror = false;
            update_navigation_action_state();
            dump_session_history("webcontent-session-history-preload-seed-ack-mismatch"sv);
            return;
        }

        abandon_pending_web_content_session_history_seed();
        m_current_web_content_session_history_matches_mirror = false;
        m_session_history.forget_web_content_state();
        m_pending_session_history_traversal.clear();
        update_navigation_action_state();
        dump_session_history("webcontent-session-history-seed-ack-mismatch"sv);
        return;
    }

    m_pending_web_content_session_history_seed.waiting_for_ack = false;
    if (m_pending_web_content_session_history_seed.should_reseed_after_current_history_load) {
        m_pending_web_content_session_history_seed.should_send_entries = true;
        m_pending_web_content_session_history_seed.ignore_updates_until_seed = true;
        m_current_web_content_session_history_matches_mirror = false;
        update_navigation_action_state();
        dump_session_history("webcontent-session-history-preload-seed-ack"sv);
        return;
    }

    m_pending_web_content_session_history_seed.ignore_updates_until_seed = false;
    // NB: A seed ack can arrive while a top-level navigation is still blocked. Keep the mirror provisional until that
    //     navigation settles.
    m_current_web_content_session_history_matches_mirror = !m_pending_web_content_session_history_seed.step_after_loading_top_level_entry.has_value()
        && !m_pending_session_history_navigation.has_value();
    if (m_pending_web_content_session_history_seed.step_after_loading_top_level_entry.has_value()) {
        if (m_pending_session_history_traversal.has_value())
            m_pending_session_history_traversal->stage = PendingSessionHistoryTraversal::Stage::RestoringNestedStepAfterSeed;
        client().async_traverse_the_history_to_step(page_id(), *m_pending_web_content_session_history_seed.step_after_loading_top_level_entry);
    } else {
        auto is_waiting_for_history_step_cancelation_check = m_pending_session_history_traversal.has_value()
            && m_pending_session_history_traversal->stage == PendingSessionHistoryTraversal::Stage::CheckingCancelation;
        if (!is_waiting_for_history_step_cancelation_check) {
            m_pending_session_history_traversal.clear();
            if (!m_pending_session_history_navigation.has_value())
                complete_webdriver_pending_navigation_if_url_matches(m_url);
        }
    }

    update_navigation_action_state();
    dump_session_history("webcontent-session-history-seed-ack"sv);
}

void ViewImplementation::did_traverse_the_history_to_step(Badge<WebContentClient>, i32 step, bool step_was_available, Web::HTML::HistoryStepResult result)
{
    if (!should_manage_session_history_in_ui_process())
        return;

    if (!m_pending_web_content_session_history_seed.step_after_loading_top_level_entry.has_value()) {
        if (!m_pending_session_history_traversal.has_value() || m_pending_session_history_traversal->target_step != step) {
            dump_session_history("ignored-stale-webcontent-history-step-result"sv);
            return;
        }

        if (!step_was_available) {
            auto target = m_session_history.traversal_target_for_step(step);
            if (target.has_value()) {
                load_session_history_traversal_target_from_ui_process(*target, "webcontent-history-step-unavailable-fallback-load"sv);
                return;
            }

            m_current_web_content_session_history_matches_mirror = false;
            m_session_history.forget_web_content_state();
            m_pending_session_history_traversal.clear();
            update_navigation_action_state();
            dump_session_history("webcontent-history-step-unavailable"sv);
            return;
        }

        if (result != Web::HTML::HistoryStepResult::Applied) {
            if (m_webdriver_pending_navigation_url.has_value())
                m_webdriver_pending_navigation_url = m_url;
            m_webdriver_pending_navigation_completes_with_session_history_update = false;
            complete_webdriver_pending_navigation_if_url_matches(m_url);
            m_pending_session_history_traversal.clear();
            update_navigation_action_state();
            dump_session_history("webcontent-history-step-canceled"sv);
        } else {
            if (!m_session_history.did_apply_web_content_traversal_to_step(step)) {
                if (auto target = m_session_history.traversal_target_for_step(step); target.has_value()) {
                    load_session_history_traversal_target_from_ui_process(*target, "webcontent-history-step-applied-with-stale-mirror-fallback-load"sv);
                    return;
                }

                m_current_web_content_session_history_matches_mirror = false;
                m_session_history.forget_web_content_state();
                m_pending_session_history_traversal.clear();
                update_navigation_action_state();
                dump_session_history("webcontent-history-step-applied-without-ui-target"sv);
                return;
            }

            m_current_web_content_session_history_matches_mirror = true;
            if (auto const* current_entry = m_session_history.current_entry()) {
                auto current_url = current_entry->url;
                auto const url_changed = m_url != current_url;
                set_url(current_url);
                if (url_changed && on_url_change)
                    on_url_change(m_url);
                if (m_webdriver_pending_navigation_url.has_value() && *m_webdriver_pending_navigation_url != current_url)
                    m_webdriver_pending_navigation_url = current_url;
            }
            if (!m_pending_session_history_traversal->will_change_top_level_entry)
                complete_webdriver_pending_navigation_if_url_matches(m_url);
            m_pending_session_history_traversal.clear();
            update_navigation_action_state();
            dump_session_history("webcontent-history-step-applied"sv);
        }
        return;
    }

    if (*m_pending_web_content_session_history_seed.step_after_loading_top_level_entry != step) {
        dump_session_history("ignored-stale-webcontent-history-step-result"sv);
        return;
    }

    if (step_was_available && result == Web::HTML::HistoryStepResult::Applied) {
        m_pending_web_content_session_history_seed.step_after_loading_top_level_entry.clear();
        m_current_web_content_session_history_matches_mirror = m_session_history.did_restore_web_content_to_current_step(step);
        m_pending_session_history_traversal.clear();
        complete_webdriver_pending_navigation_if_url_matches(m_url);
        update_navigation_action_state();
        dump_session_history("webcontent-history-step-restored"sv);
    } else {
        auto reason = step_was_available ? "webcontent-pending-history-step-canceled"sv : "webcontent-history-step-unavailable"sv;
        if (restore_pending_session_history_navigation(reason))
            return;

        m_pending_web_content_session_history_seed.step_after_loading_top_level_entry.clear();
        m_current_web_content_session_history_matches_mirror = false;
        m_session_history.forget_web_content_state();
        m_pending_session_history_traversal.clear();
        update_navigation_action_state();
        dump_session_history(reason);
    }
}

void ViewImplementation::did_check_if_traverse_history_step_is_canceled(
    Badge<WebContentClient>, u64 request_id, i32 step, bool canceled)
{
    if (!should_manage_session_history_in_ui_process())
        return;

    if (!m_pending_session_history_traversal.has_value()
        || m_pending_session_history_traversal->stage != PendingSessionHistoryTraversal::Stage::CheckingCancelation
        || m_pending_session_history_traversal->cancelation_check_request_id != request_id
        || m_pending_session_history_traversal->target_step != step) {
        dump_session_history("ignored-stale-history-step-cancelation-check-result"sv);
        return;
    }

    if (canceled) {
        auto on_cancelation_check_complete = move(m_pending_session_history_traversal->on_cancelation_check_complete);
        if (m_webdriver_pending_navigation_url.has_value())
            m_webdriver_pending_navigation_url = m_url;
        m_webdriver_pending_navigation_completes_with_session_history_update = false;
        complete_webdriver_pending_navigation_if_url_matches(m_url);
        m_pending_session_history_traversal.clear();
        update_navigation_action_state();
        dump_session_history("traverse-fallback-canceled-by-webcontent"sv);
        if (on_cancelation_check_complete)
            on_cancelation_check_complete({
                .status = HistoryTraversalStatus::Canceled,
            });
        return;
    }

    auto target = m_session_history.traversal_target_for_step(step);
    if (!target.has_value()) {
        auto on_cancelation_check_complete = move(m_pending_session_history_traversal->on_cancelation_check_complete);
        m_current_web_content_session_history_matches_mirror = false;
        m_session_history.forget_web_content_state();
        m_pending_session_history_traversal.clear();
        update_navigation_action_state();
        dump_session_history("traverse-fallback-cancelation-check-without-ui-target"sv);
        if (on_cancelation_check_complete)
            on_cancelation_check_complete({
                .status = HistoryTraversalStatus::NoEntry,
            });
        return;
    }

    auto on_cancelation_check_complete = move(m_pending_session_history_traversal->on_cancelation_check_complete);
    if (on_cancelation_check_complete)
        on_cancelation_check_complete({
            .status = HistoryTraversalStatus::Started,
            .will_replace_web_content_process = m_pending_session_history_traversal->will_replace_web_content_process,
            .will_change_top_level_entry = m_pending_session_history_traversal->will_change_top_level_entry,
        });

    load_session_history_traversal_target_from_ui_process(*target, "traverse-fallback-load-after-cancelation-check"sv);
}

void ViewImplementation::did_reset_session_history_for_testing(Badge<WebContentClient>)
{
    auto promise = move(m_pending_session_history_reset_for_testing);
    m_session_history.clear();
    m_current_web_content_session_history_matches_mirror = false;
    m_pending_session_history_navigation.clear();
    m_pending_session_history_traversal.clear();
    m_session_history_entry_url_loading_from_ui_process.clear();
    abandon_pending_web_content_session_history_seed();
    m_webdriver_pending_navigation_url.clear();
    m_webdriver_pending_navigation_completes_with_session_history_update = false;
    update_navigation_action_state();

    if (promise)
        promise->resolve({});
}

void ViewImplementation::mark_web_content_session_history_stale_for_testing(Badge<WebContentClient>)
{
    m_current_web_content_session_history_matches_mirror = false;
    update_navigation_action_state();
    dump_session_history("marked-webcontent-session-history-stale-for-testing"sv);
}

void ViewImplementation::dump_session_history(StringView reason, SessionHistoryDumpMode mode) const
{
    if (mode == SessionHistoryDumpMode::IfDebuggingEnabled && !history_debug_enabled())
        return;

    auto web_content_known_used_steps = m_session_history.web_content_known_used_steps();
    auto web_content_current_step = m_session_history.web_content_current_step();
    Optional<size_t> web_content_current_step_index;
    if (web_content_current_step.has_value())
        web_content_current_step_index = web_content_known_used_steps.find_first_index(*web_content_current_step);
    auto web_content_known_entries = m_session_history.web_content_known_entries();
    auto web_content_current_top_level_entry_index = current_top_level_history_entry_index_for_step(web_content_known_entries, web_content_current_step);

    auto pending_navigation_url = "none"sv;
    auto pending_navigation_restore_mode = "none"sv;
    String pending_navigation_url_storage;
    if (m_pending_session_history_navigation.has_value()) {
        pending_navigation_url_storage = m_pending_session_history_navigation->url.serialize();
        pending_navigation_url = pending_navigation_url_storage.bytes_as_string_view();
        pending_navigation_restore_mode = pending_session_history_navigation_web_content_restore_mode_to_string(m_pending_session_history_navigation->web_content_restore_mode);
    }

    dbgln("[History] UI session history page={} pid={} reason={} url='{}' webcontent_matches={} webcontent_uses_ui_steps={} loading_from_ui={} waiting_to_seed={} waiting_for_seed_ack={} ignore_until_seed={} reseed_after_current_load={} pending_webcontent_step={} pending_navigation_url={} pending_navigation_restore={} pending_traversal_target={} pending_traversal_stage={} webcontent_current_step={} back={} forward={} entries={} webcontent_known_entries={} webcontent_known_used_steps={}",
        page_id(),
        client().pid(),
        reason,
        m_url,
        m_current_web_content_session_history_matches_mirror,
        m_session_history.web_content_uses_ui_step_coordinates(),
        m_session_history_entry_url_loading_from_ui_process.has_value(),
        m_pending_web_content_session_history_seed.should_send_entries,
        m_pending_web_content_session_history_seed.waiting_for_ack,
        m_pending_web_content_session_history_seed.ignore_updates_until_seed,
        m_pending_web_content_session_history_seed.should_reseed_after_current_history_load,
        m_pending_web_content_session_history_seed.step_after_loading_top_level_entry,
        pending_navigation_url,
        pending_navigation_restore_mode,
        m_pending_session_history_traversal.has_value() ? Optional<i32> { m_pending_session_history_traversal->target_step } : Optional<i32> {},
        m_pending_session_history_traversal.has_value() ? pending_session_history_traversal_stage_to_string(m_pending_session_history_traversal->stage) : "none"sv,
        web_content_current_step,
        m_navigate_back_action->enabled(),
        m_navigate_forward_action->enabled(),
        history_log_entries(m_session_history),
        history_log_entries(web_content_known_entries, web_content_current_top_level_entry_index),
        history_log_steps(web_content_known_used_steps, web_content_current_step_index));
}

void ViewImplementation::handle_web_content_process_crash(LoadErrorPage load_error_page)
{
    auto const headless_mode = Application::browser_options().headless_mode.has_value();

    if (!headless_mode) {
        dbgln("\033[31;1mWebContent process crashed!\033[0m Last page loaded: {}", m_url);
        dbgln("Consider raising an issue at https://github.com/LadybirdBrowser/ladybird/issues/new/choose");
    }

    ++m_crash_count;
    constexpr size_t max_reasonable_crash_count = 5U;
    if (m_crash_count >= max_reasonable_crash_count) {
        if (!headless_mode) {
            dbgln("WebContent has crashed {} times in quick succession! Not restarting...", m_crash_count);
            m_repeated_crash_timer->stop();
            return;
        }
        // In headless mode, always respawn - tests need a working WebContent for each test.
        // Reset the crash count so we can continue running tests.
        m_crash_count = 0;
    }
    m_repeated_crash_timer->restart();

    // In headless mode, respawn WebContent but skip the error page.
    if (headless_mode)
        load_error_page = LoadErrorPage::No;

    initialize_client();
    VERIFY(m_client_state.client);

    // Don't keep a stale backup bitmap around.
    m_backup_shared_image_buffer = nullptr;

    if (should_manage_session_history_in_ui_process()) {
        m_session_history_entry_url_loading_from_ui_process.clear();
        prepare_to_seed_web_content_session_history_from_ui_process();
    }

    handle_resize();

    if (load_error_page == LoadErrorPage::Yes) {
        auto escaped_url = escape_html_entities(m_url.serialize());

        StringBuilder builder;
        builder.appendff(ERROR_HTML_HEADER, NO_FALLBACK_FAVICON_LINK, CRASH_ERROR_SVG, "Ladybird flew off-course!"sv);
        builder.appendff("<p>The web page <a href=\"{}\">{}</a> has crashed.<br><br>You can reload the page to try again.</p>", escaped_url, escaped_url);
        builder.append(ERROR_HTML_FOOTER);
        load_crash_page_html(builder.string_view(), m_url);
    } else if (should_manage_session_history_in_ui_process()) {
        m_should_suppress_history_for_current_load = false;
        m_should_suppress_history_for_next_load = false;
        restore_current_session_history_entry_from_ui_process();
    }
}

void ViewImplementation::default_zoom_level_factor_changed()
{
    apply_zoom_for_current_host();
}

void ViewImplementation::zoom_per_host_changed(StringView host)
{
    if (current_host() != host)
        return;
    apply_zoom_for_current_host();
}

void ViewImplementation::languages_changed()
{
    auto const& languages = Application::settings().languages();
    client().async_set_preferred_languages(page_id(), languages);
}

void ViewImplementation::browsing_behavior_changed()
{
    auto const& browsing_behavior = Application::settings().browsing_behavior();
    client().async_set_browsing_behavior(page_id(), browsing_behavior);
}

void ViewImplementation::autoplay_settings_changed()
{
    auto const& autoplay_settings = Application::settings().autoplay_settings();
    auto const& web_content_options = Application::web_content_options();

    auto policy = autoplay_settings.policy;
    if (web_content_options.enable_autoplay == EnableAutoplay::Yes)
        policy = Web::HTML::AutoplayPolicy::AllowAudioAndVideo;

    client().async_set_autoplay_settings(page_id(), policy, autoplay_settings.site_filters.values());
}

void ViewImplementation::global_privacy_control_changed()
{
    auto global_privacy_control = Application::settings().global_privacy_control();
    client().async_set_enable_global_privacy_control(page_id(), global_privacy_control == GlobalPrivacyControl::Yes);
}

void ViewImplementation::bookmarks_changed()
{
    update_bookmark_action();
}

void ViewImplementation::update_bookmark_action()
{
    Application::the().update_bookmark_action_for_current_web_view();

    auto is_bookmarked = Application::bookmark_store().is_bookmarked(url());

    m_toggle_bookmark_action->set_tooltip(is_bookmarked ? "Remove bookmark"sv : "Bookmark this page"sv);
    m_toggle_bookmark_action->set_engaged(is_bookmarked);
}

static ErrorOr<LexicalPath> save_screenshot(Gfx::Bitmap const* bitmap)
{
    if (!bitmap)
        return Error::from_string_literal("Failed to take a screenshot");

    auto path = TRY([] -> ErrorOr<LexicalPath> {
        if (auto const& screenshot_path = Application::browser_options().screenshot_path; screenshot_path.has_value())
            return LexicalPath { *screenshot_path };

        auto file = AK::UnixDateTime::now().to_byte_string("screenshot-%Y-%m-%d-%H-%M-%S.png"sv);
        return Application::the().path_for_downloaded_file(file);
    }());

    auto encoded = TRY(Gfx::PNGWriter::encode(*bitmap));

    auto dump_file = TRY(Core::File::open(path.string(), Core::File::OpenMode::Write));
    TRY(dump_file->write_until_depleted(encoded));

    return path;
}

NonnullRefPtr<Core::Promise<LexicalPath>> ViewImplementation::take_screenshot(ScreenshotType type)
{
    auto promise = Core::Promise<LexicalPath>::construct();

    if (m_pending_screenshot) {
        // For simplicity, only allow taking one screenshot at a time for now. Revisit if we need
        // to allow spamming screenshot requests for some reason.
        promise->reject(Error::from_string_literal("A screenshot request is already in progress"));
        return promise;
    }

    switch (type) {
    case ScreenshotType::Visible: {
        Gfx::Bitmap const* visible_bitmap = nullptr;
        if (m_client_state.has_usable_bitmap) {
            VERIFY(m_client_state.front_bitmap.shared_image_buffer);
            visible_bitmap = m_client_state.front_bitmap.shared_image_buffer->bitmap().ptr();
        } else if (m_backup_shared_image_buffer) {
            visible_bitmap = m_backup_shared_image_buffer->bitmap().ptr();
        }
        if (visible_bitmap) {
            if (auto result = save_screenshot(visible_bitmap); result.is_error())
                promise->reject(result.release_error());
            else
                promise->resolve(result.release_value());
        }
        break;
    }

    case ScreenshotType::Full:
        m_pending_screenshot = promise;
        client().async_take_document_screenshot(page_id());
        break;
    }

    return promise;
}

NonnullRefPtr<Core::Promise<LexicalPath>> ViewImplementation::take_dom_node_screenshot(Web::UniqueNodeID node_id)
{
    auto promise = Core::Promise<LexicalPath>::construct();

    if (m_pending_screenshot) {
        // For simplicity, only allow taking one screenshot at a time for now. Revisit if we need
        // to allow spamming screenshot requests for some reason.
        promise->reject(Error::from_string_literal("A screenshot request is already in progress"));
        return promise;
    }

    m_pending_screenshot = promise;
    client().async_take_dom_node_screenshot(page_id(), node_id);

    return promise;
}

void ViewImplementation::did_receive_screenshot(Badge<WebContentClient>, Gfx::ShareableBitmap const& screenshot)
{
    VERIFY(m_pending_screenshot);

    if (auto result = save_screenshot(screenshot.bitmap()); result.is_error())
        m_pending_screenshot->reject(result.release_error());
    else
        m_pending_screenshot->resolve(result.release_value());

    m_pending_screenshot = nullptr;
}

NonnullRefPtr<Core::Promise<String>> ViewImplementation::request_internal_page_info(PageInfoType type)
{
    auto promise = Core::Promise<String>::construct();

    if (m_pending_info_request) {
        // For simplicity, only allow one info request at a time for now.
        promise->reject(Error::from_string_literal("A page info request is already in progress"));
        return promise;
    }

    m_pending_info_request = promise;
    client().async_request_internal_page_info(page_id(), type);

    return promise;
}

void ViewImplementation::did_receive_internal_page_info(Badge<WebContentClient>, PageInfoType, Optional<Core::AnonymousBuffer> const& info)
{
    VERIFY(m_pending_info_request);

    String info_string;
    if (!info.has_value()) {
        info_string = "(no page)"_string;
    } else {
        info_string = MUST(String::from_utf8(info->bytes()));
    }
    m_pending_info_request->resolve(move(info_string));
    m_pending_info_request = nullptr;
}

ErrorOr<LexicalPath> ViewImplementation::dump_gc_graph()
{
    auto promise = request_internal_page_info(PageInfoType::GCGraph);
    auto gc_graph_json = TRY(promise->await());

    LexicalPath path { Core::StandardPaths::tempfile_directory() };
    path = path.append(TRY(AK::UnixDateTime::now().to_string("gc-graph-%Y-%m-%d-%H-%M-%S.js"sv)));

    // Write as a .js file so gc-heap-explorer.html can load it via <script> tag (avoiding CORS issues with file:// URLs)
    auto dump_file = TRY(Core::File::open(path.string(), Core::File::OpenMode::Write));
    TRY(dump_file->write_until_depleted("var GC_GRAPH_DUMP = "sv.bytes()));
    TRY(dump_file->write_until_depleted(gc_graph_json.bytes()));
    TRY(dump_file->write_until_depleted(";\n"sv.bytes()));

    return path;
}

void ViewImplementation::set_user_style_sheet(String const& source)
{
    client().async_set_user_style(page_id(), source);
}

void ViewImplementation::initialize_context_menus()
{
    auto& application = Application::the();

    m_navigate_back_action = Action::create("Go Back"sv, ActionID::NavigateBack, [this]() {
        (void)traverse_the_history_by_delta(-1);
    });
    m_navigate_forward_action = Action::create("Go Forward"sv, ActionID::NavigateForward, [this]() {
        (void)traverse_the_history_by_delta(+1);
    });
    m_navigate_back_action->set_enabled(false);
    m_navigate_forward_action->set_enabled(false);

    m_toggle_bookmark_action = Action::create("Toggle Bookmark"sv, ActionID::ToggleBookmarkViaToolbar, [this]() {
        auto& bookmark_store = Application::bookmark_store();

        if (auto bookmark = bookmark_store.find_bookmark_by_url(url()); bookmark.has_value())
            bookmark_store.remove_item(bookmark->id);
        else
            bookmark_store.add_bookmark(url(), title().to_utf8(), favicon_base64_png());
    });
    update_bookmark_action();

    m_reset_zoom_action = Action::create("100%"sv, ActionID::ResetZoomViaToolbar, [this]() {
        reset_zoom();
    });
    m_reset_zoom_action->set_tooltip("Reset zoom level"sv);
    m_reset_zoom_action->set_visible(false);

    m_search_selected_text_action = Action::create("Search Selected Text"sv, ActionID::SearchSelectedText, [this]() {
        auto const& search_engine = Application::settings().search_engine();
        if (!search_engine.has_value())
            return;

        auto url_string = search_engine->format_search_query_for_navigation(*m_search_text);
        auto url = URL::Parser::basic_parse(url_string);
        VERIFY(url.has_value());

        Application::the().open_url_in_new_tab(*url, Web::HTML::ActivateTab::Yes);
    });
    m_search_selected_text_action->set_visible(false);

    auto take_and_save_screenshot = [this](auto type) {
        take_screenshot(type)
            ->when_resolved([](auto const& path) {
                Application::the().display_download_confirmation_dialog("Screenshot"sv, path);
            })
            .when_rejected([](auto const& error) {
                if (error.is_errno() && error.code() == ECANCELED)
                    return;

                auto error_message = MUST(String::formatted("{}", error));
                Application::the().display_error_dialog(error_message);
            });
    };

    m_take_visible_screenshot_action = Action::create("Take Visible Screenshot"sv, ActionID::TakeVisibleScreenshot, [take_and_save_screenshot]() {
        take_and_save_screenshot(ScreenshotType::Visible);
    });
    m_take_full_screenshot_action = Action::create("Take Full Screenshot"sv, ActionID::TakeFullScreenshot, [take_and_save_screenshot]() {
        take_and_save_screenshot(ScreenshotType::Full);
    });

    m_open_in_new_tab_action = Action::create("Open in New Tab"sv, ActionID::OpenInNewTab, [this]() {
        Application::the().open_url_in_new_tab(m_context_menu_url, Web::HTML::ActivateTab::No);
    });
    m_open_in_new_window_action = Action::create("Open in New Window"sv, ActionID::OpenInNewWindow, [this]() {
        Application::the().open_url_in_new_window(m_context_menu_url);
    });
    m_copy_url_action = Action::create("Copy URL"sv, ActionID::CopyURL, [this]() {
        Application::the().insert_clipboard_entry({ url_text_to_copy(m_context_menu_url), "text/plain"_string });
    });

    m_open_image_action = Action::create("Open Image"sv, ActionID::OpenImage, [this]() {
        load(m_context_menu_url);
    });
    m_save_image_action = Action::create("Save Image As..."sv, ActionID::SaveImage, [this]() {
        auto download_path = Application::the().path_for_downloaded_file(m_context_menu_url.basename());
        if (download_path.is_error())
            return;

        Application::the().file_downloader().download_file(m_context_menu_url, download_path.release_value());
    });
    m_copy_image_action = Action::create("Copy Image"sv, ActionID::CopyImage, [this]() {
        if (!m_image_context_menu_bitmap.has_value())
            return;

        auto bitmap = m_image_context_menu_bitmap.release_value();
        if (!bitmap.is_valid())
            return;

        auto encoded = Gfx::PNGWriter::encode(*bitmap.bitmap());
        if (encoded.is_error())
            return;

        Application::the().insert_clipboard_entry({ ByteString { encoded.value().bytes() }, "image/png"_string });
    });

    m_open_audio_action = Action::create("Open Audio"sv, ActionID::OpenAudio, [this]() {
        load(m_context_menu_url);
    });
    m_open_video_action = Action::create("Open Video"sv, ActionID::OpenVideo, [this]() {
        load(m_context_menu_url);
    });
    m_media_play_action = Action::create("Play"sv, ActionID::PlayMedia, [this]() {
        client().async_toggle_media_play_state(page_id());
    });
    m_media_pause_action = Action::create("Pause"sv, ActionID::PauseMedia, [this]() {
        client().async_toggle_media_play_state(page_id());
    });
    m_media_mute_action = Action::create("Mute"sv, ActionID::MuteMedia, [this]() {
        client().async_toggle_media_mute_state(page_id());
    });
    m_media_unmute_action = Action::create("Unmute"sv, ActionID::UnmuteMedia, [this]() {
        client().async_toggle_media_mute_state(page_id());
    });
    m_media_show_controls_action = Action::create("Show Controls"sv, ActionID::ShowControls, [this]() {
        client().async_toggle_media_controls_state(page_id());
    });
    m_media_hide_controls_action = Action::create("Hide Controls"sv, ActionID::HideControls, [this]() {
        client().async_toggle_media_controls_state(page_id());
    });
    m_media_loop_action = Action::create_checkable("Loop"sv, ActionID::ToggleMediaLoopState, [this]() {
        client().async_toggle_media_loop_state(page_id());
    });
    m_media_enter_fullscreen_action = Action::create("Full Screen"sv, ActionID::EnterFullscreen, [this]() {
        client().async_toggle_media_fullscreen_state(page_id());
    });
    m_media_exit_fullscreen_action = Action::create("Exit Full Screen"sv, ActionID::ExitFullscreen, [this]() {
        client().async_toggle_media_fullscreen_state(page_id());
    });

    m_page_context_menu = Menu::create("Page Context Menu"sv);
    m_page_context_menu->add_action(*m_navigate_back_action);
    m_page_context_menu->add_action(*m_navigate_forward_action);
    m_page_context_menu->add_action(application.reload_action());
    m_page_context_menu->add_separator();
    m_page_context_menu->add_action(application.cut_selection_action());
    m_page_context_menu->add_action(application.copy_selection_action());
    m_page_context_menu->add_action(application.paste_action());
    m_page_context_menu->add_action(application.select_all_action());
    m_page_context_menu->add_separator();
    m_page_context_menu->add_action(*m_search_selected_text_action);
    m_page_context_menu->add_separator();
    m_page_context_menu->add_action(*m_take_visible_screenshot_action);
    m_page_context_menu->add_action(*m_take_full_screenshot_action);
    m_page_context_menu->add_separator();
    m_page_context_menu->add_action(application.view_source_action());

    m_link_context_menu = Menu::create("Link Context Menu"sv);
    m_link_context_menu->add_action(*m_open_in_new_tab_action);
    m_link_context_menu->add_action(*m_open_in_new_window_action);
    m_link_context_menu->add_action(*m_copy_url_action);

    m_image_context_menu = Menu::create("Image Context Menu"sv);
    m_image_context_menu->add_action(*m_open_image_action);
    m_image_context_menu->add_action(*m_open_in_new_tab_action);
    m_image_context_menu->add_separator();
    m_image_context_menu->add_action(*m_save_image_action);
    m_image_context_menu->add_separator();
    m_image_context_menu->add_action(*m_copy_image_action);
    m_image_context_menu->add_action(*m_copy_url_action);

    m_media_context_menu = Menu::create("Media Context Menu"sv);
    m_media_context_menu->add_action(*m_media_play_action);
    m_media_context_menu->add_action(*m_media_pause_action);
    m_media_context_menu->add_action(*m_media_mute_action);
    m_media_context_menu->add_action(*m_media_unmute_action);
    m_media_context_menu->add_action(*m_media_show_controls_action);
    m_media_context_menu->add_action(*m_media_hide_controls_action);
    m_media_context_menu->add_action(*m_media_loop_action);
    m_media_context_menu->add_action(*m_media_enter_fullscreen_action);
    m_media_context_menu->add_action(*m_media_exit_fullscreen_action);
    m_media_context_menu->add_separator();
    m_media_context_menu->add_action(*m_open_audio_action);
    m_media_context_menu->add_action(*m_open_video_action);
    m_media_context_menu->add_action(*m_open_in_new_tab_action);
    m_media_context_menu->add_separator();
    m_media_context_menu->add_action(*m_copy_url_action);
}

void ViewImplementation::did_request_page_context_menu(Badge<WebContentClient>, Gfx::IntPoint content_position, Web::ContextMenuForInputEventsTarget for_input_events_target)
{
    auto& cut_selection_action = Application::the().cut_selection_action();
    cut_selection_action.set_visible(for_input_events_target == Web::ContextMenuForInputEventsTarget::Yes);

    auto const& search_engine = Application::settings().search_engine();
    m_search_text = search_engine.has_value() ? selected_text_with_whitespace_collapsed() : OptionalNone {};

    ScopeGuard guard { [&]() {
        cut_selection_action.set_visible(true);
        m_search_text.clear();
    } };

    if (m_search_text.has_value()) {
        m_search_selected_text_action->set_text(search_engine->format_search_query_for_display(*m_search_text));
        m_search_selected_text_action->set_visible(true);
    } else {
        m_search_selected_text_action->set_visible(false);
    }

    if (m_page_context_menu->on_activation)
        m_page_context_menu->on_activation(to_widget_position(content_position));
}

void ViewImplementation::did_request_link_context_menu(Badge<WebContentClient>, Gfx::IntPoint content_position, URL::URL url)
{
    m_context_menu_url = move(url);

    m_open_in_new_tab_action->set_text("Open in New Tab"sv);

    switch (url_type(m_context_menu_url)) {
    case URLType::Email:
        m_copy_url_action->set_text("Copy Email Address"sv);
        break;
    case URLType::Telephone:
        m_copy_url_action->set_text("Copy Phone Number"sv);
        break;
    case URLType::Other:
        m_copy_url_action->set_text("Copy Link Address"sv);
        break;
    }

    if (m_link_context_menu->on_activation)
        m_link_context_menu->on_activation(to_widget_position(content_position));
}

void ViewImplementation::did_request_image_context_menu(Badge<WebContentClient>, Gfx::IntPoint content_position, URL::URL url, Optional<Gfx::ShareableBitmap> bitmap)
{
    m_context_menu_url = move(url);
    m_image_context_menu_bitmap = move(bitmap);

    m_open_in_new_tab_action->set_text("Open Image in New Tab"sv);
    m_copy_url_action->set_text("Copy Image URL"sv);

    m_copy_image_action->set_enabled(m_image_context_menu_bitmap.has_value());

    if (m_image_context_menu->on_activation)
        m_image_context_menu->on_activation(to_widget_position(content_position));
}

void ViewImplementation::did_request_media_context_menu(Badge<WebContentClient>, Gfx::IntPoint content_position, Web::Page::MediaContextMenu menu)
{
    m_context_menu_url = move(menu.media_url);

    m_open_in_new_tab_action->set_text(menu.is_video ? "Open Video in New Tab"sv : "Open Audio in new Tab"sv);
    m_copy_url_action->set_text(menu.is_video ? "Copy Video URL"sv : "Copy Audio URL"sv);

    m_open_audio_action->set_visible(!menu.is_video);
    m_open_video_action->set_visible(menu.is_video);

    m_media_play_action->set_visible(!menu.is_playing);
    m_media_pause_action->set_visible(menu.is_playing);

    m_media_mute_action->set_visible(!menu.is_muted);
    m_media_unmute_action->set_visible(menu.is_muted);

    m_media_show_controls_action->set_visible(!menu.has_user_agent_controls);
    m_media_hide_controls_action->set_visible(menu.has_user_agent_controls);

    m_media_loop_action->set_checked(menu.is_looping);

    m_media_enter_fullscreen_action->set_visible(menu.is_video && !menu.is_fullscreen);
    m_media_exit_fullscreen_action->set_visible(menu.is_video && menu.is_fullscreen);

    if (m_media_context_menu->on_activation)
        m_media_context_menu->on_activation(to_widget_position(content_position));
}

u64 ViewImplementation::add_navigation_listener(NavigationListener listener)
{
    auto id = m_next_navigation_listener_id++;
    m_navigation_listeners.set(id, move(listener));
    return id;
}

void ViewImplementation::remove_navigation_listener(u64 listener_id)
{
    m_navigation_listeners.remove(listener_id);
}

void ViewImplementation::request_close()
{
    if (needs_beforeunload_check()) {
        client().async_request_close(page_id());
        return;
    }

    client().request_close(page_id());
}

Function<void()> ViewImplementation::prepare_for_immediate_close()
{
    VERIFY(!needs_beforeunload_check());

    auto client = m_client_state.client;
    auto page_id = m_client_state.page_index;
    client->prepare_for_detached_close(page_id);
    return [client = move(client), page_id] {
        client->async_request_close(page_id);
    };
}

}
