/*
 * Copyright (c) 2024, Feng Yu <f3n67u@outlook.com>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <LibJS/Runtime/Realm.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Clipboard/ClipboardItem.h>
#include <LibWeb/FileAPI/Blob.h>
#include <LibWeb/MimeSniff/MimeType.h>
#include <LibWeb/WebIDL/Promise.h>

namespace Web::Clipboard {

GC_DEFINE_ALLOCATOR(ClipboardItem);

// https://w3c.github.io/clipboard-apis/#dom-clipboarditem-clipboarditem
WebIDL::ExceptionOr<GC::Ref<ClipboardItem>> ClipboardItem::construct_impl(JS::Realm& realm, OrderedHashMap<String, GC::Root<WebIDL::Promise>> const& items, ClipboardItemOptions const& options)
{
    // 1. If items is empty, then throw a TypeError.
    if (items.is_empty())
        return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Items cannot be empty"sv };

    // 2. If options is empty, then set options["presentationStyle"] = "unspecified".
    // NOTE: This step is handled by presentationStyle's default value in ClipboardItemOptions.

    // 3. Set this's clipboard item to a new clipboard item.
    auto clipboard_item = realm.create<ClipboardItem>(realm);

    // 4. Set this's clipboard item's presentation style to options["presentationStyle"].
    clipboard_item->m_presentation_style = options.presentation_style;

    // 5. Let types be a list of DOMString.
    Vector<String> types;

    // 6. For each (key, value) in items:
    for (auto const& [key, value] : items) {
        // 2. Let isCustom be false.
        bool is_custom = false;

        // 3. If key starts with `"web "` prefix, then:
        auto key_without_prefix = key;
        if (key.starts_with_bytes(WEB_CUSTOM_FORMAT_PREFIX)) {
            // 1. Remove `"web "` prefix and assign the remaining string to key.
            key_without_prefix = MUST(key.substring_from_byte_offset(WEB_CUSTOM_FORMAT_PREFIX.length()));

            // 2. Set isCustom to true.
            is_custom = true;
        }

        // 5. Let mimeType be the result of parsing a MIME type given key.
        auto mime_type = MimeSniff::MimeType::parse(key_without_prefix);

        // 6. If mimeType is failure, then throw a TypeError.
        if (!mime_type.has_value()) {
            return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, MUST(String::formatted("Invalid MIME type: {}", key)) };
        }

        auto mime_type_serialized = mime_type->serialized();

        // 7. If this's clipboard item's list of representations contains a representation whose MIME type
        //    is mimeType and whose [representation/isCustom] is isCustom, then throw a TypeError.
        auto existing = clipboard_item->m_representations.find_if([&](auto const& item) {
            return item.mime_type == mime_type_serialized && item.is_custom == is_custom;
        });
        if (!existing.is_end()) {
            return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, MUST(String::formatted("Duplicate MIME type: {}", key)) };
        }

        // 11. Let mimeTypeString be the result of serializing a MIME type with mimeType.
        // 12. If isCustom is true, prefix mimeTypeString with `"web "`.
        auto mime_type_string = is_custom ? MUST(String::formatted("{}{}", WEB_CUSTOM_FORMAT_PREFIX, mime_type_serialized)) : mime_type_serialized;

        // 13. Add mimeTypeString to types.
        types.append(move(mime_type_string));

        // 1. Let representation be a new representation.
        // 4. Set representation’s isCustom flag to isCustom.
        // 8. Set representation’s MIME type to mimeType.
        // 9. Set representation’s data to value.
        // 10. Append representation to this's clipboard item's list of representations.
        clipboard_item->m_representations.empend(move(mime_type_serialized), is_custom, *value);
    }

    // 7. Set this's types array to the result of running create a frozen array from types.
    clipboard_item->m_types = types;

    return clipboard_item;
}

// https://w3c.github.io/clipboard-apis/#dom-clipboarditem-gettype
WebIDL::ExceptionOr<GC::Ref<WebIDL::Promise>> ClipboardItem::get_type(String const& type)
{
    // 1. Let realm be this's relevant realm.
    auto& realm = HTML::relevant_realm(*this);

    // 2. Let isCustom be false.
    bool is_custom = false;

    // 3. If type starts with `"web "` prefix, then:
    auto type_without_prefix = type;
    if (type.starts_with_bytes(WEB_CUSTOM_FORMAT_PREFIX)) {
        // 1. Remove `"web "` prefix and assign the remaining string to type.
        type_without_prefix = MUST(type.substring_from_byte_offset(WEB_CUSTOM_FORMAT_PREFIX.length()));

        // 2. Set isCustom to true.
        is_custom = true;
    }

    // 4. Let mimeType be the result of parsing a MIME type given type.
    auto mime_type = MimeSniff::MimeType::parse(type_without_prefix);

    // 5. If mimeType is failure, then throw a TypeError.
    if (!mime_type.has_value()) {
        return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, MUST(String::formatted("Invalid MIME type: {}", type)) };
    }

    auto mime_type_serialized = mime_type->serialized();

    // 6. Let itemTypeList be this's clipboard item's list of representations.
    auto const& item_type_list = m_representations;

    // 7.  Let p be a new promise in realm.
    auto promise = WebIDL::create_promise(realm);

    // 8. For each representation in itemTypeList:
    for (auto const& representation : item_type_list) {
        // 1. If representation’s MIME type is mimeType and representation’s isCustom is isCustom, then:
        if (representation.mime_type == mime_type_serialized && representation.is_custom == is_custom) {
            // 1. Let representationDataPromise be the representation’s data.
            auto representation_data_promise = representation.data;

            // 2. React to representationDataPromise:
            WebIDL::react_to_promise(
                *representation_data_promise,
                GC::create_function(realm.heap(), [&realm, promise, mime_type_serialized](JS::Value value) -> WebIDL::ExceptionOr<JS::Value> {
                    // 1. If v is a DOMString, then follow the below steps:
                    if (value.is_string()) {
                        // 1. Let dataAsBytes be the result of UTF-8 encoding v.
                        auto utf8_string = value.as_string().utf8_string();
                        auto data_as_bytes = MUST(ByteBuffer::copy(utf8_string.bytes()));

                        // 2. Let blobData be a Blob created using dataAsBytes with its type set to mimeType, serialized.
                        auto blob_data = FileAPI::Blob::create(realm, data_as_bytes, mime_type_serialized);

                        // 3. Resolve p with blobData.
                        WebIDL::resolve_promise(realm, promise, blob_data);
                    }
                    // 2. If v is a Blob, then follow the below steps:
                    if (value.is_object() && is<FileAPI::Blob>(value.as_object())) {
                        // 1. Resolve p with v.
                        WebIDL::resolve_promise(realm, promise, value);
                    }

                    return JS::js_undefined();
                }),
                // 2. If representationDataPromise was rejected, then:
                GC::create_function(realm.heap(), [&realm, type, promise](JS::Value) -> WebIDL::ExceptionOr<JS::Value> {
                    // 1. Reject p with "NotFoundError" DOMException in realm.
                    WebIDL::reject_promise(realm, promise, WebIDL::NotFoundError::create(realm, MUST(String::formatted("No data found for MIME type: {}", type))));

                    return JS::js_undefined();
                }));

            // 3. Return p.
            return promise;
        }
    }

    // 9. Reject p with "NotFoundError" DOMException in realm.
    WebIDL::reject_promise(realm, promise, WebIDL::NotFoundError::create(realm, MUST(String::formatted("No data found for MIME type: {}", type))));

    // 10. Return p.
    return promise;
}

// https://w3c.github.io/clipboard-apis/#dom-clipboarditem-supports
bool ClipboardItem::supports(JS::VM&, String const& type)
{
    // 1. If type is in mandatory data types or optional data types, then return true.
    // 2. If not, then return false.
    // TODO: Implement optional data types, like web custom formats and image/svg+xml.
    return any_of(MANDATORY_DATA_TYPES, [&](auto supported) { return supported == type; });
}

ClipboardItem::ClipboardItem(JS::Realm& realm)
    : Bindings::PlatformObject(realm)
{
}

ClipboardItem::~ClipboardItem() = default;

void ClipboardItem::initialize(JS::Realm& realm)
{
    Base::initialize(realm);
    WEB_SET_PROTOTYPE_FOR_INTERFACE(ClipboardItem);
}

void ClipboardItem::visit_edges(Cell::Visitor& visitor)
{
    Base::visit_edges(visitor);
    for (auto& representation : m_representations) {
        visitor.visit(representation.data);
    }
}

}
