#include <AK/Variant.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/CSSMathClamp.h>
#include <LibWeb/Bindings/CSSMathValue.h>
#include <LibWeb/Bindings/ExceptionOrUtils.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/PlatformObject.h>
#include <LibWeb/CSS/CSSMathClamp.h>
#include <LibWeb/CSS/CSSNumericValue.h>
#include <LibWeb/WebIDL/Tracing.h>

namespace Web::Bindings {

void CSSMathClampConstructor::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<CSSMathValuePrototype>(realm, "CSSMathValue"_fly_string));
    object.define_direct_property(vm.names.length, JS::Value(3), JS::Attribute::Configurable);
    object.define_direct_property(vm.names.name, JS::PrimitiveString::create(vm, "CSSMathClamp"_utf16), JS::Attribute::Configurable);
    object.define_direct_property(vm.names.prototype, &ensure_web_prototype<CSSMathClampPrototype>(realm, "CSSMathClamp"_fly_string), 0);
}

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

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

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

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

    auto arg0 = vm.argument(0);
    auto lower = TRY(throw_dom_exception_if_needed(vm, [&] { return [&]() -> JS::ThrowCompletionOr<Variant<double, GC::Ref<CSS::CSSNumericValue>>> {

        if (arg0.is_object()) {
            [[maybe_unused]] auto& object = arg0.as_object();

            if (is<PlatformObject>(object)) {

                if (auto* result = as_if<CSS::CSSNumericValue>(object))
                    return Variant<double, GC::Ref<CSS::CSSNumericValue>> { GC::Ref { *result } };
            }
        }

        if (arg0.is_number()) {
            auto lower_number = TRY([&]() -> JS::ThrowCompletionOr<double> {
        auto x = TRY(arg0.to_double(vm));
        if (isinf(x) || isnan(x))
            return vm.throw_completion<JS::TypeError>(JS::ErrorType::InvalidRestrictedFloatingPointParameter, "lower");
        return x;
    }());
            return Variant<double, GC::Ref<CSS::CSSNumericValue>> { lower_number };
        }

        auto lower_number_fallback = TRY([&]() -> JS::ThrowCompletionOr<double> {
        auto x = TRY(arg0.to_double(vm));
        if (isinf(x) || isnan(x))
            return vm.throw_completion<JS::TypeError>(JS::ErrorType::InvalidRestrictedFloatingPointParameter, "lower");
        return x;
    }());
        return Variant<double, GC::Ref<CSS::CSSNumericValue>> { lower_number_fallback };

        return vm.throw_completion<JS::TypeError>("No union types matched"_utf16);
    }(); }));

    auto arg1 = vm.argument(1);
    auto value = TRY(throw_dom_exception_if_needed(vm, [&] { return [&]() -> JS::ThrowCompletionOr<Variant<double, GC::Ref<CSS::CSSNumericValue>>> {

        if (arg1.is_object()) {
            [[maybe_unused]] auto& object = arg1.as_object();

            if (is<PlatformObject>(object)) {

                if (auto* result = as_if<CSS::CSSNumericValue>(object))
                    return Variant<double, GC::Ref<CSS::CSSNumericValue>> { GC::Ref { *result } };
            }
        }

        if (arg1.is_number()) {
            auto value_number = TRY([&]() -> JS::ThrowCompletionOr<double> {
        auto x = TRY(arg1.to_double(vm));
        if (isinf(x) || isnan(x))
            return vm.throw_completion<JS::TypeError>(JS::ErrorType::InvalidRestrictedFloatingPointParameter, "value");
        return x;
    }());
            return Variant<double, GC::Ref<CSS::CSSNumericValue>> { value_number };
        }

        auto value_number_fallback = TRY([&]() -> JS::ThrowCompletionOr<double> {
        auto x = TRY(arg1.to_double(vm));
        if (isinf(x) || isnan(x))
            return vm.throw_completion<JS::TypeError>(JS::ErrorType::InvalidRestrictedFloatingPointParameter, "value");
        return x;
    }());
        return Variant<double, GC::Ref<CSS::CSSNumericValue>> { value_number_fallback };

        return vm.throw_completion<JS::TypeError>("No union types matched"_utf16);
    }(); }));

    auto arg2 = vm.argument(2);
    auto upper = TRY(throw_dom_exception_if_needed(vm, [&] { return [&]() -> JS::ThrowCompletionOr<Variant<double, GC::Ref<CSS::CSSNumericValue>>> {

        if (arg2.is_object()) {
            [[maybe_unused]] auto& object = arg2.as_object();

            if (is<PlatformObject>(object)) {

                if (auto* result = as_if<CSS::CSSNumericValue>(object))
                    return Variant<double, GC::Ref<CSS::CSSNumericValue>> { GC::Ref { *result } };
            }
        }

        if (arg2.is_number()) {
            auto upper_number = TRY([&]() -> JS::ThrowCompletionOr<double> {
        auto x = TRY(arg2.to_double(vm));
        if (isinf(x) || isnan(x))
            return vm.throw_completion<JS::TypeError>(JS::ErrorType::InvalidRestrictedFloatingPointParameter, "upper");
        return x;
    }());
            return Variant<double, GC::Ref<CSS::CSSNumericValue>> { upper_number };
        }

        auto upper_number_fallback = TRY([&]() -> JS::ThrowCompletionOr<double> {
        auto x = TRY(arg2.to_double(vm));
        if (isinf(x) || isnan(x))
            return vm.throw_completion<JS::TypeError>(JS::ErrorType::InvalidRestrictedFloatingPointParameter, "upper");
        return x;
    }());
        return Variant<double, GC::Ref<CSS::CSSNumericValue>> { upper_number_fallback };

        return vm.throw_completion<JS::TypeError>("No union types matched"_utf16);
    }(); }));

    auto impl = TRY(throw_dom_exception_if_needed(vm, [&] { return CSS::CSSMathClamp::construct_impl(realm, lower, value, upper); }));

    // 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 CSSMathClamp" algorithm
    // (https://webidl.spec.whatwg.org/#js-platform-objects) are currently not handled, or are handled within CSS::CSSMathClamp::construct_impl().

    return *impl;
}

void CSSMathClampPrototype::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<CSSMathValuePrototype>(realm, "CSSMathValue"_fly_string) });

    auto lower_id = "lower"_utf16_fly_string;
    auto native_lower_getter = JS::NativeFunction::create(realm, lower_getter, 0, lower_id, &realm, "get"sv);
    GC::Ptr<JS::NativeFunction> native_lower_setter;

    // 4. Let configurable be false if attr is unforgeable and true otherwise.
    auto lower_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(lower_id, native_lower_getter, native_lower_setter, lower_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 value_id = "value"_utf16_fly_string;
    auto native_value_getter = JS::NativeFunction::create(realm, value_getter, 0, value_id, &realm, "get"sv);
    GC::Ptr<JS::NativeFunction> native_value_setter;

    // 4. Let configurable be false if attr is unforgeable and true otherwise.
    auto value_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(value_id, native_value_getter, native_value_setter, value_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 upper_id = "upper"_utf16_fly_string;
    auto native_upper_getter = JS::NativeFunction::create(realm, upper_getter, 0, upper_id, &realm, "get"sv);
    GC::Ptr<JS::NativeFunction> native_upper_setter;

    // 4. Let configurable be false if attr is unforgeable and true otherwise.
    auto upper_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(upper_id, native_upper_getter, native_upper_setter, upper_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_direct_property(vm.well_known_symbol_to_string_tag(), JS::PrimitiveString::create(vm, "CSSMathClamp"_utf16), JS::Attribute::Configurable);
}

void CSSMathClampPrototype::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<CSS::CSSMathClamp*> impl_from(JS::VM& vm, JS::Value js_value)
{

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

[[maybe_unused]] static JS::ThrowCompletionOr<CSS::CSSMathClamp*> 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(CSSMathClampPrototype::lower_getter)
{
    WebIDL::log_trace(vm, "CSSMathClampPrototype::lower_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->lower(); }));

    return JS::Value(R);
}

JS_DEFINE_NATIVE_FUNCTION(CSSMathClampPrototype::value_getter)
{
    WebIDL::log_trace(vm, "CSSMathClampPrototype::value_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->value(); }));

    return JS::Value(R);
}

JS_DEFINE_NATIVE_FUNCTION(CSSMathClampPrototype::upper_getter)
{
    WebIDL::log_trace(vm, "CSSMathClampPrototype::upper_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->upper(); }));

    return JS::Value(R);
}

} // namespace Web::Bindings
