#include <AK/TypeCasts.h>
#include <LibGC/Heap.h>
#include <LibJS/Runtime/Error.h>
#include <LibJS/Runtime/FunctionObject.h>
#include <LibJS/Runtime/Promise.h>
#include <LibJS/Runtime/Value.h>
#include <LibJS/Runtime/ValueInlines.h>
#include <LibWeb/Bindings/ExceptionOrUtils.h>
#include <LibWeb/Bindings/FakeXRDevice.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/XRTest.h>
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/Internals/XRTest.h>
#include <LibWeb/WebIDL/CallbackType.h>
#include <LibWeb/WebIDL/Promise.h>
#include <LibWeb/WebIDL/Tracing.h>

namespace Web::Bindings {

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

    
    object.define_direct_property(vm.names.length, JS::Value(0), JS::Attribute::Configurable);
    object.define_direct_property(vm.names.name, JS::PrimitiveString::create(vm, "XRTest"_utf16), JS::Attribute::Configurable);
    object.define_direct_property(vm.names.prototype, &ensure_web_prototype<XRTestPrototype>(realm, "XRTest"_fly_string), 0);
}

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

void XRTestPrototype::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(realm.intrinsics().object_prototype());
    object.define_native_function(realm, "simulateDeviceConnection"_utf16_fly_string, simulate_device_connection, 1, default_attributes);

    object.define_native_function(realm, "simulateUserActivation"_utf16_fly_string, simulate_user_activation, 1, default_attributes);

    object.define_native_function(realm, "disconnectAllDevices"_utf16_fly_string, disconnect_all_devices, 0, default_attributes);

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

void XRTestPrototype::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<Internals::XRTest*> impl_from(JS::VM& vm, JS::Value js_value)
{

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

[[maybe_unused]] static JS::ThrowCompletionOr<Internals::XRTest*> 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(XRTestPrototype::simulate_device_connection)
{
    WebIDL::log_trace(vm, "XRTestPrototype::simulate_device_connection");
    [[maybe_unused]] auto& realm = *vm.current_realm();
    auto steps = [&realm, &vm]() -> JS::ThrowCompletionOr<GC::Ref<WebIDL::Promise>> {
        (void)realm;
    [[maybe_unused]] Internals::XRTest* idl_object = TRY(impl_from(vm));

    if (vm.argument_count() < 1)
        return vm.throw_completion<JS::TypeError>(JS::ErrorType::BadArgCountOne, "simulateDeviceConnection");

    auto arg0 = vm.argument(0);
    auto init = TRY(throw_dom_exception_if_needed(vm, [&] { return convert_to_idl_value_for_fake_xr_device_init(vm, arg0); }));

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

    auto maybe_R = steps();

    // And then, if an exception E was thrown:
    // 1. If op has a return type that is a promise type, then return ! Call(%Promise.reject%, %Promise%, «E»).
    // 2. Otherwise, end these steps and allow the exception to propagate.
    if (maybe_R.is_throw_completion())
        return WebIDL::create_rejected_promise(realm, maybe_R.error_value())->promise();

    return GC::Ref { as<JS::Promise>(*maybe_R.release_value()->promise()) };
}

JS_DEFINE_NATIVE_FUNCTION(XRTestPrototype::simulate_user_activation)
{
    WebIDL::log_trace(vm, "XRTestPrototype::simulate_user_activation");
    [[maybe_unused]] auto& realm = *vm.current_realm();
    [[maybe_unused]] Internals::XRTest* idl_object = TRY(impl_from(vm));

    if (vm.argument_count() < 1)
        return vm.throw_completion<JS::TypeError>(JS::ErrorType::BadArgCountOne, "simulateUserActivation");

    auto arg0 = vm.argument(0);
    auto f = TRY(throw_dom_exception_if_needed(vm, [&] { return [&]() -> JS::ThrowCompletionOr<GC::Ref<WebIDL::CallbackType>> {

        if (!arg0.is_function())
            return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAFunction, arg0);

        return vm.heap().allocate<WebIDL::CallbackType>(arg0.as_object(),  HTML::incumbent_realm(), WebIDL::OperationReturnsPromise::No);
    }(); }));

    [[maybe_unused]] auto R = TRY(throw_dom_exception_if_needed(vm, [&] { return idl_object->simulate_user_activation(f); }));
    return JS::js_undefined();
}

JS_DEFINE_NATIVE_FUNCTION(XRTestPrototype::disconnect_all_devices)
{
    WebIDL::log_trace(vm, "XRTestPrototype::disconnect_all_devices");
    [[maybe_unused]] auto& realm = *vm.current_realm();
    auto steps = [&realm, &vm]() -> JS::ThrowCompletionOr<GC::Ref<WebIDL::Promise>> {
        (void)realm;
    [[maybe_unused]] Internals::XRTest* idl_object = TRY(impl_from(vm));

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

    auto maybe_R = steps();

    // And then, if an exception E was thrown:
    // 1. If op has a return type that is a promise type, then return ! Call(%Promise.reject%, %Promise%, «E»).
    // 2. Otherwise, end these steps and allow the exception to propagate.
    if (maybe_R.is_throw_completion())
        return WebIDL::create_rejected_promise(realm, maybe_R.error_value())->promise();

    return GC::Ref { as<JS::Promise>(*maybe_R.release_value()->promise()) };
}

} // namespace Web::Bindings
