/*
 * Copyright (c) 2021, Andreas Kling <andreas@ladybird.org>
 * Copyright (c) 2022, the SerenityOS developers.
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <AK/Debug.h>
#include <AK/TemporaryChange.h>
#include <LibCore/EventLoop.h>
#include <LibJS/Runtime/VM.h>
#include <LibWeb/Animations/ScrollTimeline.h>
#include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/CSS/FontComputer.h>
#include <LibWeb/CSS/FontFaceSet.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/EventLoop/EventLoop.h>
#include <LibWeb/HTML/HTMLMediaElement.h>
#include <LibWeb/HTML/Scripting/Agent.h>
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
#include <LibWeb/HTML/TraversableNavigable.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/HighResolutionTime/Performance.h>
#include <LibWeb/HighResolutionTime/TimeOrigin.h>
#include <LibWeb/IndexedDB/Internal/Algorithms.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/Painting/PaintableBox.h>
#include <LibWeb/Painting/ViewportPaintable.h>
#include <LibWeb/Platform/EventLoopPlugin.h>
#include <LibWeb/Platform/Timer.h>

namespace Web::HTML {

GC_DEFINE_ALLOCATOR(EventLoop);

EventLoop::EventLoop(Type type)
    : m_type(type)
{
    m_task_queue = heap().allocate<TaskQueue>(*this);

    m_rendering_task_function = GC::create_function(heap(), [this] {
        update_the_rendering();
    });
}

EventLoop::~EventLoop() = default;

void EventLoop::visit_edges(Visitor& visitor)
{
    Base::visit_edges(visitor);
    visitor.visit(m_reached_step_1_tasks);
    visitor.visit(m_task_queue);
    m_microtask_queue.for_each([&](auto& task) { visitor.visit(task); });
    visitor.visit(m_currently_running_task);
    visitor.visit(m_backup_incumbent_realm_stack);
    visitor.visit(m_rendering_task_function);
    visitor.visit(m_system_event_loop_timer);
}

void EventLoop::schedule()
{
    if (!m_system_event_loop_timer) {
        m_system_event_loop_timer = Platform::Timer::create_single_shot(heap(), 0, GC::create_function(heap(), [this] {
            process();
        }));
    }

    if (!m_system_event_loop_timer->is_active())
        m_system_event_loop_timer->restart();
}

EventLoop& main_thread_event_loop()
{
    return *static_cast<HTML::Agent*>(Bindings::main_thread_vm().agent())->event_loop;
}

// https://html.spec.whatwg.org/multipage/webappapis.html#spin-the-event-loop
void EventLoop::spin_until(GC::Ref<GC::Function<bool()>> goal_condition)
{
    // FIXME: The spec wants us to do the rest of the enclosing algorithm (i.e. the caller)
    //    in the context of the currently running task on entry. That's not possible with this implementation.
    // 1. Let task be the event loop's currently running task.
    // 2. Let task source be task's source.

    // 3. Let old stack be a copy of the JavaScript execution context stack.
    // 4. Empty the JavaScript execution context stack.
    auto& vm = this->vm();
    vm.save_execution_context_stack();
    vm.clear_execution_context_stack();

    // 5. Perform a microtask checkpoint.
    perform_a_microtask_checkpoint();

    // 6. In parallel:
    //    1. Wait until the condition goal is met.
    //    2. Queue a task on task source to:
    //       1. Replace the JavaScript execution context stack with old stack.
    //       2. Perform any steps that appear after this spin the event loop instance in the original algorithm.
    //       NOTE: This is achieved by returning from the function.

    Platform::EventLoopPlugin::the().spin_until(GC::create_function(heap(), [this, goal_condition] {
        if (goal_condition->function()())
            return true;
        if (m_task_queue->has_runnable_tasks()) {
            schedule();
            // FIXME: Remove the platform event loop plugin so that this doesn't look out of place
            Core::EventLoop::current().wake();
        }
        return goal_condition->function()();
    }));

    vm.restore_execution_context_stack();

    // 7. Stop task, allowing whatever algorithm that invoked it to resume.
    // NOTE: This is achieved by returning from the function.
}

// https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model
void EventLoop::process()
{
    // 1. Let oldestTask and taskStartTime be null.
    GC::Ptr<Task> oldest_task;
    [[maybe_unused]] double task_start_time = 0;

    // Some algorithms request that steps or states only occur once the event loop has reached step 1.
    // Invoke a set of tasks that these algorithms request us to in order to achieve this.
    auto reached_step_1_tasks = move(m_reached_step_1_tasks);
    for (auto& reached_step_1_task : reached_step_1_tasks)
        reached_step_1_task->function()();

    // 2. If the event loop has a task queue with at least one runnable task, then:
    if (m_task_queue->has_runnable_tasks()) {
        // 1. Let taskQueue be one such task queue, chosen in an implementation-defined manner.
        auto task_queue = m_task_queue;

        // 2. Set taskStartTime to the unsafe shared current time.
        task_start_time = HighResolutionTime::unsafe_shared_current_time();

        // 3. Set oldestTask to the first runnable task in taskQueue, and remove it from taskQueue.
        oldest_task = task_queue->take_first_runnable();

        // FIXME: 4. If oldestTask's document is not null, then record task start time given taskStartTime and oldestTask's document.

        // 5. Set the event loop's currently running task to oldestTask.
        m_currently_running_task = oldest_task.ptr();

        // 6. Perform oldestTask's steps.
        oldest_task->execute();

        // 7. Set the event loop's currently running task back to null.
        m_currently_running_task = nullptr;

        // 8. Perform a microtask checkpoint.
        perform_a_microtask_checkpoint();
    }

    // 3. Let taskEndTime be the unsafe shared current time. [HRT]
    [[maybe_unused]] auto task_end_time = HighResolutionTime::unsafe_shared_current_time();

    // 4. If oldestTask is not null, then:
    if (oldest_task) {
        // FIXME: 1. Let top-level browsing contexts be an empty set.
        // FIXME: 2. For each environment settings object settings of oldestTask's script evaluation environment settings object set:
        // FIXME: 2.1. Let global be settings's global object.
        // FIXME: 2.2. If global is not a Window object, then continue.
        // FIXME: 2.3. If global's browsing context is null, then continue.
        // FIXME: 2.4. Let tlbc be global's browsing context's top-level browsing context.
        // FIXME: 2.5. If tlbc is not null, then append it to top-level browsing contexts.
        // FIXME: 3. Report long tasks, passing in taskStartTime, taskEndTime, top-level browsing contexts, and oldestTask.
        // FIXME: 4. If oldestTask's document is not null, then record task end time given taskEndTime and oldestTask's document.
    }

    // 5. If this is a window event loop that has no runnable task in this event loop's task queues, then:
    if (m_type == Type::Window && !m_task_queue->has_runnable_tasks()) {
        // 1. Set this event loop's last idle period start time to the unsafe shared current time.
        m_last_idle_period_start_time = HighResolutionTime::unsafe_shared_current_time();

        // 2. Let computeDeadline be the following steps:
        // Implemented in EventLoop::compute_deadline()

        // 3. For each win of the same-loop windows for this event loop, perform the start an idle period algorithm for win with the following step: return the result of calling computeDeadline, coarsened given win's relevant settings object's cross-origin isolated capability. [REQUESTIDLECALLBACK]
        for (auto& win : same_loop_windows()) {
            win->start_an_idle_period();
        }
    }

    // If there are eligible tasks in the queue, schedule a new round of processing. :^)
    if (m_task_queue->has_runnable_tasks() || (!m_microtask_queue.is_empty() && !m_performing_a_microtask_checkpoint)) {
        schedule();
    }
}

// https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model
void EventLoop::queue_task_to_update_the_rendering()
{
    // FIXME: 1. Wait until at least one navigable whose active document's relevant agent's event loop is eventLoop might have a rendering opportunity.

    // 2. Set eventLoop's last render opportunity time to the unsafe shared current time.
    m_last_render_opportunity_time = HighResolutionTime::unsafe_shared_current_time();

    // OPTIMIZATION: If there are already rendering tasks in the queue, we don't need to queue another one.
    if (m_task_queue->has_rendering_tasks()) {
        return;
    }

    // 3. For each navigable that has a rendering opportunity, queue a global task on the rendering task source given navigable's active window to update the rendering:
    for (auto& navigable : all_navigables()) {
        if (!navigable->is_traversable())
            continue;
        if (!navigable->has_a_rendering_opportunity())
            continue;

        auto document = navigable->active_document();
        if (!document)
            continue;
        if (document->is_decoded_svg())
            continue;

        queue_global_task(Task::Source::Rendering, *navigable->active_window(), *m_rendering_task_function);
    }
}

void EventLoop::process_input_events() const
{
    auto process_input_events_queue = [&](Page& page) {
        auto& page_client = page.client();
        auto& input_events_queue = page_client.input_event_queue();

        // Process events only for this page, collecting others to re-enqueue
        Queue<Web::QueuedInputEvent> events_for_other_pages;

        while (!input_events_queue.is_empty()) {
            auto event = input_events_queue.dequeue();

            // Skip events that are not intended for this page
            if (event.page_id != page_client.id()) {
                events_for_other_pages.enqueue(move(event));
                continue;
            }

            auto result = event.event.visit(
                [&](KeyEvent const& key_event) {
                    switch (key_event.type) {
                    case KeyEvent::Type::KeyDown:
                        return page.handle_keydown(key_event.key, key_event.modifiers, key_event.code_point, key_event.repeat);
                    case KeyEvent::Type::KeyUp:
                        return page.handle_keyup(key_event.key, key_event.modifiers, key_event.code_point, key_event.repeat);
                    }
                    VERIFY_NOT_REACHED();
                },
                [&](MouseEvent const& mouse_event) {
                    switch (mouse_event.type) {
                    case MouseEvent::Type::MouseDown:
                        return page.handle_mousedown(mouse_event.position, mouse_event.screen_position, mouse_event.button, mouse_event.buttons, mouse_event.modifiers, mouse_event.click_count);
                    case MouseEvent::Type::MouseUp:
                        return page.handle_mouseup(mouse_event.position, mouse_event.screen_position, mouse_event.button, mouse_event.buttons, mouse_event.modifiers);
                    case MouseEvent::Type::MouseMove:
                        return page.handle_mousemove(mouse_event.position, mouse_event.screen_position, mouse_event.buttons, mouse_event.modifiers);
                    case MouseEvent::Type::MouseLeave:
                        return page.handle_mouseleave();
                    case MouseEvent::Type::MouseWheel:
                        if (mouse_event.async_scroll_performed_default_action) {
                            dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Main thread handling DOM wheel after async default action");
                            return page.handle_mousewheel(mouse_event.position, mouse_event.screen_position, mouse_event.button, mouse_event.buttons, mouse_event.modifiers, mouse_event.wheel_delta_x, mouse_event.wheel_delta_y, true);
                        }
                        return page.handle_mousewheel(mouse_event.position, mouse_event.screen_position, mouse_event.button, mouse_event.buttons, mouse_event.modifiers, mouse_event.wheel_delta_x, mouse_event.wheel_delta_y);
                    }
                    VERIFY_NOT_REACHED();
                },
                [&](Web::DragEvent& drag_event) {
                    return page.handle_drag_and_drop_event(drag_event.type, drag_event.position, drag_event.screen_position, drag_event.button, drag_event.buttons, drag_event.modifiers, move(drag_event.files));
                },
                [&](Web::PinchEvent& pinch_event) {
                    return page.handle_pinch_event(pinch_event.position, pinch_event.scale_delta);
                });

            for (size_t i = 0; i < event.coalesced_event_count; ++i)
                page_client.report_finished_handling_input_event(event.page_id, EventResult::Dropped);
            page_client.report_finished_handling_input_event(event.page_id, result);
        }

        // Re-enqueue events for other pages
        while (!events_for_other_pages.is_empty()) {
            input_events_queue.enqueue(events_for_other_pages.dequeue());
        }

        page.handle_sdl_input_events();
    };

    auto documents_of_traversable_navigables = documents_in_this_event_loop_matching([&](auto const& document) {
        if (document.is_decoded_svg())
            return false;
        if (!document.navigable())
            return false;
        if (!document.navigable()->is_traversable())
            return false;
        return true;
    });

    for (auto const& document : documents_of_traversable_navigables) {
        process_input_events_queue(document->page());
    }
}

// https://html.spec.whatwg.org/multipage/webappapis.html#update-the-rendering
void EventLoop::update_the_rendering()
{
    VERIFY(!m_running_rendering_task);
    m_running_rendering_task = true;
    ScopeGuard const guard = [this] {
        m_running_rendering_task = false;
    };

    process_input_events();

    // 1. Let frameTimestamp be eventLoop's last render opportunity time.
    auto frame_timestamp = m_last_render_opportunity_time;

    // 2. Let docs be all fully active Document objects whose relevant agent's event loop is
    //    eventLoop, sorted arbitrarily except that the following conditions must be met:
    //    - Any Document B whose container document is A must be listed after A in the list.
    //    - If there are two documents A and B that both have the same non-null container document
    //      C, then the order of A and B in the list must match the shadow-including tree order
    //      of their respective navigable containers in C's node tree.
    // 3. Filter non-renderable documents: Remove from docs any Document object doc for which any of the following are true:
    auto docs = documents_in_this_event_loop_matching([&](auto const& document) {
        if (!document.is_fully_active())
            return false;

        // doc is render-blocked;
        if (document.is_render_blocked()) {
            return false;
        }

        // doc's visibility state is "hidden";
        if (document.hidden())
            return false;

        // doc's rendering is suppressed for view transitions; or
        if (document.rendering_suppression_for_view_transitions())
            return false;

        auto navigable = document.navigable();
        if (!navigable)
            return false;

        // doc's node navigable doesn't currently have a rendering opportunity.
        if (!navigable->has_a_rendering_opportunity())
            return false;

        return true;
    });

    // AD-HOC: Update all the displayed video frames on HTMLMediaElements in documents' pages.
    for (auto& document : docs)
        document->page().update_all_media_element_video_sinks();

    // FIXME: 4. Unnecessary rendering: Remove from docs any Document object doc for which all of the following are true:

    // FIXME: 5. Remove from docs all Document objects for which the user agent believes that it's preferable to skip updating the rendering for other reasons.

    // FIXME: 6. For each doc of docs, reveal doc.

    // 7. For each doc of docs, flush autofocus candidates for doc if its node navigable is a top-level traversable.
    for (auto& document : docs) {
        auto navigable = document->navigable();
        if (navigable && navigable->is_top_level_traversable())
            document->flush_autofocus_candidates();
    }

    // 8. For each doc of docs, run the resize steps for doc. [CSSOMVIEW]
    for (auto& document : docs) {
        document->run_the_resize_steps();
    }

    // 9. For each doc of docs, run the scroll steps for doc. [CSSOMVIEW]
    for (auto& document : docs) {
        if (auto navigable = document->navigable())
            navigable->adopt_pending_async_scroll_offsets();
        document->run_the_scroll_steps();
    }

    // 10. For each doc of docs, evaluate media queries and report changes for doc. [CSSOMVIEW]
    for (auto& document : docs) {
        document->evaluate_media_queries_and_report_changes();
    }

    // 11. For each doc of docs, update animations and send events for doc, passing in relative high resolution time given frameTimestamp and doc's relevant global object as the timestamp [WEBANIMATIONS]
    for (auto& document : docs) {
        document->update_animations_and_send_events(HighResolutionTime::relative_high_resolution_time(frame_timestamp, relevant_global_object(*document)));
    };

    // 12. For each doc of docs, run the fullscreen steps for doc. [FULLSCREEN]
    for (auto& document : docs) {
        document->run_fullscreen_steps();
    }

    // FIXME: 13. For each doc of docs, if the user agent detects that the backing storage associated with a CanvasRenderingContext2D or an OffscreenCanvasRenderingContext2D, context, has been lost, then it must run the context lost steps for each such context:

    // 14. For each doc of docs, run the animation frame callbacks for doc, passing in the relative high resolution time given frameTimestamp and doc's relevant global object as the timestamp.
    for (auto& document : docs) {
        auto now = HighResolutionTime::relative_high_resolution_time(frame_timestamp, relevant_global_object(*document));
        run_animation_frame_callbacks(*document, now);
    }

    // FIXME: 15. Let unsafeStyleAndLayoutStartTime be the unsafe shared current time.

    // 16. For each doc of docs:
    for (auto& document : docs) {
        // 1. Let resizeObserverDepth be 0.
        size_t resize_observer_depth = 0;

        // 2. While true:
        while (true) {
            // 1. Recalculate styles and update layout for doc.
            // NOTE: Recalculation of styles is handled by update_layout()
            document->update_layout(DOM::UpdateLayoutReason::HTMLEventLoopRenderingUpdate);

            // Clamp viewport scroll offset to valid range after layout, in case the
            // scrollable overflow area has shrunk (e.g. after a viewport size change).
            if (auto navigable = document->navigable())
                navigable->clamp_viewport_scroll_offset();

            // 2. Let hadInitialVisibleContentVisibilityDetermination be false.
            bool had_initial_visible_content_visibility_determination = false;

            // 3. For each element element with 'auto' used value of 'content-visibility':
            auto* document_element = document->document_element();
            if (document_element) {
                for (auto& paintable_box : document->paintable()->paintable_boxes_with_auto_content_visibility()) {
                    auto& element = as<DOM::Element>(*paintable_box->dom_node());

                    // 1. Let checkForInitialDetermination be true if element's proximity to the viewport is not determined and it is not relevant to the user. Otherwise, let checkForInitialDetermination be false.
                    bool check_for_initial_determination = element.proximity_to_the_viewport() == Web::DOM::ProximityToTheViewport::NotDetermined && !element.is_relevant_to_the_user();

                    // 2. Determine proximity to the viewport for element.
                    element.determine_proximity_to_the_viewport();

                    // 3. If checkForInitialDetermination is true and element is now relevant to the user, then set hadInitialVisibleContentVisibilityDetermination to true.
                    if (check_for_initial_determination && element.is_relevant_to_the_user()) {
                        had_initial_visible_content_visibility_determination = true;
                    }
                }
            }

            // 4. If hadInitialVisibleContentVisibilityDetermination is true, then continue.
            if (had_initial_visible_content_visibility_determination)
                continue;

            // 5. Gather active resize observations at depth resizeObserverDepth for doc.
            document->gather_active_observations_at_depth(resize_observer_depth);

            // 6. If doc has active resize observations:
            if (document->has_active_resize_observations()) {
                // 1. Set resizeObserverDepth to the result of broadcasting active resize observations given doc.
                resize_observer_depth = document->broadcast_active_resize_observations();

                // 2. Continue.
                continue;
            }

            // 7. Otherwise, break.
            break;
        }

        // 3. If doc has skipped resize observations, then deliver resize loop error given doc.
        if (document->has_skipped_resize_observations()) {
            // FIXME: Deliver resize loop error.
        }

        // https://drafts.csswg.org/scroll-animations-1/#event-loop
        // During step 7.14.1 of the HTML Processing Model, any created scroll progress timelines or view progress
        // timelines are collected into a stale timelines set. After step 7.14 if any timelines' named timeline ranges
        // have changed, these timelines are added to the stale timelines set. If there are any stale timelines they now
        // update their current time and associated ranges, the set of stale timelines is cleared and we run an additional
        // step to recalculate styles and update layout.

        // AD-HOC: This was step 7.14.1 at the time the CSS web-animations spec was written, but it has since been
        //         moved, see https://github.com/w3c/csswg-drafts/issues/12120

        bool requires_style_and_layout_update = false;

        TemporaryExecutionContext context { document->realm() };

        for (auto const& timeline : document->associated_animation_timelines()) {
            auto* scroll_timeline = as_if<Animations::ScrollTimeline>(*timeline);

            if (!scroll_timeline)
                continue;

            if (!scroll_timeline->is_stale())
                continue;

            // NB: The passed timestamp is ignored for ScrollTimelines so we can just use 0.
            timeline->update_current_time(0);
            requires_style_and_layout_update = true;
        }

        if (requires_style_and_layout_update)
            document->update_layout(DOM::UpdateLayoutReason::HTMLEventLoopRenderingUpdate);
    }

    // FIXME: 17. For each doc of docs, if the focused area of doc is not a focusable area, then run the focusing steps for doc's viewport, and set doc's relevant global object's navigation API's focus changed during ongoing navigation to false.

    // 18. For each doc of docs, perform pending transition operations for doc. [CSSVIEWTRANSITIONS]
    for (auto& document : docs) {
        document->perform_pending_transition_operations();
    }

    // 19. For each doc of docs, run the update intersection observations steps for doc, passing in the relative high resolution time given now and doc's relevant global object as the timestamp. [INTERSECTIONOBSERVER]
    for (auto& document : docs) {
        // NB: Layout may have been invalidated by previous steps (e.g. view transitions at step 18).
        //     Re-run layout here since intersection observations need up-to-date geometry.
        document->update_layout(DOM::UpdateLayoutReason::HTMLEventLoopRenderingUpdate);

        auto now = HighResolutionTime::relative_high_resolution_time(frame_timestamp, relevant_global_object(*document));
        document->run_the_update_intersection_observations_steps(now);
    }

    // FIXME: 20. For each doc of docs, record rendering time for doc given unsafeStyleAndLayoutStartTime.

    // FIXME: 21. For each doc of docs, mark paint timing for doc.

    // AD-HOC: Present all canvas element surfaces in documents' pages after callbacks
    // have had a chance to update them, and before painting snapshots the frame.
    for (auto& document : docs)
        document->page().present_all_canvas_element_surfaces();

    // 22. For each doc of docs, update the rendering or user interface of doc and its node navigable to reflect the current state.
    for (auto& doc : docs.in_reverse()) {
        auto navigable = doc->navigable();
        if (!navigable->needs_repaint())
            continue;
        // OPTIMIZATION: Don't paint navigables hidden by an ancestor iframe with visibility: hidden.
        //               needs_repaint() stays true — so, once the navigable becomes visible, it's painted.
        if (navigable->has_inclusive_ancestor_with_visibility_hidden())
            continue;
        if (navigable->is_svg_page())
            continue;
        if (auto document = navigable->active_document())
            document->update_layout(DOM::UpdateLayoutReason::HTMLEventLoopRenderingUpdate);
        navigable->paint_next_frame();
        if (navigable->is_traversable()) {
            auto traversable = navigable->traversable_navigable();
            traversable->process_screenshot_requests();
        }
    }

    // 23. For each doc of docs, process top layer removals given doc.
    for (auto& document : docs) {
        document->process_top_layer_removals();
    }

    for (auto& document : docs) {
        // https://drafts.csswg.org/css-font-loading/#fontfaceset-pending-on-the-environment
        // A FontFaceSet is pending on the environment if any of the following are true:
        // - the document is still loading
        // - the document has pending stylesheet requests
        // FIXME: - the document has pending layout operations which might cause the user agent to request a font, or which depend on recently-loaded fonts
        TemporaryExecutionContext context(document->realm(), TemporaryExecutionContext::CallbacksEnabled::Yes);
        document->fonts()->set_is_pending_on_the_environment(document->readiness() == DocumentReadyState::Loading);
    }
}

void run_when_event_loop_reaches_step_1(GC::Ref<GC::Function<void()>> steps)
{
    auto& event_loop = main_thread_event_loop();
    event_loop.run_upon_reaching_step_1(steps);
}

// https://html.spec.whatwg.org/multipage/webappapis.html#queue-a-task
TaskID queue_a_task(HTML::Task::Source source, GC::Ptr<EventLoop> event_loop, GC::Ptr<DOM::Document> document, GC::Ref<GC::Function<void()>> steps)
{
    // 1. If event loop was not given, set event loop to the implied event loop.
    if (!event_loop)
        event_loop = main_thread_event_loop();

    // FIXME: 2. If document was not given, set document to the implied document.

    // 3. Let task be a new task.
    // 4. Set task's steps to steps.
    // 5. Set task's source to source.
    // 6. Set task's document to the document.
    // 7. Set task's script evaluation environment settings object set to an empty set.
    auto task = HTML::Task::create(event_loop->vm(), source, document, steps);

    // 8. Let queue be the task queue to which source is associated on event loop.
    // 9. Append task to queue.
    if (source == HTML::Task::Source::Microtask)
        event_loop->enqueue_microtask(task);
    else
        event_loop->task_queue().add(task);

    return task->id();
}

// https://html.spec.whatwg.org/multipage/webappapis.html#queue-a-global-task
TaskID queue_global_task(HTML::Task::Source source, JS::Object& global_object, GC::Ref<GC::Function<void()>> steps)
{
    // 1. Let event loop be global's relevant agent's event loop.
    auto& event_loop = relevant_agent(global_object).event_loop;

    // 2. Let document be global's associated Document, if global is a Window object; otherwise null.
    DOM::Document* document { nullptr };
    if (auto* window_object = as_if<HTML::Window>(global_object))
        document = &window_object->associated_document();

    // 3. Queue a task given source, event loop, document, and steps.
    return queue_a_task(source, *event_loop, document, steps);
}

// https://html.spec.whatwg.org/multipage/webappapis.html#queue-a-microtask
void queue_a_microtask(DOM::Document const* document, GC::Ref<GC::Function<void()>> steps)
{
    // 1. If event loop was not given, set event loop to the implied event loop.
    auto& event_loop = HTML::main_thread_event_loop();

    // FIXME: 2. If document was not given, set document to the implied document.

    // 3. Let microtask be a new task.
    // 4. Set microtask's steps to steps.
    // 5. Set microtask's source to the microtask task source.
    // 6. Set microtask's document to document.
    auto& vm = event_loop.vm();
    auto microtask = HTML::Task::create(vm, HTML::Task::Source::Microtask, document, steps);

    // FIXME: 7. Set microtask's script evaluation environment settings object set to an empty set.

    // 8. Enqueue microtask on event loop's microtask queue.
    event_loop.enqueue_microtask(microtask);
}

void perform_a_microtask_checkpoint()
{
    main_thread_event_loop().perform_a_microtask_checkpoint();
}

// https://html.spec.whatwg.org/multipage/webappapis.html#perform-a-microtask-checkpoint
void EventLoop::perform_a_microtask_checkpoint()
{
    if (execution_paused())
        return;

    // 1. If the event loop's performing a microtask checkpoint is true, then return.
    if (m_performing_a_microtask_checkpoint)
        return;

    // NOTE: This assertion is per requirement 9.5 of the ECMA-262 spec, see: https://tc39.es/ecma262/#sec-jobs
    // > At some future point in time, when there is no running context in the agent for which the job is scheduled and that agent's execution context stack is empty...
    VERIFY(vm().execution_context_stack().is_empty());
    VERIFY(!vm().has_running_execution_context());

    // 2. Set the event loop's performing a microtask checkpoint to true.
    m_performing_a_microtask_checkpoint = true;

    // 3. While the event loop's microtask queue is not empty:
    while (!m_microtask_queue.is_empty()) {
        // 1. Let oldestMicrotask be the result of dequeuing from the event loop's microtask queue.
        auto oldest_microtask = m_microtask_queue.dequeue();

        // 2. Set the event loop's currently running task to oldestMicrotask.
        m_currently_running_task = oldest_microtask;

        // 3. Run oldestMicrotask.
        oldest_microtask->execute();

        // 4. Set the event loop's currently running task back to null.
        m_currently_running_task = nullptr;
    }

    // 4. For each environment settings object settingsObject whose responsible event loop is this event loop, notify about rejected promises given settingsObject's global object.
    auto environments = GC::RootVector { m_related_environment_settings_objects };
    for (auto& environment_settings_object : environments) {
        environment_settings_object->universal_global_scope().notify_about_rejected_promises({});
    }

    // 5. Cleanup Indexed Database transactions.
    IndexedDB::cleanup_indexed_database_transactions(*this);

    // 6. Perform ClearKeptObjects().
    vm().finish_execution_generation();

    // 7. Set the event loop's performing a microtask checkpoint to false.
    m_performing_a_microtask_checkpoint = false;

    // FIXME: 8. Record timing info for microtask checkpoint.
}

Vector<GC::Root<DOM::Document>> EventLoop::documents_in_this_event_loop_matching(Function<bool(DOM::Document&)> callback) const
{
    ensure_documents_sorted();
    Vector<GC::Root<DOM::Document>> documents;
    for (auto& document : m_documents) {
        VERIFY(document);
        if (document->is_decoded_svg())
            continue;
        if (!callback(*document))
            continue;
        documents.append(GC::make_root(*document));
    }
    return documents;
}

void EventLoop::register_document(Badge<DOM::Document>, DOM::Document& document)
{
    m_documents.append(&document);
    m_documents_sort_dirty = true;
}

void EventLoop::unregister_document(Badge<DOM::Document>, DOM::Document& document)
{
    bool did_remove = m_documents.remove_first_matching([&](auto& entry) { return entry.ptr() == &document; });
    VERIFY(did_remove);
}

void EventLoop::document_navigable_did_change(Badge<DOM::Document>)
{
    m_documents_sort_dirty = true;
}

void EventLoop::ensure_documents_sorted() const
{
    // https://html.spec.whatwg.org/multipage/webappapis.html#update-the-rendering step 3.2:
    // - Any Document B whose container document is A must be listed after A in the list.
    // - If there are two documents A and B that both have the same non-null container document
    //   C, then the order of A and B in the list must match the shadow-including tree order
    //   of their respective navigable containers in C's node tree.

    if (!m_documents_sort_dirty)
        return;
    m_documents_sort_dirty = false;

    HashMap<DOM::Document*, size_t> doc_to_index;
    doc_to_index.ensure_capacity(m_documents.size());
    for (size_t i = 0; i < m_documents.size(); ++i)
        doc_to_index.set(m_documents[i].ptr(), i);

    Vector<bool> visited;
    visited.resize(m_documents.size());
    Vector<GC::Weak<DOM::Document>> sorted;
    sorted.ensure_capacity(m_documents.size());

    auto visit = [&](auto& self, size_t idx) -> void {
        if (visited[idx])
            return;
        visited[idx] = true;
        if (auto navigable = m_documents[idx]->navigable()) {
            if (auto container_doc = navigable->container_document()) {
                if (auto container_idx = doc_to_index.get(container_doc.ptr()); container_idx.has_value())
                    self(self, *container_idx);
            }
        }
        sorted.append(m_documents[idx]);
    };
    for (size_t i = 0; i < m_documents.size(); ++i)
        visit(visit, i);

    m_documents = move(sorted);
}

void EventLoop::push_onto_backup_incumbent_realm_stack(GC::Ref<EnvironmentSettingsObject> environment_settings_object)
{
    m_backup_incumbent_realm_stack.append(environment_settings_object);
}

void EventLoop::pop_backup_incumbent_realm_stack()
{
    m_backup_incumbent_realm_stack.take_last();
}

EnvironmentSettingsObject& EventLoop::top_of_backup_incumbent_realm_stack()
{
    return m_backup_incumbent_realm_stack.last();
}

void EventLoop::register_environment_settings_object(Badge<EnvironmentSettingsObject>, EnvironmentSettingsObject& environment_settings_object)
{
    m_related_environment_settings_objects.append(&environment_settings_object);
}

void EventLoop::unregister_environment_settings_object(Badge<EnvironmentSettingsObject>, EnvironmentSettingsObject& environment_settings_object)
{
    bool did_remove = m_related_environment_settings_objects.remove_first_matching([&](auto& entry) { return entry == &environment_settings_object; });
    VERIFY(did_remove);
}

// https://html.spec.whatwg.org/multipage/webappapis.html#same-loop-windows
Vector<GC::Root<HTML::Window>> EventLoop::same_loop_windows() const
{
    Vector<GC::Root<HTML::Window>> windows;
    for (auto& document : documents_in_this_event_loop_matching([](auto& document) { return document.is_fully_active(); })) {
        windows.append(GC::make_root(document->window()));
    }
    return windows;
}

// https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model:last-idle-period-start-time
double EventLoop::compute_deadline() const
{
    // 1. Let deadline be this event loop's last idle period start time plus 50.
    auto deadline = m_last_idle_period_start_time + 50;
    // 2. Let hasPendingRenders be false.
    auto has_pending_renders = false;
    // 3. For each windowInSameLoop of the same-loop windows for this event loop:
    for (auto& window : same_loop_windows()) {
        // 1. If windowInSameLoop's map of animation frame callbacks is not empty,
        //    or if the user agent believes that the windowInSameLoop might have pending rendering updates,
        //    set hasPendingRenders to true.
        if (window->has_animation_frame_callbacks())
            has_pending_renders = true;
        // FIXME: 2. Let timerCallbackEstimates be the result of getting the values of windowInSameLoop's map of active timers.
        // FIXME: 3. For each timeoutDeadline of timerCallbackEstimates, if timeoutDeadline is less than deadline, set deadline to timeoutDeadline.
    }
    // 4. If hasPendingRenders is true, then:
    if (has_pending_renders) {
        // 1. Let nextRenderDeadline be this event loop's last render opportunity time plus (1000 divided by the current refresh rate).
        // FIXME: Hardcoded to 60Hz
        auto next_render_deadline = m_last_render_opportunity_time + (1000.0 / 60.0);
        // 2. If nextRenderDeadline is less than deadline, then return nextRenderDeadline.
        if (next_render_deadline < deadline)
            return next_render_deadline;
    }
    // 5. Return deadline.
    return deadline;
}

EventLoop::PauseHandle::PauseHandle(EventLoop& event_loop, JS::Object const& global, HighResolutionTime::DOMHighResTimeStamp time_before_pause)
    : event_loop(event_loop)
    , global(global)
    , time_before_pause(time_before_pause)
{
}

EventLoop::PauseHandle::~PauseHandle()
{
    event_loop->unpause({}, *global, time_before_pause);
}

// https://html.spec.whatwg.org/multipage/webappapis.html#pause
EventLoop::PauseHandle EventLoop::pause()
{
    m_execution_paused = true;

    // 1. Let global be the current global object.
    auto& global = current_global_object();

    // 2. Let timeBeforePause be the current high resolution time given global.
    auto time_before_pause = HighResolutionTime::current_high_resolution_time(global);

    // 3. If necessary, update the rendering or user interface of any Document or navigable to reflect the current state.
    if (!m_running_rendering_task)
        update_the_rendering();

    // 4. Wait until the condition goal is met. While a user agent has a paused task, the corresponding event loop must
    //    not run further tasks, and any script in the currently running task must block. User agents should remain
    //    responsive to user input while paused, however, albeit in a reduced capacity since the event loop will not be
    //    doing anything.

    return PauseHandle { *this, global, time_before_pause };
}

void EventLoop::unpause(Badge<PauseHandle>, JS::Object const& global, HighResolutionTime::DOMHighResTimeStamp time_before_pause)
{
    m_execution_paused = false;

    // FIXME: 5. Record pause duration given the duration from timeBeforePause to the current high resolution time given global.
    [[maybe_unused]] auto pause_duration = HighResolutionTime::current_high_resolution_time(global) - time_before_pause;
}

}
