#include <LibJS/Runtime/Error.h>
#include <LibJS/Runtime/Object.h>
#include <LibJS/Runtime/Value.h>
#include <LibJS/Runtime/ValueInlines.h>
#include <LibWeb/Bindings/ExceptionOrUtils.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/PerformanceEntry.h>
#include <LibWeb/Bindings/PerformanceEventTiming.h>
#include <LibWeb/DOM/Node.h>
#include <LibWeb/EventTiming/PerformanceEventTiming.h>
#include <LibWeb/WebIDL/AbstractOperations.h>
#include <LibWeb/WebIDL/Tracing.h>
#include <LibWeb/WebIDL/Types.h>

namespace Web::Bindings {

void PerformanceEventTimingConstructor::initialize(JS::Realm& realm, JS::NativeFunction& object)
{
    auto& vm = realm.vm();
    [[maybe_unused]] u8 default_attributes = JS::Attribute::Enumerable;

    object.set_prototype(&ensure_web_constructor<PerformanceEntryPrototype>(realm, "PerformanceEntry"_fly_string));
    object.define_direct_property(vm.names.length, JS::Value(0), JS::Attribute::Configurable);
    object.define_direct_property(vm.names.name, JS::PrimitiveString::create(vm, "PerformanceEventTiming"_utf16), JS::Attribute::Configurable);
    object.define_direct_property(vm.names.prototype, &ensure_web_prototype<PerformanceEventTimingPrototype>(realm, "PerformanceEventTiming"_fly_string), 0);
}

JS::ThrowCompletionOr<GC::Ref<JS::Object>> PerformanceEventTimingConstructor::construct([[maybe_unused]] InterfaceConstructor& constructor, [[maybe_unused]] JS::FunctionObject& new_target)
{
    WebIDL::log_trace(constructor.vm(), "PerformanceEventTimingConstructor::construct");
    return constructor.vm().throw_completion<JS::TypeError>(JS::ErrorType::NotAConstructor, "PerformanceEventTiming");
}

void PerformanceEventTimingPrototype::initialize(JS::Realm& realm, JS::Object& object)
{
    [[maybe_unused]] auto& vm = realm.vm();
    [[maybe_unused]] u8 default_attributes = JS::Attribute::Enumerable | JS::Attribute::Configurable | JS::Attribute::Writable;

    object.set_prototype(GC::Ref { ensure_web_prototype<PerformanceEntryPrototype>(realm, "PerformanceEntry"_fly_string) });

    auto processing_start_id = "processingStart"_utf16_fly_string;
    auto native_processing_start_getter = JS::NativeFunction::create(realm, processing_start_getter, 0, processing_start_id, &realm, "get"sv);
    GC::Ptr<JS::NativeFunction> native_processing_start_setter;

    // 4. Let configurable be false if attr is unforgeable and true otherwise.
    auto processing_start_attributes = default_attributes;

    // 5. Let desc be the PropertyDescriptor{[[Get]]: getter, [[Set]]: setter, [[Enumerable]]: true, [[Configurable]]: configurable}.

    // 7. Perform ! DefinePropertyOrThrow(target, id, desc).
    object.define_direct_accessor(processing_start_id, native_processing_start_getter, native_processing_start_setter, processing_start_attributes);

    // 8. FIXME: If attr’s type is an observable array type with type argument T, then set target’s backing observable array exotic object for attr to the result of creating an observable array exotic object in realm, given T, attr’s set an indexed value algorithm, and attr’s delete an indexed value algorithm.
    auto processing_end_id = "processingEnd"_utf16_fly_string;
    auto native_processing_end_getter = JS::NativeFunction::create(realm, processing_end_getter, 0, processing_end_id, &realm, "get"sv);
    GC::Ptr<JS::NativeFunction> native_processing_end_setter;

    // 4. Let configurable be false if attr is unforgeable and true otherwise.
    auto processing_end_attributes = default_attributes;

    // 5. Let desc be the PropertyDescriptor{[[Get]]: getter, [[Set]]: setter, [[Enumerable]]: true, [[Configurable]]: configurable}.

    // 7. Perform ! DefinePropertyOrThrow(target, id, desc).
    object.define_direct_accessor(processing_end_id, native_processing_end_getter, native_processing_end_setter, processing_end_attributes);

    // 8. FIXME: If attr’s type is an observable array type with type argument T, then set target’s backing observable array exotic object for attr to the result of creating an observable array exotic object in realm, given T, attr’s set an indexed value algorithm, and attr’s delete an indexed value algorithm.
    auto cancelable_id = "cancelable"_utf16_fly_string;
    auto native_cancelable_getter = JS::NativeFunction::create(realm, cancelable_getter, 0, cancelable_id, &realm, "get"sv);
    GC::Ptr<JS::NativeFunction> native_cancelable_setter;

    // 4. Let configurable be false if attr is unforgeable and true otherwise.
    auto cancelable_attributes = default_attributes;

    // 5. Let desc be the PropertyDescriptor{[[Get]]: getter, [[Set]]: setter, [[Enumerable]]: true, [[Configurable]]: configurable}.

    // 7. Perform ! DefinePropertyOrThrow(target, id, desc).
    object.define_direct_accessor(cancelable_id, native_cancelable_getter, native_cancelable_setter, cancelable_attributes);

    // 8. FIXME: If attr’s type is an observable array type with type argument T, then set target’s backing observable array exotic object for attr to the result of creating an observable array exotic object in realm, given T, attr’s set an indexed value algorithm, and attr’s delete an indexed value algorithm.
    auto target_id = "target"_utf16_fly_string;
    auto native_target_getter = JS::NativeFunction::create(realm, target_getter, 0, target_id, &realm, "get"sv);
    GC::Ptr<JS::NativeFunction> native_target_setter;

    // 4. Let configurable be false if attr is unforgeable and true otherwise.
    auto target_attributes = default_attributes;

    // 5. Let desc be the PropertyDescriptor{[[Get]]: getter, [[Set]]: setter, [[Enumerable]]: true, [[Configurable]]: configurable}.

    // 7. Perform ! DefinePropertyOrThrow(target, id, desc).
    object.define_direct_accessor(target_id, native_target_getter, native_target_setter, target_attributes);

    // 8. FIXME: If attr’s type is an observable array type with type argument T, then set target’s backing observable array exotic object for attr to the result of creating an observable array exotic object in realm, given T, attr’s set an indexed value algorithm, and attr’s delete an indexed value algorithm.
    auto interaction_id_id = "interactionId"_utf16_fly_string;
    auto native_interaction_id_getter = JS::NativeFunction::create(realm, interaction_id_getter, 0, interaction_id_id, &realm, "get"sv);
    GC::Ptr<JS::NativeFunction> native_interaction_id_setter;

    // 4. Let configurable be false if attr is unforgeable and true otherwise.
    auto interaction_id_attributes = default_attributes;

    // 5. Let desc be the PropertyDescriptor{[[Get]]: getter, [[Set]]: setter, [[Enumerable]]: true, [[Configurable]]: configurable}.

    // 7. Perform ! DefinePropertyOrThrow(target, id, desc).
    object.define_direct_accessor(interaction_id_id, native_interaction_id_getter, native_interaction_id_setter, interaction_id_attributes);

    // 8. FIXME: If attr’s type is an observable array type with type argument T, then set target’s backing observable array exotic object for attr to the result of creating an observable array exotic object in realm, given T, attr’s set an indexed value algorithm, and attr’s delete an indexed value algorithm.
    object.define_native_function(realm, "toJSON"_utf16_fly_string, to_json, 0, default_attributes);

    object.define_direct_property(vm.well_known_symbol_to_string_tag(), JS::PrimitiveString::create(vm, "PerformanceEventTiming"_utf16), JS::Attribute::Configurable);
}

void PerformanceEventTimingPrototype::define_unforgeable_attributes(JS::Realm& realm, [[maybe_unused]] JS::Object& object)
{
    [[maybe_unused]] auto& vm = realm.vm();
    [[maybe_unused]] u8 default_attributes = JS::Attribute::Enumerable;
}

[[maybe_unused]] static JS::ThrowCompletionOr<EventTiming::PerformanceEventTiming*> impl_from(JS::VM& vm, JS::Value js_value)
{

    if (auto impl = js_value.as_if<EventTiming::PerformanceEventTiming>())
        return impl.ptr();
    return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObjectOfType, "PerformanceEventTiming");
}

[[maybe_unused]] static JS::ThrowCompletionOr<EventTiming::PerformanceEventTiming*> impl_from(JS::VM& vm)
{
    auto this_value = vm.this_value();
    if (this_value.is_nullish())
        this_value = &vm.current_realm()->global_object();
    return impl_from(vm, this_value);
}

JS_DEFINE_NATIVE_FUNCTION(PerformanceEventTimingPrototype::processing_start_getter)
{
    WebIDL::log_trace(vm, "PerformanceEventTimingPrototype::processing_start_getter");
    [[maybe_unused]] auto& realm = *vm.current_realm();

    auto* idl_object = TRY(impl_from(vm));


    auto R = TRY(throw_dom_exception_if_needed(vm, [&] { return idl_object->processing_start(); }));

    return JS::Value(R);
}

JS_DEFINE_NATIVE_FUNCTION(PerformanceEventTimingPrototype::processing_end_getter)
{
    WebIDL::log_trace(vm, "PerformanceEventTimingPrototype::processing_end_getter");
    [[maybe_unused]] auto& realm = *vm.current_realm();

    auto* idl_object = TRY(impl_from(vm));


    auto R = TRY(throw_dom_exception_if_needed(vm, [&] { return idl_object->processing_end(); }));

    return JS::Value(R);
}

JS_DEFINE_NATIVE_FUNCTION(PerformanceEventTimingPrototype::cancelable_getter)
{
    WebIDL::log_trace(vm, "PerformanceEventTimingPrototype::cancelable_getter");
    [[maybe_unused]] auto& realm = *vm.current_realm();

    auto* idl_object = TRY(impl_from(vm));


    auto R = TRY(throw_dom_exception_if_needed(vm, [&] { return idl_object->cancelable(); }));

    return JS::Value(R);
}

JS_DEFINE_NATIVE_FUNCTION(PerformanceEventTimingPrototype::target_getter)
{
    WebIDL::log_trace(vm, "PerformanceEventTimingPrototype::target_getter");
    [[maybe_unused]] auto& realm = *vm.current_realm();

    auto* idl_object = TRY(impl_from(vm));


    auto R = TRY(throw_dom_exception_if_needed(vm, [&] { return idl_object->target(); }));

    return [&]() -> JS::Value {
        // 1. If the IDL nullable type T? value is null, then the JavaScript value is null.
        if (!R)
            return JS::js_null();

        // 2. Otherwise, the JavaScript value is the result of converting the IDL nullable type value to the inner IDL type T.
        return JS::Value(JS::Value(R));
    }();
}

JS_DEFINE_NATIVE_FUNCTION(PerformanceEventTimingPrototype::interaction_id_getter)
{
    WebIDL::log_trace(vm, "PerformanceEventTimingPrototype::interaction_id_getter");
    [[maybe_unused]] auto& realm = *vm.current_realm();

    auto* idl_object = TRY(impl_from(vm));


    auto R = TRY(throw_dom_exception_if_needed(vm, [&] { return idl_object->interaction_id(); }));

    return JS::Value(static_cast<double>(R));
}

JS_DEFINE_NATIVE_FUNCTION(PerformanceEventTimingPrototype::to_json)
{
    WebIDL::log_trace(vm, "PerformanceEventTimingPrototype::to_json");
    auto& realm = *vm.current_realm();

    [[maybe_unused]] auto* idl_object = TRY(impl_from(vm));

    // 4. Let result be OrdinaryObjectCreate(%Object.prototype%).
    auto result = JS::Object::create(realm, realm.intrinsics().object_prototype());

    // 5. For each key → value of map:
    auto name_0_value = TRY(throw_dom_exception_if_needed(vm, [&] { return idl_object->name(); }));

    // 1. Let k be key converted to a JavaScript value.
    auto name_0_key = "name"_utf16_fly_string;

    // 2. Let v be value converted to a JavaScript value.
    auto name_0_value_js = WebIDL::primitive_string_from_string(vm, name_0_value);

    // 3. Perform ! CreateDataPropertyOrThrow(result, k, v).
    MUST(result->create_data_property(name_0_key, name_0_value_js));
    auto entry_type_1_value = TRY(throw_dom_exception_if_needed(vm, [&] { return idl_object->entry_type(); }));

    // 1. Let k be key converted to a JavaScript value.
    auto entry_type_1_key = "entryType"_utf16_fly_string;

    // 2. Let v be value converted to a JavaScript value.
    auto entry_type_1_value_js = WebIDL::primitive_string_from_string(vm, entry_type_1_value);

    // 3. Perform ! CreateDataPropertyOrThrow(result, k, v).
    MUST(result->create_data_property(entry_type_1_key, entry_type_1_value_js));
    auto start_time_2_value = TRY(throw_dom_exception_if_needed(vm, [&] { return idl_object->start_time(); }));

    // 1. Let k be key converted to a JavaScript value.
    auto start_time_2_key = "startTime"_utf16_fly_string;

    // 2. Let v be value converted to a JavaScript value.
    auto start_time_2_value_js = JS::Value(start_time_2_value);

    // 3. Perform ! CreateDataPropertyOrThrow(result, k, v).
    MUST(result->create_data_property(start_time_2_key, start_time_2_value_js));
    auto duration_3_value = TRY(throw_dom_exception_if_needed(vm, [&] { return idl_object->duration(); }));

    // 1. Let k be key converted to a JavaScript value.
    auto duration_3_key = "duration"_utf16_fly_string;

    // 2. Let v be value converted to a JavaScript value.
    auto duration_3_value_js = JS::Value(duration_3_value);

    // 3. Perform ! CreateDataPropertyOrThrow(result, k, v).
    MUST(result->create_data_property(duration_3_key, duration_3_value_js));
    auto processing_start_4_value = TRY(throw_dom_exception_if_needed(vm, [&] { return idl_object->processing_start(); }));

    // 1. Let k be key converted to a JavaScript value.
    auto processing_start_4_key = "processingStart"_utf16_fly_string;

    // 2. Let v be value converted to a JavaScript value.
    auto processing_start_4_value_js = JS::Value(processing_start_4_value);

    // 3. Perform ! CreateDataPropertyOrThrow(result, k, v).
    MUST(result->create_data_property(processing_start_4_key, processing_start_4_value_js));
    auto processing_end_5_value = TRY(throw_dom_exception_if_needed(vm, [&] { return idl_object->processing_end(); }));

    // 1. Let k be key converted to a JavaScript value.
    auto processing_end_5_key = "processingEnd"_utf16_fly_string;

    // 2. Let v be value converted to a JavaScript value.
    auto processing_end_5_value_js = JS::Value(processing_end_5_value);

    // 3. Perform ! CreateDataPropertyOrThrow(result, k, v).
    MUST(result->create_data_property(processing_end_5_key, processing_end_5_value_js));
    auto cancelable_6_value = TRY(throw_dom_exception_if_needed(vm, [&] { return idl_object->cancelable(); }));

    // 1. Let k be key converted to a JavaScript value.
    auto cancelable_6_key = "cancelable"_utf16_fly_string;

    // 2. Let v be value converted to a JavaScript value.
    auto cancelable_6_value_js = JS::Value(cancelable_6_value);

    // 3. Perform ! CreateDataPropertyOrThrow(result, k, v).
    MUST(result->create_data_property(cancelable_6_key, cancelable_6_value_js));
    auto interaction_id_7_value = TRY(throw_dom_exception_if_needed(vm, [&] { return idl_object->interaction_id(); }));

    // 1. Let k be key converted to a JavaScript value.
    auto interaction_id_7_key = "interactionId"_utf16_fly_string;

    // 2. Let v be value converted to a JavaScript value.
    auto interaction_id_7_value_js = JS::Value(static_cast<double>(interaction_id_7_value));

    // 3. Perform ! CreateDataPropertyOrThrow(result, k, v).
    MUST(result->create_data_property(interaction_id_7_key, interaction_id_7_value_js));

    // 6. Return result.
    return result;
}

} // namespace Web::Bindings
