#include <AK/String.h>
#include <LibGC/Heap.h>
#include <LibJS/Runtime/AbstractOperations.h>
#include <LibJS/Runtime/Error.h>
#include <LibJS/Runtime/Realm.h>
#include <LibJS/Runtime/Value.h>
#include <LibJS/Runtime/ValueInlines.h>
#include <LibWeb/Bindings/ExceptionOrUtils.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/XPathEvaluator.h>
#include <LibWeb/DOM/Node.h>
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/WebIDL/AbstractOperations.h>
#include <LibWeb/WebIDL/CallbackType.h>
#include <LibWeb/WebIDL/Tracing.h>
#include <LibWeb/WebIDL/Types.h>
#include <LibWeb/XPath/XPathEvaluator.h>
#include <LibWeb/XPath/XPathExpression.h>
#include <LibWeb/XPath/XPathNSResolver.h>
#include <LibWeb/XPath/XPathResult.h>

namespace Web::Bindings {

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

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

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

    // 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<XPathEvaluatorPrototype>(*target_realm, "XPathEvaluator"_fly_string);
    }

    auto impl = TRY(throw_dom_exception_if_needed(vm, [&] { return XPath::XPathEvaluator::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 XPathEvaluator" algorithm
    // (https://webidl.spec.whatwg.org/#js-platform-objects) are currently not handled, or are handled within XPath::XPathEvaluator::construct_impl().

    return *impl;
}

void XPathEvaluatorPrototype::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, "createExpression"_utf16_fly_string, create_expression, 1, default_attributes);

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

    object.define_native_function(realm, "evaluate"_utf16_fly_string, evaluate, 2, default_attributes);

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

void XPathEvaluatorPrototype::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<XPath::XPathEvaluator*> impl_from(JS::VM& vm, JS::Value js_value)
{

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

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

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

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

    auto arg1 = vm.argument(1);
    GC::Ptr<XPath::XPathNSResolver> resolver = nullptr;
    if (!arg1.is_undefined())
        resolver = TRY(throw_dom_exception_if_needed(vm, [&] { return [&]() -> JS::ThrowCompletionOr<GC::Ptr<XPath::XPathNSResolver>> {
        GC::Ptr<XPath::XPathNSResolver> value;

        if (!arg1.is_nullish()) {

            value = TRY([&]() -> JS::ThrowCompletionOr<GC::Ref<XPath::XPathNSResolver>> {
        if (!arg1.is_object())
            return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObject, arg1);

        auto callback_type = vm.heap().allocate<WebIDL::CallbackType>(arg1.as_object(), HTML::incumbent_realm());
        return TRY(throw_dom_exception_if_needed(vm, [&] { return XPath::XPathNSResolver::create(realm, callback_type); }));
    }());
        }
        return value;
    }(); }));

    [[maybe_unused]] auto R = TRY(throw_dom_exception_if_needed(vm, [&] { return idl_object->create_expression(expression, resolver); }));
    return JS::Value(R);
}

JS_DEFINE_NATIVE_FUNCTION(XPathEvaluatorPrototype::create_ns_resolver)
{
    WebIDL::log_trace(vm, "XPathEvaluatorPrototype::create_ns_resolver");
    [[maybe_unused]] auto& realm = *vm.current_realm();
    [[maybe_unused]] XPath::XPathEvaluator* idl_object = TRY(impl_from(vm));

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

    auto arg0 = vm.argument(0);
    auto node_resolver = 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");
    }(); }));

    [[maybe_unused]] auto R = TRY(throw_dom_exception_if_needed(vm, [&] { return idl_object->create_ns_resolver(node_resolver); }));
    return JS::Value(R);
}

JS_DEFINE_NATIVE_FUNCTION(XPathEvaluatorPrototype::evaluate)
{
    WebIDL::log_trace(vm, "XPathEvaluatorPrototype::evaluate");
    [[maybe_unused]] auto& realm = *vm.current_realm();
    [[maybe_unused]] XPath::XPathEvaluator* idl_object = TRY(impl_from(vm));

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

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

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

    auto arg2 = vm.argument(2);
    GC::Ptr<XPath::XPathNSResolver> resolver = nullptr;
    if (!arg2.is_undefined())
        resolver = TRY(throw_dom_exception_if_needed(vm, [&] { return [&]() -> JS::ThrowCompletionOr<GC::Ptr<XPath::XPathNSResolver>> {
        GC::Ptr<XPath::XPathNSResolver> value;

        if (!arg2.is_nullish()) {

            value = TRY([&]() -> JS::ThrowCompletionOr<GC::Ref<XPath::XPathNSResolver>> {
        if (!arg2.is_object())
            return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObject, arg2);

        auto callback_type = vm.heap().allocate<WebIDL::CallbackType>(arg2.as_object(), HTML::incumbent_realm());
        return TRY(throw_dom_exception_if_needed(vm, [&] { return XPath::XPathNSResolver::create(realm, callback_type); }));
    }());
        }
        return value;
    }(); }));

    auto arg3 = vm.argument(3);
    WebIDL::UnsignedShort type = 0;
    if (!arg3.is_undefined())
        type = TRY(throw_dom_exception_if_needed(vm, [&] { return WebIDL::convert_to_int<WebIDL::UnsignedShort>(vm, arg3, WebIDL::EnforceRange::No, WebIDL::Clamp::No); }));

    auto arg4 = vm.argument(4);
    GC::Ptr<XPath::XPathResult> result = nullptr;
    if (!arg4.is_undefined())
        result = TRY(throw_dom_exception_if_needed(vm, [&] { return [&]() -> JS::ThrowCompletionOr<GC::Ptr<XPath::XPathResult>> {
        GC::Ptr<XPath::XPathResult> value;

        if (!arg4.is_nullish()) {

            value = TRY([&]() -> JS::ThrowCompletionOr<GC::Ref<XPath::XPathResult>> {
        if (auto impl = arg4.as_if<XPath::XPathResult>())
            return *impl;
        return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObjectOfType, "XPathResult");
    }());
        }
        return value;
    }(); }));

    [[maybe_unused]] auto R = TRY(throw_dom_exception_if_needed(vm, [&] { return idl_object->evaluate(expression, context_node, resolver, type, result); }));
    return JS::Value(R);
}

} // namespace Web::Bindings
