#include <AK/String.h>
#include <AK/TypeCasts.h>
#include <LibJS/Runtime/Error.h>
#include <LibJS/Runtime/Promise.h>
#include <LibJS/Runtime/VM.h>
#include <LibJS/Runtime/Value.h>
#include <LibJS/Runtime/ValueInlines.h>
#include <LibWeb/Bindings/ExceptionOrUtils.h>
#include <LibWeb/Bindings/IDBFactory.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/IndexedDB/IDBFactory.h>
#include <LibWeb/IndexedDB/IDBOpenDBRequest.h>
#include <LibWeb/WebIDL/AbstractOperations.h>
#include <LibWeb/WebIDL/Promise.h>
#include <LibWeb/WebIDL/Tracing.h>
#include <LibWeb/WebIDL/Types.h>

namespace Web::Bindings {

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

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

void IDBFactoryPrototype::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, "open"_utf16_fly_string, open, 1, default_attributes);

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

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

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

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

void IDBFactoryPrototype::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<IndexedDB::IDBFactory*> impl_from(JS::VM& vm, JS::Value js_value)
{

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

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

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

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

    auto arg1 = vm.argument(1);
    Optional<WebIDL::UnsignedLongLong> version {};
    if (!arg1.is_undefined())
        version = TRY(throw_dom_exception_if_needed(vm, [&] { return WebIDL::convert_to_int<WebIDL::UnsignedLongLong>(vm, arg1, WebIDL::EnforceRange::Yes, WebIDL::Clamp::No); }));

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

JS_DEFINE_NATIVE_FUNCTION(IDBFactoryPrototype::delete_database)
{
    WebIDL::log_trace(vm, "IDBFactoryPrototype::delete_database");
    [[maybe_unused]] auto& realm = *vm.current_realm();
    [[maybe_unused]] IndexedDB::IDBFactory* idl_object = TRY(impl_from(vm));

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

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

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

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

    [[maybe_unused]] auto R = TRY(throw_dom_exception_if_needed(vm, [&] { return idl_object->databases(); }));
        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(IDBFactoryPrototype::cmp)
{
    WebIDL::log_trace(vm, "IDBFactoryPrototype::cmp");
    [[maybe_unused]] auto& realm = *vm.current_realm();
    [[maybe_unused]] IndexedDB::IDBFactory* idl_object = TRY(impl_from(vm));

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

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

    auto arg1 = vm.argument(1);
    auto second = TRY(throw_dom_exception_if_needed(vm, [&] { return arg1; }));

    [[maybe_unused]] auto R = TRY(throw_dom_exception_if_needed(vm, [&] { return idl_object->cmp(first, second); }));
    return JS::Value(static_cast<WebIDL::Short>(R));
}

// https://webidl.spec.whatwg.org/#es-dictionary
JS::ThrowCompletionOr<IDBDatabaseInfo> convert_to_idl_value_for_idb_database_info(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, "IDBDatabaseInfo");

    // 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 IDBDatabaseInfo {
        .name = TRY([&]() -> JS::ThrowCompletionOr<Optional<String>> {
            // 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("name"_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<String> {
        return TRY(WebIDL::to_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 {};
        }()),
        .version = TRY([&]() -> JS::ThrowCompletionOr<Optional<WebIDL::UnsignedLongLong>> {
            // 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("version"_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 WebIDL::convert_to_int<WebIDL::UnsignedLongLong>(vm, js_member_value, WebIDL::EnforceRange::No, WebIDL::Clamp::No); }));

                // 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
