/*
 * Copyright (c) 2022-2023, Linus Groh <linusg@serenityos.org>
 * Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <AK/HashTable.h>
#include <AK/JsonArray.h>
#include <AK/JsonObject.h>
#include <AK/JsonValue.h>
#include <AK/NumericLimits.h>
#include <AK/Utf16String.h>
#include <AK/Variant.h>
#include <LibJS/Runtime/AbstractOperations.h>
#include <LibJS/Runtime/Array.h>
#include <LibJS/Runtime/Object.h>
#include <LibJS/Runtime/PrimitiveString.h>
#include <LibWeb/DOM/DOMTokenList.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/HTMLCollection.h>
#include <LibWeb/DOM/NodeList.h>
#include <LibWeb/DOM/ShadowRoot.h>
#include <LibWeb/FileAPI/FileList.h>
#include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/HTMLAllCollection.h>
#include <LibWeb/HTML/HTMLFormControlsCollection.h>
#include <LibWeb/HTML/HTMLOptionsCollection.h>
#include <LibWeb/HTML/WindowProxy.h>
#include <LibWeb/WebDriver/Contexts.h>
#include <LibWeb/WebDriver/ElementReference.h>
#include <LibWeb/WebDriver/JSON.h>

namespace Web::WebDriver {

#define TRY_OR_JS_ERROR(expression)                                                                       \
    ({                                                                                                    \
        auto&& _temporary_result = (expression);                                                          \
        if (_temporary_result.is_error()) [[unlikely]]                                                    \
            return WebDriver::Error::from_code(ErrorCode::JavascriptError, "Script returned an error"sv); \
        static_assert(!::AK::Detail::IsLvalueReference<decltype(_temporary_result.release_value())>,      \
            "Do not return a reference from a fallible expression");                                      \
        _temporary_result.release_value();                                                                \
    })

using SeenMap = HashTable<GC::RawPtr<JS::Object const>>;

// https://w3c.github.io/webdriver/#dfn-collection
static bool is_collection(JS::Object const& value)
{
    // A collection is an Object that implements the Iterable interface, and whose:
    return (
        // - initial value of the toString own property is "Arguments"
        value.has_parameter_map()
        // - instance of Array
        || is<JS::Array>(value)
        // - instance of DOMTokenList
        || is<DOM::DOMTokenList>(value)
        // - instance of FileList
        || is<FileAPI::FileList>(value)
        // - instance of HTMLAllCollection
        || is<HTML::HTMLAllCollection>(value)
        // - instance of HTMLCollection
        || is<DOM::HTMLCollection>(value)
        // - instance of HTMLFormControlsCollection
        || is<HTML::HTMLFormControlsCollection>(value)
        // - instance of HTMLOptionsCollection
        || is<HTML::HTMLOptionsCollection>(value)
        // - instance of NodeList
        || is<DOM::NodeList>(value));
}

// Helper to convert AK::JsonValue to JS::Value (for WebDriver protocol)
static JS::Value json_value_to_js_value(JS::VM& vm, JsonValue const& value);

static JS::Object* json_object_to_js_object(JS::VM& vm, JsonObject const& json_object)
{
    auto& realm = *vm.current_realm();
    auto object = JS::Object::create(realm, realm.intrinsics().object_prototype());
    json_object.for_each_member([&](auto& key, auto& value) {
        object->define_direct_property(Utf16String::from_utf8(key), json_value_to_js_value(vm, value), JS::default_attributes);
    });
    return object;
}

static JS::Array* json_array_to_js_array(JS::VM& vm, JsonArray const& json_array)
{
    auto& realm = *vm.current_realm();
    auto array = MUST(JS::Array::create(realm, 0));
    size_t index = 0;
    json_array.for_each([&](auto& value) {
        array->define_direct_property(index++, json_value_to_js_value(vm, value), JS::default_attributes);
    });
    return array;
}

static JS::Value json_value_to_js_value(JS::VM& vm, JsonValue const& value)
{
    if (value.is_object())
        return JS::Value(json_object_to_js_object(vm, value.as_object()));
    if (value.is_array())
        return JS::Value(json_array_to_js_array(vm, value.as_array()));
    if (value.is_null())
        return JS::js_null();
    if (auto double_value = value.get_double_with_precision_loss(); double_value.has_value())
        return JS::Value(double_value.value());
    if (value.is_string())
        return JS::PrimitiveString::create(vm, value.as_string());
    if (value.is_bool())
        return JS::Value(value.as_bool());
    VERIFY_NOT_REACHED();
}

// https://w3c.github.io/webdriver/#dfn-clone-an-object
template<typename ResultType, typename CloneAlgorithm>
static ErrorOr<ResultType, WebDriver::Error> clone_an_object(HTML::BrowsingContext const& browsing_context, JS::Object const& value, SeenMap& seen, CloneAlgorithm const& clone_algorithm)
{
    static constexpr bool is_json_value = IsSame<ResultType, JsonValue>;

    auto& realm = browsing_context.active_document()->realm();
    auto& vm = realm.vm();

    // 1. If value is in seen, return error with error code javascript error.
    if (seen.contains(value))
        return WebDriver::Error::from_code(ErrorCode::JavascriptError, "Attempted to recursively clone an Object"sv);

    // 2. Append value to seen.
    seen.set(value);

    // 3. Let result be the value of the first matching statement, matching on value:
    auto result = TRY(([&]() -> ErrorOr<ResultType, WebDriver::Error> {
        // -> a collection
        if (is_collection(value)) {
            // A new Array which length property is equal to the result of getting the property length of value.
            auto length_property = TRY_OR_JS_ERROR(value.get(vm.names.length));

            auto length = TRY_OR_JS_ERROR(length_property.to_length(vm));
            if (length > NumericLimits<u32>::max())
                return WebDriver::Error::from_code(ErrorCode::JavascriptError, "Length of Object too large"sv);

            if constexpr (is_json_value)
                return JsonArray { length };
            else
                return TRY_OR_JS_ERROR(JS::Array::create(realm, length));
        }
        // -> Otherwise
        else {
            // A new Object.
            if constexpr (is_json_value)
                return JsonObject {};
            else
                return JS::Object::create(realm, realm.intrinsics().object_prototype());
        }
    }()));

    Optional<WebDriver::Error> error;

    // 4. For each enumerable property in value, run the following substeps:
    (void)value.enumerate_object_properties([&](auto property) -> Optional<JS::Completion> {
        // 1. Let name be the name of the property.
        auto name = MUST(JS::PropertyKey::from_value(vm, property));

        // 2. Let source property value be the result of getting a property named name from value. If doing so causes
        //    script to be run and that script throws an error, return error with error code javascript error.
        auto source_property_value = value.get(name);
        if (source_property_value.is_error()) {
            error = WebDriver::Error::from_code(ErrorCode::JavascriptError, "Script returned an error"sv);
            return JS::normal_completion(JS::js_undefined());
        }

        // 3. Let cloned property result be the result of calling the clone algorithm with session, source property
        //    value and seen.
        auto cloned_property_result = clone_algorithm(browsing_context, source_property_value.value(), seen);

        // 4. If cloned property result is a success, set a property of result with name name and value equal to cloned
        //    property result's data.
        if (!cloned_property_result.is_error()) {
            if constexpr (is_json_value) {
                if (result.is_array() && name.is_number())
                    result.as_array().set(name.as_number(), cloned_property_result.value());
                else if (result.is_object())
                    result.as_object().set(name.to_string().to_utf8(), cloned_property_result.value());
            } else {
                (void)result->set(name, cloned_property_result.value(), JS::Object::ShouldThrowExceptions::No);
            }
        }
        // 5. Otherwise, return cloned property result.
        else {
            error = cloned_property_result.release_error();
            return JS::normal_completion(JS::js_undefined());
        }

        return {};
    });

    if (error.has_value())
        return error.release_value();

    // 5. Remove the last element of seen.
    seen.remove(value);

    // 6. Return success with data result.
    return result;
}

// https://w3c.github.io/webdriver/#dfn-internal-json-clone
static Response internal_json_clone(HTML::BrowsingContext const& browsing_context, JS::Value value, SeenMap& seen)
{
    auto& vm = browsing_context.vm();

    // To internal JSON clone given session, value and seen, return the value of the first matching statement, matching
    // on value:

    // -> undefined
    // -> null
    if (value.is_nullish()) {
        // Return success with data null.
        return JsonValue {};
    }

    // -> type Boolean
    // -> type Number
    // -> type String
    //     Return success with data value.
    if (value.is_boolean())
        return JsonValue { value.as_bool() };
    if (value.is_number())
        return JsonValue { value.as_double() };
    if (value.is_string())
        return JsonValue { value.as_string().utf8_string() };

    // AD-HOC: BigInt and Symbol not mentioned anywhere in the WebDriver spec, as it references ES5.
    //         It assumes that all primitives are handled above, and the value is an object for the remaining steps.
    if (value.is_bigint())
        return WebDriver::Error::from_code(ErrorCode::JavascriptError, "Cannot clone a BigInt"sv);
    if (value.is_symbol())
        return WebDriver::Error::from_code(ErrorCode::JavascriptError, "Cannot clone a Symbol"sv);

    VERIFY(value.is_object());
    auto const& object = static_cast<JS::Object const&>(value.as_object());

    // -> instance of Element
    if (is<DOM::Element>(object)) {
        auto const& element = static_cast<DOM::Element const&>(object);

        // If the element is stale, return error with error code stale element reference.
        if (is_element_stale(element)) {
            return WebDriver::Error::from_code(ErrorCode::StaleElementReference, "Referenced element has become stale"sv);
        }
        // Otherwise:
        else {
            // 1. Let reference be the web element reference object for session and value.
            auto reference = web_element_reference_object(browsing_context, element);

            // 2. Return success with data reference.
            return JsonValue { move(reference) };
        }
    }

    // -> instance of ShadowRoot
    if (is<DOM::ShadowRoot>(object)) {
        auto const& shadow_root = static_cast<DOM::ShadowRoot const&>(object);

        // If the shadow root is detached, return error with error code detached shadow root.
        if (is_shadow_root_detached(shadow_root)) {
            return WebDriver::Error::from_code(ErrorCode::DetachedShadowRoot, "Referenced shadow root has become detached"sv);
        }
        // Otherwise:
        else {
            // 1. Let reference be the shadow root reference object for session and value.
            auto reference = shadow_root_reference_object(browsing_context, shadow_root);

            // 2. Return success with data reference.
            return JsonValue { move(reference) };
        }
    }

    // -> a WindowProxy object
    if (is<HTML::WindowProxy>(object)) {
        auto const& window_proxy = static_cast<HTML::WindowProxy const&>(object);

        // If the associated browsing context of the WindowProxy object in value has been destroyed, return error
        // with error code stale element reference.
        if (window_proxy.associated_browsing_context()->has_navigable_been_destroyed()) {
            return WebDriver::Error::from_code(ErrorCode::StaleElementReference, "Browsing context has been discarded"sv);
        }
        // Otherwise:
        else {
            // 1. Let reference be the WindowProxy reference object for value.
            auto reference = window_proxy_reference_object(window_proxy);

            // 2. Return success with data reference.
            return JsonValue { move(reference) };
        }
    }

    // -> has an own property named "toJSON" that is a Function
    if (auto to_json = object.get_without_side_effects(vm.names.toJSON); to_json.is_function()) {
        // Return success with the value returned by Function.[[Call]](toJSON) with value as the this value.
        auto to_json_result = TRY_OR_JS_ERROR(JS::call(vm, to_json.as_function(), value));
        if (!to_json_result.is_string())
            return WebDriver::Error::from_code(ErrorCode::JavascriptError, "toJSON did not return a String"sv);

        return JsonValue { to_json_result.as_string().utf8_string() };
    }

    // -> Otherwise
    // 1. Let result be clone an object with session value and seen, and internal JSON clone as the clone algorithm.
    auto result = TRY(clone_an_object<JsonValue>(browsing_context, object, seen, internal_json_clone));

    // 2. Return success with data result.
    return result;
}

// https://w3c.github.io/webdriver/#dfn-json-clone
Response json_clone(HTML::BrowsingContext const& browsing_context, JS::Value value)
{
    SeenMap seen;

    // To JSON clone given session and value, return the result of internal JSON clone with session, value and an empty List.
    return internal_json_clone(browsing_context, value, seen);
}

// https://w3c.github.io/webdriver/#dfn-json-deserialize
static ErrorOr<JS::Value, WebDriver::Error> internal_json_deserialize(HTML::BrowsingContext const& browsing_context, JS::Value value, SeenMap& seen)
{
    // 1. If seen is not provided, let seen be an empty List.
    // 2. Jump to the first appropriate step below:
    // 3. Matching on value:
    // -> undefined
    // -> null
    // -> type Boolean
    // -> type Number
    // -> type String
    if (value.is_nullish() || value.is_boolean() || value.is_number() || value.is_string()) {
        // Return success with data value.
        return value;
    }

    // -> Object that represents a web element
    if (represents_a_web_element(value)) {
        // Return the deserialized web element of value.
        return deserialize_web_element(browsing_context, value.as_object());
    }

    // -> Object that represents a shadow root
    if (represents_a_shadow_root(value)) {
        // Return the deserialized shadow root of value.
        return deserialize_shadow_root(browsing_context, value.as_object());
    }

    // -> Object that represents a web frame
    if (represents_a_web_frame(value)) {
        // Return the deserialized web frame of value.
        return deserialize_web_frame(value.as_object());
    }

    // -> Object that represents a web window
    if (represents_a_web_window(value)) {
        // Return the deserialized web window of value.
        return deserialize_web_window(value.as_object());
    }

    // -> instance of Array
    // -> instance of Object
    if (value.is_object()) {
        // Return clone an object algorithm with session, value and seen, and the JSON deserialize algorithm as the
        // clone algorithm.
        return clone_an_object<GC::Ref<JS::Object>>(browsing_context, value.as_object(), seen, internal_json_deserialize);
    }

    return WebDriver::Error::from_code(ErrorCode::JavascriptError, "Unrecognized value type"sv);
}

// https://w3c.github.io/webdriver/#dfn-json-deserialize
ErrorOr<JS::Value, WebDriver::Error> json_deserialize(HTML::BrowsingContext const& browsing_context, JsonValue const& value)
{
    auto& vm = browsing_context.vm();

    SeenMap seen;
    return internal_json_deserialize(browsing_context, json_value_to_js_value(vm, value), seen);
}

}
