#include <AK/TypeCasts.h>
#include <AK/Utf16FlyString.h>
#include <AK/Variant.h>
#include <LibGC/Heap.h>
#include <LibJS/Runtime/AbstractOperations.h>
#include <LibJS/Runtime/Error.h>
#include <LibJS/Runtime/FunctionObject.h>
#include <LibJS/Runtime/Promise.h>
#include <LibJS/Runtime/Realm.h>
#include <LibJS/Runtime/VM.h>
#include <LibJS/Runtime/Value.h>
#include <LibJS/Runtime/ValueInlines.h>
#include <LibWeb/Bindings/CustomElementRegistry.h>
#include <LibWeb/Bindings/ExceptionOrUtils.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/DOM/Node.h>
#include <LibWeb/HTML/CustomElements/CustomElementRegistry.h>
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/HTML/Scripting/SimilarOriginWindowAgent.h>
#include <LibWeb/WebIDL/AbstractOperations.h>
#include <LibWeb/WebIDL/CallbackType.h>
#include <LibWeb/WebIDL/Promise.h>
#include <LibWeb/WebIDL/Tracing.h>

namespace Web::Bindings {

void CustomElementRegistryConstructor::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, "CustomElementRegistry"_utf16), JS::Attribute::Configurable);
    object.define_direct_property(vm.names.prototype, &ensure_web_prototype<CustomElementRegistryPrototype>(realm, "CustomElementRegistry"_fly_string), 0);
}

JS::ThrowCompletionOr<GC::Ref<JS::Object>> CustomElementRegistryConstructor::construct([[maybe_unused]] InterfaceConstructor& constructor, [[maybe_unused]] JS::FunctionObject& new_target)
{
    WebIDL::log_trace(constructor.vm(), "CustomElementRegistryConstructor::construct");
    auto& vm = constructor.vm();
    [[maybe_unused]] auto& realm = *vm.current_realm();

    // To internally create a new object implementing the interface CustomElementRegistry:

    // 3.2. Let prototype be ? Get(newTarget, "prototype").
    auto prototype = TRY(new_target.get(vm.names.prototype));

    // 3.3. If Type(prototype) is not Object, then:
    if (!prototype.is_object()) {
        // 1. Let targetRealm be ? GetFunctionRealm(newTarget).
        auto* target_realm = TRY(JS::get_function_realm(vm, new_target));

        // 2. Set prototype to the interface prototype object for interface in targetRealm.
        VERIFY(target_realm);
        prototype = &Bindings::ensure_web_prototype<CustomElementRegistryPrototype>(*target_realm, "CustomElementRegistry"_fly_string);
    }

    auto impl = TRY(throw_dom_exception_if_needed(vm, [&] { return HTML::CustomElementRegistry::construct_impl(realm); }));

    // 7. Set instance.[[Prototype]] to prototype.
    VERIFY(prototype.is_object());
    impl->set_prototype(&prototype.as_object());

    // FIXME: Steps 8...11. of the "internally create a new object implementing the interface CustomElementRegistry" algorithm
    // (https://webidl.spec.whatwg.org/#js-platform-objects) are currently not handled, or are handled within HTML::CustomElementRegistry::construct_impl().

    return *impl;
}

void CustomElementRegistryPrototype::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, "define"_utf16_fly_string, define, 2, default_attributes);

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

    object.define_native_function(realm, "getName"_utf16_fly_string, get_name, 1, default_attributes);

    object.define_native_function(realm, "whenDefined"_utf16_fly_string, when_defined, 1, default_attributes);

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

    object.define_native_function(realm, "initialize"_utf16_fly_string, initialize_, 1, default_attributes);

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

void CustomElementRegistryPrototype::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<HTML::CustomElementRegistry*> impl_from(JS::VM& vm, JS::Value js_value)
{

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

[[maybe_unused]] static JS::ThrowCompletionOr<HTML::CustomElementRegistry*> 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(CustomElementRegistryPrototype::define)
{
    WebIDL::log_trace(vm, "CustomElementRegistryPrototype::define");
    [[maybe_unused]] auto& realm = *vm.current_realm();
    [[maybe_unused]] HTML::CustomElementRegistry* idl_object = TRY(impl_from(vm));

    if (vm.argument_count() < 2)
        return vm.throw_completion<JS::TypeError>(JS::ErrorType::BadArgCountMany, "define", "2");

    auto arg0 = vm.argument(0);
    auto name = TRY(throw_dom_exception_if_needed(vm, [&] { return [&]() -> JS::ThrowCompletionOr<Utf16FlyString> {
        return TRY(WebIDL::to_utf16_string(vm, arg0));
    }(); }));

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

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

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

    auto arg2 = vm.argument(2);
    ElementDefinitionOptions options = ElementDefinitionOptions {};
    if (!arg2.is_undefined())
        options = TRY(throw_dom_exception_if_needed(vm, [&] { return convert_to_idl_value_for_element_definition_options(vm, arg2); }));

    auto original_steps = [&] {
        return throw_dom_exception_if_needed(vm, [&] { return idl_object->define(name, constructor, options); });
    };

    [[maybe_unused]] auto R = TRY([&]() -> decltype(original_steps()) {
        // For [CEReactions]: https://html.spec.whatwg.org/multipage/custom-elements.html#cereactions

        // 1. Push a new element queue onto this object's relevant agent's custom element reactions stack.
        auto& reactions_stack = HTML::relevant_similar_origin_window_agent(*idl_object).custom_element_reactions_stack;
        reactions_stack.element_queue_stack.append({});

        // 2. Run the originally-specified steps for this construct, catching any exceptions. If the steps return a value, let value be the returned value. If they throw an exception, let exception be the thrown exception.
        auto value_or_exception = original_steps();

        // 3. Let queue be the result of popping from this object's relevant agent's custom element reactions stack.
        // 4. Invoke custom element reactions in queue.
        auto queue = reactions_stack.element_queue_stack.take_last();
        Bindings::invoke_custom_element_reactions(queue);

        // 5. If an exception exception was thrown by the original steps, rethrow exception.
        if (value_or_exception.is_error())
            return value_or_exception.release_error();

        // 6. If a value value was returned from the original steps, return value.
        return value_or_exception.release_value();
    }());
    return JS::js_undefined();
}

JS_DEFINE_NATIVE_FUNCTION(CustomElementRegistryPrototype::get)
{
    WebIDL::log_trace(vm, "CustomElementRegistryPrototype::get");
    [[maybe_unused]] auto& realm = *vm.current_realm();
    [[maybe_unused]] HTML::CustomElementRegistry* idl_object = TRY(impl_from(vm));

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

    auto arg0 = vm.argument(0);
    auto name = TRY(throw_dom_exception_if_needed(vm, [&] { return [&]() -> JS::ThrowCompletionOr<Utf16FlyString> {
        return TRY(WebIDL::to_utf16_string(vm, arg0));
    }(); }));

    [[maybe_unused]] auto R = TRY(throw_dom_exception_if_needed(vm, [&] { return idl_object->get(name); }));
    return R.visit(
        [&](GC::Ref<WebIDL::CallbackType> const& visited_union_value0) -> JS::Value
        {
            return visited_union_value0->callback;
        },
        [](Empty) -> JS::Value
        {
            return JS::js_undefined();
        }
    );
}

JS_DEFINE_NATIVE_FUNCTION(CustomElementRegistryPrototype::get_name)
{
    WebIDL::log_trace(vm, "CustomElementRegistryPrototype::get_name");
    [[maybe_unused]] auto& realm = *vm.current_realm();
    [[maybe_unused]] HTML::CustomElementRegistry* idl_object = TRY(impl_from(vm));

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

    auto arg0 = vm.argument(0);
    auto constructor = 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->get_name(constructor); }));
    return [&]() -> JS::Value {
        // 1. If the IDL nullable type T? value is null, then the JavaScript value is null.
        if (!R.has_value())
            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(WebIDL::primitive_string_from_string(vm, R.value()));
    }();
}

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

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

    auto arg0 = vm.argument(0);
    auto name = TRY(throw_dom_exception_if_needed(vm, [&] { return [&]() -> JS::ThrowCompletionOr<Utf16FlyString> {
        return TRY(WebIDL::to_utf16_string(vm, arg0));
    }(); }));

    [[maybe_unused]] auto R = TRY(throw_dom_exception_if_needed(vm, [&] { return idl_object->when_defined(name); }));
        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(CustomElementRegistryPrototype::upgrade)
{
    WebIDL::log_trace(vm, "CustomElementRegistryPrototype::upgrade");
    [[maybe_unused]] auto& realm = *vm.current_realm();
    [[maybe_unused]] HTML::CustomElementRegistry* idl_object = TRY(impl_from(vm));

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

    auto arg0 = vm.argument(0);
    auto root = TRY(throw_dom_exception_if_needed(vm, [&] { return [&]() -> JS::ThrowCompletionOr<GC::Ref<DOM::Node>> {
        if (auto impl = arg0.as_if<DOM::Node>())
            return *impl;
        return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObjectOfType, "Node");
    }(); }));

    auto original_steps = [&] {
        return throw_dom_exception_if_needed(vm, [&] { return idl_object->upgrade(root); });
    };

    [[maybe_unused]] auto R = TRY([&]() -> decltype(original_steps()) {
        // For [CEReactions]: https://html.spec.whatwg.org/multipage/custom-elements.html#cereactions

        // 1. Push a new element queue onto this object's relevant agent's custom element reactions stack.
        auto& reactions_stack = HTML::relevant_similar_origin_window_agent(*idl_object).custom_element_reactions_stack;
        reactions_stack.element_queue_stack.append({});

        // 2. Run the originally-specified steps for this construct, catching any exceptions. If the steps return a value, let value be the returned value. If they throw an exception, let exception be the thrown exception.
        auto value_or_exception = original_steps();

        // 3. Let queue be the result of popping from this object's relevant agent's custom element reactions stack.
        // 4. Invoke custom element reactions in queue.
        auto queue = reactions_stack.element_queue_stack.take_last();
        Bindings::invoke_custom_element_reactions(queue);

        // 5. If an exception exception was thrown by the original steps, rethrow exception.
        if (value_or_exception.is_error())
            return value_or_exception.release_error();

        // 6. If a value value was returned from the original steps, return value.
        return value_or_exception.release_value();
    }());
    return JS::js_undefined();
}

JS_DEFINE_NATIVE_FUNCTION(CustomElementRegistryPrototype::initialize_)
{
    WebIDL::log_trace(vm, "CustomElementRegistryPrototype::initialize_");
    [[maybe_unused]] auto& realm = *vm.current_realm();
    [[maybe_unused]] HTML::CustomElementRegistry* idl_object = TRY(impl_from(vm));

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

    auto arg0 = vm.argument(0);
    auto root = TRY(throw_dom_exception_if_needed(vm, [&] { return [&]() -> JS::ThrowCompletionOr<GC::Ref<DOM::Node>> {
        if (auto impl = arg0.as_if<DOM::Node>())
            return *impl;
        return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObjectOfType, "Node");
    }(); }));

    auto original_steps = [&] {
        return throw_dom_exception_if_needed(vm, [&] { return idl_object->initialize_for_bindings(root); });
    };

    [[maybe_unused]] auto R = TRY([&]() -> decltype(original_steps()) {
        // For [CEReactions]: https://html.spec.whatwg.org/multipage/custom-elements.html#cereactions

        // 1. Push a new element queue onto this object's relevant agent's custom element reactions stack.
        auto& reactions_stack = HTML::relevant_similar_origin_window_agent(*idl_object).custom_element_reactions_stack;
        reactions_stack.element_queue_stack.append({});

        // 2. Run the originally-specified steps for this construct, catching any exceptions. If the steps return a value, let value be the returned value. If they throw an exception, let exception be the thrown exception.
        auto value_or_exception = original_steps();

        // 3. Let queue be the result of popping from this object's relevant agent's custom element reactions stack.
        // 4. Invoke custom element reactions in queue.
        auto queue = reactions_stack.element_queue_stack.take_last();
        Bindings::invoke_custom_element_reactions(queue);

        // 5. If an exception exception was thrown by the original steps, rethrow exception.
        if (value_or_exception.is_error())
            return value_or_exception.release_error();

        // 6. If a value value was returned from the original steps, return value.
        return value_or_exception.release_value();
    }());
    return JS::js_undefined();
}

// https://webidl.spec.whatwg.org/#es-dictionary
JS::ThrowCompletionOr<ElementDefinitionOptions> convert_to_idl_value_for_element_definition_options(JS::VM& vm, JS::Value js_dict)
{
    // 1. If jsDict is not an Object and jsDict is neither undefined nor null, then throw a TypeError.
    if (!js_dict.is_object() && !js_dict.is_nullish())
        return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObjectOfType, "ElementDefinitionOptions");

    // 2. Let idlDict be an empty ordered map, representing a dictionary of type D.
    // 3. Let dictionaries be a list consisting of D and all of D’s inherited dictionaries, in order from least to most derived.
    // 4. For each dictionary dictionary in dictionaries, in order:
    // NB: We defer construction until the return initializer because some members may not be default-constructible. Inherited dictionaries are represented by the generated C++ struct inheritance.

    // 5. Return idlDict.
    return ElementDefinitionOptions {
        .extends = TRY([&]() -> JS::ThrowCompletionOr<Optional<Utf16FlyString>> {
            // 1. Let key be the identifier of member.
            // 2. If jsDict is either undefined or null, then:
            //     1. Let jsMemberValue be undefined.
            // 3. Otherwise,
            //     1. Let jsMemberValue be ? Get(jsDict, key).
            auto js_member_value = JS::js_undefined();
            if (js_dict.is_object())
                js_member_value = TRY(js_dict.as_object().get("extends"_utf16_fly_string));

            // 4. If jsMemberValue is not undefined, then:
            if (!js_member_value.is_undefined()) {
                // 1. Let idlMemberValue be the result of converting jsMemberValue to an IDL value whose type is the type member is declared to be of.
                auto idl_member_value = TRY(throw_dom_exception_if_needed(vm, [&] { return [&]() -> JS::ThrowCompletionOr<Utf16FlyString> {
        return TRY(WebIDL::to_utf16_string(vm, js_member_value));
    }(); }));

                // 2. Set idlDict[key] to idlMemberValue.
                return idl_member_value;
            }
            // 7. Otherwise, jsMemberValue is undefined and the member is optional.
            return OptionalNone {};
        }()),
    };
}

} // namespace Web::Bindings
