#include <AK/Variant.h>
#include <LibJS/Runtime/Error.h>
#include <LibJS/Runtime/PrimitiveString.h>
#include <LibJS/Runtime/VM.h>
#include <LibJS/Runtime/Value.h>
#include <LibJS/Runtime/ValueInlines.h>
#include <LibWeb/Bindings/ExceptionOrUtils.h>
#include <LibWeb/Bindings/IDBCursor.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/IndexedDB/IDBCursor.h>
#include <LibWeb/IndexedDB/IDBIndex.h>
#include <LibWeb/IndexedDB/IDBObjectStore.h>
#include <LibWeb/IndexedDB/IDBRequest.h>
#include <LibWeb/WebIDL/AbstractOperations.h>
#include <LibWeb/WebIDL/Tracing.h>
#include <LibWeb/WebIDL/Types.h>

namespace Web::Bindings {

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

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

void IDBCursorPrototype::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());

    auto source_id = "source"_utf16_fly_string;
    auto native_source_getter = JS::NativeFunction::create(realm, source_getter, 0, source_id, &realm, "get"sv);
    GC::Ptr<JS::NativeFunction> native_source_setter;

    // 4. Let configurable be false if attr is unforgeable and true otherwise.
    auto source_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(source_id, native_source_getter, native_source_setter, source_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 direction_id = "direction"_utf16_fly_string;
    auto native_direction_getter = JS::NativeFunction::create(realm, direction_getter, 0, direction_id, &realm, "get"sv);
    GC::Ptr<JS::NativeFunction> native_direction_setter;

    // 4. Let configurable be false if attr is unforgeable and true otherwise.
    auto direction_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(direction_id, native_direction_getter, native_direction_setter, direction_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 key_id = "key"_utf16_fly_string;
    auto native_key_getter = JS::NativeFunction::create(realm, key_getter, 0, key_id, &realm, "get"sv);
    GC::Ptr<JS::NativeFunction> native_key_setter;

    // 4. Let configurable be false if attr is unforgeable and true otherwise.
    auto key_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(key_id, native_key_getter, native_key_setter, key_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 primary_key_id = "primaryKey"_utf16_fly_string;
    auto native_primary_key_getter = JS::NativeFunction::create(realm, primary_key_getter, 0, primary_key_id, &realm, "get"sv);
    GC::Ptr<JS::NativeFunction> native_primary_key_setter;

    // 4. Let configurable be false if attr is unforgeable and true otherwise.
    auto primary_key_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(primary_key_id, native_primary_key_getter, native_primary_key_setter, primary_key_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 request_id = "request"_utf16_fly_string;
    auto native_request_getter = JS::NativeFunction::create(realm, request_getter, 0, request_id, &realm, "get"sv);
    GC::Ptr<JS::NativeFunction> native_request_setter;

    // 4. Let configurable be false if attr is unforgeable and true otherwise.
    auto request_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(request_id, native_request_getter, native_request_setter, request_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_native_function(realm, "advance"_utf16_fly_string, advance, 1, default_attributes);

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

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

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

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

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

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

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

[[maybe_unused]] static JS::ThrowCompletionOr<IndexedDB::IDBCursor*> 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(IDBCursorPrototype::source_getter)
{
    WebIDL::log_trace(vm, "IDBCursorPrototype::source_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->source_handle(); }));

    return R.visit(
        [&](GC::Ref<IndexedDB::IDBObjectStore> const& visited_union_value0) -> JS::Value
        {
            return JS::Value(visited_union_value0);
        },
        [&](GC::Ref<IndexedDB::IDBIndex> const& visited_union_value1) -> JS::Value
        {
            return JS::Value(visited_union_value1);
        }
    );
}

JS_DEFINE_NATIVE_FUNCTION(IDBCursorPrototype::direction_getter)
{
    WebIDL::log_trace(vm, "IDBCursorPrototype::direction_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->direction(); }));

    return JS::PrimitiveString::create(vm, idl_enum_to_string(R));
}

JS_DEFINE_NATIVE_FUNCTION(IDBCursorPrototype::key_getter)
{
    WebIDL::log_trace(vm, "IDBCursorPrototype::key_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->key(); }));

    return R;
}

JS_DEFINE_NATIVE_FUNCTION(IDBCursorPrototype::primary_key_getter)
{
    WebIDL::log_trace(vm, "IDBCursorPrototype::primary_key_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->primary_key(); }));

    return R;
}

JS_DEFINE_NATIVE_FUNCTION(IDBCursorPrototype::request_getter)
{
    WebIDL::log_trace(vm, "IDBCursorPrototype::request_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->request(); }));

    return JS::Value(R);
}

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

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

    auto arg0 = vm.argument(0);
    auto count = TRY(throw_dom_exception_if_needed(vm, [&] { return WebIDL::convert_to_int<WebIDL::UnsignedLong>(vm, arg0, WebIDL::EnforceRange::Yes, WebIDL::Clamp::No); }));

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

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

    auto arg0 = vm.argument(0);
    Optional<JS::Value> key {};
    if (!arg0.is_undefined())
        key = TRY(throw_dom_exception_if_needed(vm, [&] { return arg0; }));

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

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

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

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

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

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

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

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

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

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

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

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

// https://webidl.spec.whatwg.org/#idl-enumeration
JS::ThrowCompletionOr<IDBCursorDirection> convert_to_idl_value_for_idb_cursor_direction(JS::VM& vm, JS::Value value)
{
    // 1. Let S be the result of calling ? ToString(V).
    auto value_as_string = TRY(value.to_utf16_string(vm));

    // 2. If S is not one of E’s enumeration values, then throw a TypeError.
    // 3. Return the enumeration value of type E that is equal to S.
    if (value_as_string == "next"sv)
        return IDBCursorDirection::Next;
    if (value_as_string == "nextunique"sv)
        return IDBCursorDirection::Nextunique;
    if (value_as_string == "prev"sv)
        return IDBCursorDirection::Prev;
    if (value_as_string == "prevunique"sv)
        return IDBCursorDirection::Prevunique;
    return vm.throw_completion<JS::TypeError>(JS::ErrorType::InvalidEnumerationValue, value_as_string, "IDBCursorDirection");
}

// https://webidl.spec.whatwg.org/#idl-enumeration
Utf16String idl_enum_to_string(IDBCursorDirection value)
{
    // The result of converting an IDL enumeration type value to a JavaScript value is the String value that represents the same sequence of code units as the enumeration value.
    switch (value) {
    case IDBCursorDirection::Next:
        return "next"_utf16;
    case IDBCursorDirection::Nextunique:
        return "nextunique"_utf16;
    case IDBCursorDirection::Prev:
        return "prev"_utf16;
    case IDBCursorDirection::Prevunique:
        return "prevunique"_utf16;
    }
    VERIFY_NOT_REACHED();
}

} // namespace Web::Bindings
