/*
 * Copyright (c) 2020-2023, Linus Groh <linusg@serenityos.org>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <AK/Function.h>
#include <AK/StringBuilder.h>
#include <AK/TypeCasts.h>
#include <LibGC/RootVector.h>
#include <LibJS/Runtime/AbstractOperations.h>
#include <LibJS/Runtime/BoundFunction.h>
#include <LibJS/Runtime/ECMAScriptFunctionObject.h>
#include <LibJS/Runtime/Error.h>
#include <LibJS/Runtime/FunctionPrototype.h>
#include <LibJS/Runtime/GlobalObject.h>
#include <LibJS/Runtime/NativeFunction.h>

namespace JS {

GC_DEFINE_ALLOCATOR(FunctionPrototype);

FunctionPrototype::FunctionPrototype(Realm& realm)
    : FunctionObject(realm.intrinsics().object_prototype())
{
}

void FunctionPrototype::initialize(Realm& realm)
{
    auto& vm = this->vm();
    Base::initialize(realm);
    u8 attr = Attribute::Writable | Attribute::Configurable;
    define_native_function(realm, vm.names.apply, apply, 2, attr);
    define_native_function(realm, vm.names.bind, bind, 1, attr);
    define_native_function(realm, vm.names.call, call, 1, attr);
    define_native_function(realm, vm.names.toString, to_string, 0, attr);
    define_native_function(realm, vm.well_known_symbol_has_instance(), symbol_has_instance, 1, 0, Bytecode::Builtin::OrdinaryHasInstance);
    define_direct_property(vm.names.length, Value(0), Attribute::Configurable);
    define_direct_property(vm.names.name, PrimitiveString::create(vm, String {}), Attribute::Configurable);
}

ThrowCompletionOr<Value> FunctionPrototype::internal_call(ExecutionContext&, Value)
{
    // The Function prototype object:
    // - accepts any arguments and returns undefined when invoked.
    return js_undefined();
}

// 20.2.3.1 Function.prototype.apply ( thisArg, argArray ), https://tc39.es/ecma262/#sec-function.prototype.apply
JS_DEFINE_NATIVE_FUNCTION(FunctionPrototype::apply)
{
    // 1. Let func be the this value.
    auto function_value = vm.this_value();

    // 2. If IsCallable(func) is false, throw a TypeError exception.
    if (!function_value.is_function())
        return vm.throw_completion<TypeError>(ErrorType::NotAFunction, function_value);

    auto& function = static_cast<FunctionObject&>(function_value.as_object());

    auto this_arg = vm.argument(0);
    auto arg_array = vm.argument(1);

    // 3. If argArray is undefined or null, then
    if (arg_array.is_nullish()) {
        // FIXME: a. Perform PrepareForTailCall().

        // b. Return ? Call(func, thisArg).
        return TRY(JS::call(vm, function, this_arg));
    }

    // NOTE: Do the check performed by CreateListFromArrayLike here, so we could avoid branching in optimized code path.
    if (!arg_array.is_object())
        return vm.throw_completion<TypeError>(ErrorType::NotAnObject, arg_array);

    // OPTIMIZATION: If argArray has a simple indexed storage without holes and doesn't interfere with indexed property access,
    //               we can skip CreateListFromArrayLike and directly use the storage elements.
    auto& arg_array_object = arg_array.as_object();
    if (!arg_array_object.may_interfere_with_indexed_property_access() && arg_array_object.indexed_storage_kind() == IndexedStorageKind::Packed) {
        auto length = TRY(length_of_array_like(vm, arg_array_object));
        auto span = arg_array_object.indexed_packed_elements_span();
        if (span.size() >= length)
            return TRY(JS::call(vm, function, this_arg, span.slice(0, length)));
    }

    // 4. Let argList be ? CreateListFromArrayLike(argArray).
    auto arguments = TRY(create_list_from_array_like(vm, arg_array));

    // FIXME: 5. Perform PrepareForTailCall().

    // 6. Return ? Call(func, thisArg, argList).
    return TRY(JS::call(vm, function, this_arg, arguments.span()));
}

// 20.2.3.2 Function.prototype.bind ( thisArg, ...args ), https://tc39.es/ecma262/#sec-function.prototype.bind
JS_DEFINE_NATIVE_FUNCTION(FunctionPrototype::bind)
{
    auto& realm = *vm.current_realm();

    auto this_argument = vm.argument(0);

    // 1. Let Target be the this value.
    auto target_value = vm.this_value();

    // 2. If IsCallable(Target) is false, throw a TypeError exception.
    if (!target_value.is_function())
        return vm.throw_completion<TypeError>(ErrorType::NotAFunction, target_value);

    auto& target = static_cast<FunctionObject&>(target_value.as_object());

    Vector<Value> arguments;
    if (vm.argument_count() > 1) {
        arguments.append(vm.running_execution_context().arguments_span().slice(1).data(), vm.argument_count() - 1);
    }

    // 3. Let F be ? BoundFunctionCreate(Target, thisArg, args).
    auto function = TRY(BoundFunction::create(realm, target, this_argument, move(arguments)));

    // 4. Let L be 0.
    double length = 0;

    // 5. Let targetHasLength be ? HasOwnProperty(Target, "length").
    auto target_has_length = TRY(target.has_own_property(vm.names.length));

    // 6. If targetHasLength is true, then
    if (target_has_length) {
        // a. Let targetLen be ? Get(Target, "length").
        auto target_length = TRY(target.get(vm.names.length));

        // b. If targetLen is a Number, then
        if (target_length.is_number()) {
            // i. If targetLen is +∞𝔽, then
            if (target_length.is_positive_infinity()) {
                // 1. Set L to +∞.
                length = target_length.as_double();
            }
            // ii. Else if targetLen is -∞𝔽, then
            else if (target_length.is_negative_infinity()) {
                // 1. Set L to 0.
                length = 0;
            }
            // iii. Else,
            else {
                // 1. Let targetLenAsInt be ! ToIntegerOrInfinity(targetLen).
                auto target_length_as_int = MUST(target_length.to_integer_or_infinity(vm));

                // 2. Assert: targetLenAsInt is finite.
                VERIFY(!isinf(target_length_as_int));

                // 3. Let argCount be the number of elements in args.
                auto arg_count = vm.argument_count() > 1 ? vm.argument_count() - 1 : 0;

                // 4. Set L to max(targetLenAsInt - argCount, 0).
                length = max(target_length_as_int - arg_count, 0.0);
            }
        }
    }

    // 7. Perform SetFunctionLength(F, L).
    function->set_function_length(length);

    // 8. Let targetName be ? Get(Target, "name").
    auto target_name = TRY(target.get(vm.names.name));

    // 9. If targetName is not a String, set targetName to the empty String.
    // 10. Perform SetFunctionName(F, targetName, "bound").
    function->set_function_name({ target_name.is_string() ? target_name.as_string().utf16_string() : Utf16String {} }, "bound"sv);

    // 11. Return F.
    return function;
}

// 20.2.3.3 Function.prototype.call ( thisArg, ...args ), https://tc39.es/ecma262/#sec-function.prototype.call
JS_DEFINE_NATIVE_FUNCTION(FunctionPrototype::call)
{
    // 1. Let func be the this value.
    auto function_value = vm.this_value();

    // 2. If IsCallable(func) is false, throw a TypeError exception.
    if (!function_value.is_function())
        return vm.throw_completion<TypeError>(ErrorType::NotAFunction, function_value);

    auto& function = static_cast<FunctionObject&>(function_value.as_object());

    // FIXME: 3. Perform PrepareForTailCall().

    auto this_arg = vm.argument(0);
    auto args = vm.argument_count() > 1 ? vm.running_execution_context().arguments_span().slice(1) : ReadonlySpan<Value> {};

    // 4. Return ? Call(func, thisArg, args).
    return TRY(JS::call(vm, function, this_arg, args));
}

// 20.2.3.5 Function.prototype.toString ( ), https://tc39.es/ecma262/#sec-function.prototype.tostring
JS_DEFINE_NATIVE_FUNCTION(FunctionPrototype::to_string)
{
    // 1. Let func be the this value.
    auto function_value = vm.this_value();

    // OPTIMIZATION: If func is not a function, bail out early. The order of this step is not observable.
    if (!function_value.is_function()) {
        // 5. Throw a TypeError exception.
        return vm.throw_completion<TypeError>(ErrorType::NotAnObjectOfType, "Function");
    }

    auto& function = function_value.as_function();

    // 2. If Type(func) is Object and func has a [[SourceText]] internal slot and func.[[SourceText]] is a sequence of Unicode code points and HostHasSourceTextAvailable(func) is true, then
    if (auto const* ecma_script_function_object = as_if<ECMAScriptFunctionObject>(function)) {
        // a. Return CodePointsToString(func.[[SourceText]]).
        return PrimitiveString::create(vm, ecma_script_function_object->source_text());
    }

    // 3. If func is a built-in function object, return an implementation-defined String source code representation of func. The representation must have the syntax of a NativeFunction. Additionally, if func has an [[InitialName]] internal slot and func.[[InitialName]] is a String, the portion of the returned String that would be matched by NativeFunctionAccessor[opt] PropertyName must be the value of func.[[InitialName]].
    if (auto const* native_function = as_if<NativeFunction>(function)) {
        // NOTE: once we remove name(), the fallback here can simply be an empty string.
        auto const name = native_function->initial_name().value_or(native_function->name());
        return PrimitiveString::create(vm, ByteString::formatted("function {}() {{ [native code] }}", name));
    }

    // 4. If Type(func) is Object and IsCallable(func) is true, return an implementation-defined String source code representation of func. The representation must have the syntax of a NativeFunction.
    // NOTE: ProxyObject, BoundFunction, WrappedFunction
    return PrimitiveString::create(vm, "function () { [native code] }"_string);
}

// 20.2.3.6 Function.prototype [ @@hasInstance ] ( V ), https://tc39.es/ecma262/#sec-function.prototype-@@hasinstance
JS_DEFINE_NATIVE_FUNCTION(FunctionPrototype::symbol_has_instance)
{
    // 1. Let F be the this value.
    // 2. Return ? OrdinaryHasInstance(F, V).
    return TRY(ordinary_has_instance(vm, vm.argument(0), vm.this_value()));
}

Utf16String FunctionPrototype::name_for_call_stack() const
{
    return "(Function.prototype)"_utf16;
}

}
