/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "ecmascript/builtins/builtins_gc.h"
#include "ecmascript/builtins/builtins_ark_tools.h"
#include "ecmascript/mem/heap-inl.h"
#include "ecmascript/js_tagged_value-inl.h"
#include "ecmascript/interpreter/interpreter-inl.h"
#include "common_components/heap/heap.h"

namespace panda::ecmascript::builtins {
JSTaggedValue BuiltinsGc::GetFreeHeapSize(EcmaRuntimeCallInfo *info)
{
    RETURN_IF_DISALLOW_ARKTOOLS(info->GetThread());
    auto *heap = info->GetThread()->GetEcmaVM()->GetHeap();
    auto size = heap->GetHeapLimitSize() - heap->GetHeapObjectSize();
    return JSTaggedValue(static_cast<int64_t>(size));
}

JSTaggedValue BuiltinsGc::GetReservedHeapSize(EcmaRuntimeCallInfo *info)
{
    RETURN_IF_DISALLOW_ARKTOOLS(info->GetThread());
    auto *heap = info->GetThread()->GetEcmaVM()->GetHeap();
    return JSTaggedValue(static_cast<int64_t>(heap->GetHeapLimitSize()));
}

JSTaggedValue BuiltinsGc::GetUsedHeapSize(EcmaRuntimeCallInfo *info)
{
    RETURN_IF_DISALLOW_ARKTOOLS(info->GetThread());
    auto *heap = info->GetThread()->GetEcmaVM()->GetHeap();
    return JSTaggedValue(static_cast<int64_t>(heap->GetHeapObjectSize()));
}

JSTaggedValue BuiltinsGc::GetObjectAddress(EcmaRuntimeCallInfo *info)
{
    RETURN_IF_DISALLOW_ARKTOOLS(info->GetThread());
    JSHandle<JSTaggedValue> h = GetCallArg(info, 0);
    return JSTaggedValue(reinterpret_cast<int64_t>(h->GetHeapObject()));
}

JSTaggedValue BuiltinsGc::GetObjectSpaceType(EcmaRuntimeCallInfo *info)
{
    RETURN_IF_DISALLOW_ARKTOOLS(info->GetThread());
    JSHandle<JSTaggedValue> h = GetCallArg(info, 0);
    auto *region = Region::ObjectAddressToRange(h->GetHeapObject());
    return JSTaggedValue(region->GetSpaceType());
}

JSTaggedValue BuiltinsGc::RegisterNativeAllocation(EcmaRuntimeCallInfo *info)
{
    JSHandle<JSTaggedValue> h = GetCallArg(info, 0);
    auto *heap = const_cast<Heap *>(info->GetThread()->GetEcmaVM()->GetHeap());
    auto size = h->GetInt();
    if (size < 0) {
        auto *thread = info->GetThread();
        THROW_RANGE_ERROR_AND_RETURN(thread, "The value must be non negative", JSTaggedValue::Exception());
    }
    heap->IncreaseNativeBindingSize(size);
    heap->TryTriggerFullMarkOrGCByNativeSize();
    WaitAndHandleConcurrentMarkingFinished(heap);
    return JSTaggedValue::Undefined();
}

JSTaggedValue BuiltinsGc::RegisterNativeFree(EcmaRuntimeCallInfo *info)
{
    RETURN_IF_DISALLOW_ARKTOOLS(info->GetThread());
    JSHandle<JSTaggedValue> h = GetCallArg(info, 0);
    auto *heap = const_cast<Heap *>(info->GetThread()->GetEcmaVM()->GetHeap());
    auto size = h->GetInt();
    if (size < 0) {
        auto *thread = info->GetThread();
        THROW_RANGE_ERROR_AND_RETURN(thread, "The value must be non negative", JSTaggedValue::Exception());
    }
    auto allocated = heap->GetNativeBindingSize();
    heap->DecreaseNativeBindingSize(std::min(allocated, static_cast<size_t>(size)));
    return JSTaggedValue::Undefined();
}

JSTaggedValue BuiltinsGc::WaitForFinishGC(EcmaRuntimeCallInfo *info)
{
    RETURN_IF_DISALLOW_ARKTOOLS(info->GetThread());
    auto *heap = const_cast<Heap *>(info->GetThread()->GetEcmaVM()->GetHeap());
    auto gcId = GetCallArg(info, 0)->GetInt();
    if (gcId <= 0) {
        return JSTaggedValue::Undefined();
    }

    WaitAndHandleConcurrentMarkingFinished(heap);

#ifndef NDEBUG
    heap->EnableTriggerCollectionOnNewObject();
#endif
    return JSTaggedValue::Undefined();
}

static bool TriggerSharedGC(JSThread *thread, TriggerGCType cause)
{
    switch (cause) {
        case SHARED_GC:
        case SHARED_PARTIAL_GC:
            SharedHeap::GetInstance()->CollectGarbage<TriggerGCType::SHARED_GC, GCReason::EXTERNAL_TRIGGER>(thread);
            return true;
        case SHARED_FULL_GC:
            SharedHeap::GetInstance()
                ->CollectGarbage<TriggerGCType::SHARED_FULL_GC, GCReason::EXTERNAL_TRIGGER>(thread);
            return true;
        case APPSPAWN_SHARED_FULL_GC:
            SharedHeap::GetInstance()
                ->CollectGarbage<TriggerGCType::APPSPAWN_SHARED_FULL_GC, GCReason::EXTERNAL_TRIGGER>(thread);
            return true;
        default:
            return false;
    }
}

JSTaggedValue BuiltinsGc::StartGC(EcmaRuntimeCallInfo *info)
{
    RETURN_IF_DISALLOW_ARKTOOLS(info->GetThread());
    static int counter = 0;
    auto *thread = info->GetThread();
    auto *heap = const_cast<Heap *>(info->GetThread()->GetEcmaVM()->GetHeap());
    auto cause = StringToGcType(thread, GetCallArg(info, 0).GetTaggedValue());
    auto runGcInPlace = GetCallArg(info, 2).GetTaggedValue().ToBoolean();

    if (cause == GC_TYPE_LAST) {
        THROW_TYPE_ERROR_AND_RETURN(thread, "Invalid GC trigger type", JSTaggedValue::Exception());
    }
    if (TriggerSharedGC(thread, cause)) {
        return JSTaggedValue(0);
    }
    if (cause != OLD_GC) {
        heap->CollectGarbage(cause, GCReason::EXTERNAL_TRIGGER);
        return JSTaggedValue(0);
    }

    heap->SetMarkType(MarkType::MARK_FULL);
    heap->TriggerConcurrentMarking(MarkReason::EXTERNAL_TRIGGER);

    if (heap->GetConcurrentMarker()->IsTriggeredConcurrentMark()) {
        JSHandle<JSTaggedValue> hCallback = GetCallArg(info, 1);
        if (!hCallback->IsUndefinedOrNull()) {
            if (!hCallback->IsJSFunction()) {
                THROW_TYPE_ERROR_AND_RETURN(thread, "Invalid GC callback", JSTaggedValue::Exception());
            }

            auto undefined = thread->GlobalConstants()->GetHandledUndefined();
            auto *calleeInfo = EcmaInterpreter::NewRuntimeCallInfo(thread, hCallback, info->GetThis(), undefined, 0);
            JSFunction::Call(calleeInfo);
        }
    }

    if (runGcInPlace) {
        WaitAndHandleConcurrentMarkingFinished(heap);
        return JSTaggedValue(0);
    }

#ifndef NDEBUG
    heap->DisableTriggerCollectionOnNewObject();
#endif
    return JSTaggedValue(++counter);
}

void BuiltinsGc::WaitAndHandleConcurrentMarkingFinished(Heap *heap)
{
    if (heap->GetConcurrentMarker()->IsTriggeredConcurrentMark()) {
        heap->WaitConcurrentMarkingFinished();
        heap->GetConcurrentMarker()->HandleMarkingFinished(GCReason::EXTERNAL_TRIGGER);
    }
}

JSTaggedValue BuiltinsGc::AllocateArrayObject(EcmaRuntimeCallInfo *info)
{
    RETURN_IF_DISALLOW_ARKTOOLS(info->GetThread());
    auto *thread = info->GetThread();
    auto *factory = thread->GetEcmaVM()->GetFactory();
    ASSERT(thread != nullptr);
    [[maybe_unused]] EcmaHandleScope handleScope(thread);

    int64_t sizeInBytes = 0;

    if (GetCallArg(info, 0)->IsInt()) {
        sizeInBytes = GetCallArg(info, 0)->GetInt();
    } else if (GetCallArg(info, 0)->IsDouble()) {
        sizeInBytes = GetCallArg(info, 0)->GetDouble();
    } else {
        auto err = thread->GetEcmaVM()->GetFactory()->GetJSError(ErrorType::TYPE_ERROR, "The value must be an integer");
        THROW_NEW_ERROR_AND_RETURN_VALUE(thread, err.GetTaggedValue(), JSTaggedValue::Exception());
    }

    if (sizeInBytes < 0) {
        auto err = thread->GetEcmaVM()->GetFactory()->GetJSError(ErrorType::TYPE_ERROR, "The value must be positive");
        THROW_NEW_ERROR_AND_RETURN_VALUE(thread, err.GetTaggedValue(), JSTaggedValue::Exception());
    }

    sizeInBytes = RoundUp(sizeInBytes, sizeof(TaggedType)) - JSArray::SIZE;
    if (sizeInBytes < 0) {
        sizeInBytes = 0;
    }

    uint32_t numElements = static_cast<uint32_t>(sizeInBytes / sizeof(TaggedType));
    auto array = factory->NewJSArray();

    if (numElements > 0) {
        auto elements = factory->NewTaggedArray(numElements);
        if (elements.IsEmpty()) {
            return JSTaggedValue::Exception();
        }

        array->SetElements(thread, elements);
        array->SetArrayLength(thread, numElements);
    }

    return array.GetTaggedValue();
}

TriggerGCType BuiltinsGc::StringToGcType(JSThread *thread, JSTaggedValue cause)
{
    static_assert(GC_TYPE_LAST == 14, "Update this method after TrigerGCType change");
    if (JSTaggedValue::StrictEqual(thread, thread->GlobalConstants()->GetYoungGcCause(), cause)) {
        return YOUNG_GC;
    }
    if (JSTaggedValue::StrictEqual(thread, thread->GlobalConstants()->GetOldGcCause(), cause)) {
        return OLD_GC;
    }
    if (JSTaggedValue::StrictEqual(thread, thread->GlobalConstants()->GetFullGcCause(), cause)) {
        return FULL_GC;
    }
    if (JSTaggedValue::StrictEqual(thread, thread->GlobalConstants()->GetAppSpawnFullGcCause(), cause)) {
        return APPSPAWN_FULL_GC;
    }
    if (JSTaggedValue::StrictEqual(thread, thread->GlobalConstants()->GetSharedGcCause(), cause)) {
        return SHARED_GC;
    }
    if (JSTaggedValue::StrictEqual(thread, thread->GlobalConstants()->GetSharedPartialGcCause(), cause)) {
        return SHARED_PARTIAL_GC;
    }
    if (JSTaggedValue::StrictEqual(thread, thread->GlobalConstants()->GetSharedFullGcCause(), cause)) {
        return SHARED_FULL_GC;
    }
    if (JSTaggedValue::StrictEqual(thread, thread->GlobalConstants()->GetAppSpawnSharedFullGcCause(), cause)) {
        return APPSPAWN_SHARED_FULL_GC;
    }
    if (Runtime::GetInstance()->IsHybridVm() &&
        JSTaggedValue::StrictEqual(thread, thread->GlobalConstants()->GetUnifiedGcCause(), cause)) {
        return UNIFIED_GC;
    }
    if (JSTaggedValue::StrictEqual(thread, thread->GlobalConstants()->GetCMSGcCause(), cause)) {
        return CMS_GC;
    }
    if (JSTaggedValue::StrictEqual(thread, thread->GlobalConstants()->GetStickyCMSGcCause(), cause)) {
        return STICKY_CMS_GC;
    }
    if (JSTaggedValue::StrictEqual(thread, thread->GlobalConstants()->GetLocalCCGcCause(), cause)) {
        return LOCAL_CC;
    }
    return GC_TYPE_LAST;
}
}  // namespace panda::ecmascript::builtins