/*
 * Copyright (c) 2023, Luke Wilde <lukew@serenityos.org>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/PerformanceObserver.h>
#include <LibWeb/HTML/WindowOrWorkerGlobalScope.h>
#include <LibWeb/HighResolutionTime/SupportedPerformanceTypes.h>
#include <LibWeb/PerformanceTimeline/EntryTypes.h>
#include <LibWeb/PerformanceTimeline/PerformanceEntry.h>
#include <LibWeb/PerformanceTimeline/PerformanceObserver.h>
#include <LibWeb/WebIDL/CallbackType.h>
#include <LibWeb/WebIDL/ExceptionOr.h>

namespace Web::PerformanceTimeline {

GC_DEFINE_ALLOCATOR(PerformanceObserver);

WebIDL::ExceptionOr<GC::Ref<PerformanceObserver>> PerformanceObserver::construct_impl(JS::Realm& realm, GC::Ptr<WebIDL::CallbackType> callback)
{
    return realm.create<PerformanceObserver>(realm, callback);
}

PerformanceObserver::PerformanceObserver(JS::Realm& realm, GC::Ptr<WebIDL::CallbackType> callback)
    : Bindings::PlatformObject(realm)
    , m_callback(move(callback))
{
}

PerformanceObserver::~PerformanceObserver() = default;

void PerformanceObserver::initialize(JS::Realm& realm)
{
    WEB_SET_PROTOTYPE_FOR_INTERFACE(PerformanceObserver);
    Base::initialize(realm);
}

void PerformanceObserver::visit_edges(Cell::Visitor& visitor)
{
    Base::visit_edges(visitor);
    visitor.visit(m_callback);
    visitor.visit(m_observer_buffer);
}

// https://w3c.github.io/performance-timeline/#dom-performanceobserver-observe
WebIDL::ExceptionOr<void> PerformanceObserver::observe(Bindings::PerformanceObserverInit& options)
{
    auto& realm = this->realm();

    // 1. Let relevantGlobal be this's relevant global object.
    auto& relevant_global = as<HTML::WindowOrWorkerGlobalScopeMixin>(HTML::relevant_global_object(*this));

    // 2. If options's entryTypes and type members are both omitted, then throw a "TypeError".
    if (!options.entry_types.has_value() && !options.type.has_value())
        return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Must specify one of entryTypes or type"sv };

    // 3. If options's entryTypes is present and any other member is also present, then throw a "TypeError".
    if (options.entry_types.has_value() && (options.type.has_value() || options.buffered.has_value()))
        return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Cannot specify type or buffered if entryTypes is specified"sv };

    // 4. Update or check this's observer type by running these steps:
    // 1. If this's observer type is "undefined":
    if (m_observer_type == ObserverType::Undefined) {
        // 1. If options's entryTypes member is present, then set this's observer type to "multiple".
        if (options.entry_types.has_value())
            m_observer_type = ObserverType::Multiple;

        // 2. If options's type member is present, then set this's observer type to "single".
        if (options.type.has_value())
            m_observer_type = ObserverType::Single;
    }
    // 2. If this's observer type is "single" and options's entryTypes member is present, then throw an "InvalidModificationError".
    else if (m_observer_type == ObserverType::Single) {
        if (options.entry_types.has_value())
            return WebIDL::InvalidModificationError::create(realm, "Cannot change a PerformanceObserver from observing a single type to observing multiple types"_utf16);
    }
    // 3. If this's observer type is "multiple" and options's type member is present, then throw an "InvalidModificationError".
    else if (m_observer_type == ObserverType::Multiple) {
        if (options.type.has_value())
            return WebIDL::InvalidModificationError::create(realm, "Cannot change a PerformanceObserver from observing multiple types to observing a single type"_utf16);
    }

    // 5. Set this's requires dropped entries to true.
    m_requires_dropped_entries = true;

    // 6. If this's observer type is "multiple", run the following steps:
    if (m_observer_type == ObserverType::Multiple) {
        // 1. Let entry types be options's entryTypes sequence.
        VERIFY(options.entry_types.has_value());
        auto& entry_types = options.entry_types.value();

        // 2. Remove all types from entry types that are not contained in relevantGlobal's frozen array of supported entry types.
        //    The user agent SHOULD notify developers if entry types is modified. For example, a console warning listing removed
        //    types might be appropriate.
        entry_types.remove_all_matching([](String const& type) {
#define __ENUMERATE_SUPPORTED_PERFORMANCE_ENTRY_TYPES(entry_type, cpp_class) \
    if (entry_type == type)                                                  \
        return false;
            ENUMERATE_SUPPORTED_PERFORMANCE_ENTRY_TYPES
#undef __ENUMERATE_SUPPORTED_PERFORMANCE_ENTRY_TYPES

            dbgln("Potential FIXME: Removing unsupported PerformanceEntry type '{}' from list of observed types in PerformanceObserver::observe()", type);
            return true;
        });

        // 3. If the resulting entry types sequence is an empty sequence, abort these steps.
        //    The user agent SHOULD notify developers when the steps are aborted to notify that registration has been aborted.
        //    For example, a console warning might be appropriate.
        if (entry_types.is_empty()) {
            dbgln("Potential FIXME: Returning from PerformanceObserver::observe() as we don't support any of the specified types (or none was specified).");
            return {};
        }

        // 4. If the list of registered performance observer objects of relevantGlobal contains a registered performance
        //    observer whose observer is this, replace its options list with a list containing options as its only item.
        // 5. Otherwise, create and append a registered performance observer object to the list of registered performance
        //    observer objects of relevantGlobal, with observer set to this and options list set to a list containing
        //    options as its only item.
        // NOTE: See the comment on PerformanceObserver::options_list about why this doesn't create a separate registered
        //       performance observer object.
        m_options_list.clear();
        m_options_list.append(options);
        relevant_global.register_performance_observer({}, *this);
    }
    // 7. Otherwise, run the following steps:
    else {
        // 1. Assert that this's observer type is "single".
        VERIFY(m_observer_type == ObserverType::Single);

        // 2. If options's type is not contained in the relevantGlobal's frozen array of supported entry types, abort these steps.
        //    The user agent SHOULD notify developers when this happens, for instance via a console warning.
        VERIFY(options.type.has_value());
        auto& type = options.type.value();
        bool recognized_type = false;

#define __ENUMERATE_SUPPORTED_PERFORMANCE_ENTRY_TYPES(entry_type, cpp_class) \
    if (!recognized_type && entry_type == type)                              \
        recognized_type = true;
        ENUMERATE_SUPPORTED_PERFORMANCE_ENTRY_TYPES
#undef __ENUMERATE_SUPPORTED_PERFORMANCE_ENTRY_TYPES

        if (!recognized_type) {
            dbgln("Potential FIXME: Returning from PerformanceObserver::observe() as we don't support the PerformanceEntry type '{}'", type);
            return {};
        }

        // 3. If the list of registered performance observer objects of relevantGlobal contains a registered performance
        //    observer obs whose observer is this:
        if (relevant_global.has_registered_performance_observer(*this)) {
            // 1. If obs's options list contains a PerformanceObserverInit item currentOptions whose type is equal to options's type,
            //    replace currentOptions with options in obs's options list.
            auto index = m_options_list.find_first_index_if([&options](Bindings::PerformanceObserverInit const& entry) {
                return entry.type == options.type;
            });
            if (index.has_value()) {
                m_options_list[index.value()] = options;
            } else {
                // Otherwise, append options to obs's options list.
                m_options_list.append(options);
            }
        }
        // 4. Otherwise, create and append a registered performance observer object to the list of registered performance
        //    observer objects of relevantGlobal, with observer set to the this and options list set to a list containing
        //    options as its only item.
        else {
            m_options_list.clear();
            m_options_list.append(options);
            relevant_global.register_performance_observer({}, *this);
        }

        // 5. If options's buffered flag is set:
        if (options.buffered.has_value() && options.buffered.value()) {
            // 1. Let tuple be the relevant performance entry tuple of options's type and relevantGlobal.
            auto const& tuple = relevant_global.relevant_performance_entry_tuple(type);

            // 2. For each entry in tuple's performance entry buffer:
            for (auto const& entry : tuple.performance_entry_buffer) {
                // 1. If should add entry with entry and options as parameters returns true, append entry to the observer buffer.
                if (entry->should_add_entry(options) == ShouldAddEntry::Yes)
                    m_observer_buffer.append(*entry);
            }

            // 3. Queue the PerformanceObserver task with relevantGlobal as input.
            relevant_global.queue_the_performance_observer_task();
        }
    }

    return {};
}

// https://w3c.github.io/performance-timeline/#dom-performanceobserver-disconnect
void PerformanceObserver::disconnect()
{
    // 1. Remove this from the list of registered performance observer objects of relevant global object.
    auto& relevant_global = as<HTML::WindowOrWorkerGlobalScopeMixin>(HTML::relevant_global_object(*this));
    relevant_global.unregister_performance_observer({}, *this);

    // 2. Empty this's observer buffer.
    m_observer_buffer.clear();

    // 3. Empty this's options list.
    m_options_list.clear();
}

// https://w3c.github.io/performance-timeline/#dom-performanceobserver-takerecords
Vector<GC::Root<PerformanceTimeline::PerformanceEntry>> PerformanceObserver::take_records()
{
    // The takeRecords() method must return a copy of this's observer buffer, and also empty this's observer buffer.
    Vector<GC::Root<PerformanceTimeline::PerformanceEntry>> records;
    for (auto& record : m_observer_buffer)
        records.append(*record);
    m_observer_buffer.clear();
    return records;
}

// https://w3c.github.io/performance-timeline/#dom-performanceobserver-supportedentrytypes
GC::Ref<JS::Object> PerformanceObserver::supported_entry_types(JS::VM& vm)
{
    // 1. Let globalObject be the environment settings object's global object.
    auto& window_or_worker = as<HTML::WindowOrWorkerGlobalScopeMixin>(vm.get_global_object());

    // 2. Return globalObject's frozen array of supported entry types.
    return window_or_worker.supported_entry_types();
}

void PerformanceObserver::unset_requires_dropped_entries(Badge<HTML::WindowOrWorkerGlobalScopeMixin>)
{
    m_requires_dropped_entries = false;
}

void PerformanceObserver::append_to_observer_buffer(Badge<HTML::WindowOrWorkerGlobalScopeMixin>, GC::Ref<PerformanceTimeline::PerformanceEntry> entry)
{
    m_observer_buffer.append(entry);
}

}
