/*
 * Copyright (c) 2022 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/patch/patch_loader.h"

#include "ecmascript/global_handle_collection.h"
#include "ecmascript/interpreter/interpreter-inl.h"
#include "ecmascript/jspandafile/js_pandafile_manager.h"
#include "ecmascript/mem/c_string.h"
#include "ecmascript/napi/include/jsnapi.h"

namespace panda::ecmascript {
PatchErrorCode PatchLoader::LoadPatchInternal(JSThread *thread, const JSPandaFile *baseFile,
                                              const JSPandaFile *patchFile, PatchInfo &patchInfo)
{
    DISALLOW_GARBAGE_COLLECTION;
    EcmaVM *vm = thread->GetEcmaVM();

    // hot reload and hot patch only support merge-abc file.
    if (baseFile->IsBundlePack() || patchFile->IsBundlePack()) {
        LOG_ECMA(ERROR) << "base or patch is not merge abc!";
        return PatchErrorCode::PACKAGE_NOT_ESMODULE;
    }

    // Generate patchInfo for hot reload, hot patch and cold patch.
    patchInfo = PatchLoader::GeneratePatchInfo(patchFile);

    if (!thread->GetCurrentEcmaContext()->HasCachedConstpool(baseFile)) {
        LOG_ECMA(INFO) << "cold patch!";
        return PatchErrorCode::SUCCESS;
    }

    [[maybe_unused]] EcmaHandleScope handleScope(thread);

    // store base constpool in global object for avoid gc.
    uint32_t num = baseFile->GetConstpoolNum();
    GlobalHandleCollection gloalHandleCollection(thread);
    for (uint32_t idx = 0; idx < num; idx++) {
        JSTaggedValue constpool = thread->GetCurrentEcmaContext()->FindConstpool(baseFile, idx);
        if (!constpool.IsHole()) {
            JSHandle<JSTaggedValue> constpoolHandle =
                gloalHandleCollection.NewHandle<JSTaggedValue>(constpool.GetRawData());
            patchInfo.baseConstpools.emplace_back(constpoolHandle);
        }
    }

    // create empty patch constpool for replace method constpool.
    thread->GetCurrentEcmaContext()->CreateAllConstpool(patchFile);
    FindAndReplaceSameMethod(thread, baseFile, patchFile, patchInfo);

    if (!ExecutePatchMain(thread, patchFile, baseFile, patchInfo)) {
        LOG_ECMA(ERROR) << "Execute patch main failed";
        return PatchErrorCode::INTERNAL_ERROR;
    }

    vm->GetJsDebuggerManager()->GetHotReloadManager()->NotifyPatchLoaded(baseFile, patchFile);
    return PatchErrorCode::SUCCESS;
}

bool PatchLoader::ExecutePatchMain(JSThread *thread, const JSPandaFile *patchFile,
                                   const JSPandaFile *baseFile, PatchInfo &patchInfo)
{
    EcmaVM *vm = thread->GetEcmaVM();

    const auto &recordInfos = patchFile->GetJSRecordInfo();
    bool isHotPatch = false;
    bool isNewVersion = patchFile->IsNewVersion();
    for (const auto &item : recordInfos) {
        const CString &recordName = item.first;
        uint32_t mainMethodIndex = patchFile->GetMainMethodIndex(recordName);
        ASSERT(mainMethodIndex != 0);
        EntityId mainMethodId(mainMethodIndex);

        // For HotPatch, generate program and execute for every record.
        if (!isHotPatch) {
            CString mainMethodName = MethodLiteral::GetMethodName(patchFile, mainMethodId);
            if (mainMethodName != JSPandaFile::PATCH_FUNCTION_NAME_0) {
                LOG_ECMA(INFO) << "HotReload no need to execute patch main";
                return true;
            }
            isHotPatch = true;
        }

        JSTaggedValue constpoolVal = JSTaggedValue::Hole();
        if (isNewVersion) {
            constpoolVal = thread->GetCurrentEcmaContext()->FindConstpool(patchFile, mainMethodId);
        } else {
            constpoolVal = thread->GetCurrentEcmaContext()->FindConstpool(patchFile, 0);
        }
        ASSERT(!constpoolVal.IsHole());
        JSHandle<ConstantPool> constpool(thread, constpoolVal);
        MethodLiteral *mainMethodLiteral = patchFile->FindMethodLiteral(mainMethodIndex);
        JSHandle<Program> program = PandaFileTranslator::GenerateProgramInternal(vm, mainMethodLiteral, constpool);

        if (program->GetMainFunction().IsUndefined()) {
            continue;
        }

        // For add a new function, Call patch_main_0.
        JSHandle<JSTaggedValue> undefined = thread->GlobalConstants()->GetHandledUndefined();
        JSHandle<JSFunction> func(thread, program->GetMainFunction());
        JSHandle<JSTaggedValue> module =
            thread->GetCurrentEcmaContext()->GetModuleManager()->HostResolveImportedModuleWithMerge(
                patchFile->GetJSPandaFileDesc(), recordName);
        func->SetModule(thread, module);
        EcmaRuntimeCallInfo *info =
            EcmaInterpreter::NewRuntimeCallInfo(thread, JSHandle<JSTaggedValue>(func), undefined, undefined, 0);
        EcmaInterpreter::Execute(info);

        if (thread->HasPendingException()) {
            // clear exception and rollback.
            thread->ClearException();
            UnloadPatchInternal(thread, patchFile->GetJSPandaFileDesc(), baseFile->GetJSPandaFileDesc(), patchInfo);
            LOG_ECMA(ERROR) << "execute patch main has exception";
            return false;
        }
    }
    return true;
}

PatchErrorCode PatchLoader::UnloadPatchInternal(JSThread *thread, const CString &patchFileName,
                                                const CString &baseFileName, PatchInfo &patchInfo)
{
    std::shared_ptr<JSPandaFile> baseFile = JSPandaFileManager::GetInstance()->FindJSPandaFile(baseFileName);
    if (baseFile == nullptr) {
        LOG_ECMA(ERROR) << "find base jsPandafile failed";
        return PatchErrorCode::FILE_NOT_EXECUTED;
    }

    std::shared_ptr<JSPandaFile> patchFile = JSPandaFileManager::GetInstance()->FindJSPandaFile(patchFileName);
    if (patchFile == nullptr) {
        LOG_ECMA(ERROR) << "find patch jsPandafile failed";
        return PatchErrorCode::FILE_NOT_FOUND;
    }

    const auto &baseMethodInfo = patchInfo.baseMethodInfo;
    if (baseMethodInfo.empty()) {
        LOG_ECMA(INFO) << "no method need to unload";
        return PatchErrorCode::SUCCESS;
    }

    EcmaVM *vm = thread->GetEcmaVM();
    auto baseConstpoolValues = thread->GetCurrentEcmaContext()->FindConstpools(baseFile.get());
    if (!baseConstpoolValues.has_value()) {
        LOG_ECMA(ERROR) << "base constpool is empty";
        return PatchErrorCode::INTERNAL_ERROR;
    }

    for (const auto &item : baseMethodInfo) {
        const auto &methodIndex = item.first;
        ConstantPool *baseConstpool = ConstantPool::Cast(
            (baseConstpoolValues.value().get()[methodIndex.constpoolNum]).GetTaggedObject());

        uint32_t constpoolIndex = methodIndex.constpoolIndex;
        uint32_t literalIndex = methodIndex.literalIndex;
        Method *patchMethod = nullptr;
        if (literalIndex == UINT32_MAX) {
            JSTaggedValue value = baseConstpool->GetObjectFromCache(constpoolIndex);
            ASSERT(value.IsMethod());
            patchMethod = Method::Cast(value.GetTaggedObject());
        } else {
            ClassLiteral *classLiteral = ClassLiteral::Cast(baseConstpool->GetObjectFromCache(constpoolIndex));
            TaggedArray *literalArray = TaggedArray::Cast(classLiteral->GetArray());
            JSTaggedValue value = literalArray->Get(thread, literalIndex);
            ASSERT(value.IsJSFunctionBase());
            JSFunctionBase *func = JSFunctionBase::Cast(value.GetTaggedObject());
            patchMethod = Method::Cast(func->GetMethod().GetTaggedObject());
        }

        MethodLiteral *baseMethodLiteral = item.second;
        JSTaggedValue baseConstpoolValue = thread->GetCurrentEcmaContext()->FindConstpool(
            baseFile.get(), baseMethodLiteral->GetMethodId());
        ReplaceMethod(thread, patchMethod, baseMethodLiteral, baseConstpoolValue);
        LOG_ECMA(INFO) << "Replace base method: "
                       << patchMethod->GetRecordName()
                       << ":" << patchMethod->GetMethodName();
    }

    vm->GetJsDebuggerManager()->GetHotReloadManager()->NotifyPatchUnloaded(patchFile.get());

    // release base constpool.
    CVector<JSHandle<JSTaggedValue>> &baseConstpools = patchInfo.baseConstpools;
    GlobalHandleCollection gloalHandleCollection(thread);
    for (auto &item : baseConstpools) {
        gloalHandleCollection.Dispose(item);
    }

    ClearPatchInfo(thread, patchFileName);
    return PatchErrorCode::SUCCESS;
}

void PatchLoader::ClearPatchInfo(JSThread *thread, const CString &patchFileName)
{
    EcmaVM *vm = thread->GetEcmaVM();

    vm->GetGlobalEnv()->SetGlobalPatch(thread, vm->GetFactory()->EmptyArray());

    // For release patch constpool and JSPandaFile.
    vm->CollectGarbage(TriggerGCType::FULL_GC);

    std::shared_ptr<JSPandaFile> patchFile = JSPandaFileManager::GetInstance()->FindJSPandaFile(patchFileName);
    if (patchFile != nullptr) {
        LOG_ECMA(INFO) << "patch jsPandaFile is not nullptr";
    }
}

void PatchLoader::ReplaceMethod(JSThread *thread,
                                Method *destMethod,
                                MethodLiteral *srcMethodLiteral,
                                JSTaggedValue srcConstpool)
{
    // Update destmethod exclude ExtraLiteralInfo(FunctionKind). Method FunctionKind will be set after
    // building class inheritance relationship or defining gettersetter by value.
    //
    // HotReload of class inheritance will be affected.
    destMethod->SetCallField(srcMethodLiteral->GetCallField());
    destMethod->SetLiteralInfo(srcMethodLiteral->GetLiteralInfo());
    destMethod->SetCodeEntryOrLiteral(reinterpret_cast<uintptr_t>(srcMethodLiteral));
    destMethod->SetNativePointerOrBytecodeArray(const_cast<void *>(srcMethodLiteral->GetNativePointer()));
    destMethod->SetConstantPool(thread, srcConstpool);
    destMethod->SetProfileTypeInfo(thread, JSTaggedValue::Undefined());
    destMethod->SetAotCodeBit(false);
}

void PatchLoader::FindAndReplaceSameMethod(JSThread *thread, const JSPandaFile *baseFile,
                                           const JSPandaFile *patchFile, PatchInfo &patchInfo)
{
    auto context = thread->GetCurrentEcmaContext();
    const CMap<int32_t, JSTaggedValue> &baseConstpoolValues = context->FindConstpools(baseFile).value();
    for (const auto &item : baseConstpoolValues) {
        if (item.second.IsHole()) {
            continue;
        }

        ConstantPool *baseConstpool = ConstantPool::Cast(item.second.GetTaggedObject());
        uint32_t constpoolNum = item.first;
        uint32_t baseConstpoolSize = baseConstpool->GetCacheLength();
        for (uint32_t constpoolIndex = 0; constpoolIndex < baseConstpoolSize; constpoolIndex++) {
            JSTaggedValue constpoolValue = baseConstpool->GetObjectFromCache(constpoolIndex);
            if (!constpoolValue.IsMethod() && !constpoolValue.IsClassLiteral()) {
                continue;
            }

            // For normal function and class constructor.
            if (constpoolValue.IsMethod()) {
                Method *baseMethod = Method::Cast(constpoolValue.GetTaggedObject());
                EntityId baseMethodId = baseMethod->GetMethodId();
                MethodLiteral *patchMethodLiteral = FindSameMethod(patchInfo, baseFile, baseMethodId);
                if (patchMethodLiteral == nullptr) {
                    continue;
                }

                JSTaggedValue patchConstpoolValue = context->FindConstpool(patchFile,
                    patchMethodLiteral->GetMethodId());
                ReplaceMethod(thread, baseMethod, patchMethodLiteral, patchConstpoolValue);

                BaseMethodIndex indexs = {constpoolNum, constpoolIndex};
                SaveBaseMethodInfo(patchInfo, baseFile, baseMethodId, indexs);
            } else if (constpoolValue.IsClassLiteral()) {
                // For class literal.
                ClassLiteral *classLiteral = ClassLiteral::Cast(constpoolValue);
                TaggedArray *literalArray = TaggedArray::Cast(classLiteral->GetArray());
                uint32_t literalLength = literalArray->GetLength();
                for (uint32_t literalIndex = 0; literalIndex < literalLength; literalIndex++) {
                    JSTaggedValue literalItem = literalArray->Get(thread, literalIndex);
                    if (!literalItem.IsJSFunctionBase()) {
                        continue;
                    }

                    // Every record is the same in current class literal.
                    JSFunctionBase *func = JSFunctionBase::Cast(literalItem.GetTaggedObject());
                    Method *baseMethod = Method::Cast(func->GetMethod().GetTaggedObject());
                    EntityId baseMethodId = baseMethod->GetMethodId();
                    MethodLiteral *patchMethodLiteral = FindSameMethod(patchInfo, baseFile, baseMethodId);
                    if (patchMethodLiteral == nullptr) {
                        continue;
                    }

                    JSTaggedValue patchConstpoolValue = context->FindConstpool(patchFile,
                        patchMethodLiteral->GetMethodId());
                    ReplaceMethod(thread, baseMethod, patchMethodLiteral, patchConstpoolValue);

                    BaseMethodIndex indexs = {constpoolNum, constpoolIndex, literalIndex};
                    SaveBaseMethodInfo(patchInfo, baseFile, baseMethodId, indexs);
                }
            }
        }
    }
}

MethodLiteral* PatchLoader::FindSameMethod(PatchInfo &patchInfo, const JSPandaFile *baseFile, EntityId baseMethodId)
{
    const CMap<CString, CMap<CString, MethodLiteral*>> &patchMethodLiterals = patchInfo.patchMethodLiterals;
    CString baseRecordName = MethodLiteral::GetRecordName(baseFile, baseMethodId);
    auto recordIter = patchMethodLiterals.find(baseRecordName);
    if (recordIter == patchMethodLiterals.end()) {
        return nullptr;
    }

    CString baseMethodName = MethodLiteral::GetMethodName(baseFile, baseMethodId);
    auto methodIter = recordIter->second.find(baseMethodName);
    if (methodIter == recordIter->second.end()) {
        return nullptr;
    }

    LOG_ECMA(INFO) << "Replace base method: " << baseRecordName << ":" << baseMethodName;
    return methodIter->second;
}

void PatchLoader::SaveBaseMethodInfo(PatchInfo &patchInfo, const JSPandaFile *baseFile,
                                     EntityId baseMethodId, const BaseMethodIndex &indexs)
{
    CMap<BaseMethodIndex, MethodLiteral *> &baseMethodInfo = patchInfo.baseMethodInfo;
    MethodLiteral *baseMethodLiteral = baseFile->FindMethodLiteral(baseMethodId.GetOffset());
    ASSERT(baseMethodLiteral != nullptr);
    baseMethodInfo.emplace(indexs, baseMethodLiteral);
}

PatchInfo PatchLoader::GeneratePatchInfo(const JSPandaFile *patchFile)
{
    const auto &map = patchFile->GetMethodLiteralMap();
    CMap<CString, CMap<CString, MethodLiteral*>> methodLiterals;
    for (const auto &item : map) {
        MethodLiteral *methodLiteral = item.second;
        EntityId methodId = EntityId(item.first);
        CString methodName = MethodLiteral::GetMethodName(patchFile, methodId);
        if (methodName == JSPandaFile::ENTRY_FUNCTION_NAME ||
            methodName == JSPandaFile::PATCH_FUNCTION_NAME_0 ||
            methodName == JSPandaFile::PATCH_FUNCTION_NAME_1) {
            continue;
        }

        CString recordName = MethodLiteral::GetRecordName(patchFile, methodId);
        auto iter = methodLiterals.find(recordName);
        if (iter != methodLiterals.end()) {
            iter->second.emplace(methodName, methodLiteral);
        } else {
            CMap<CString, MethodLiteral*> methodNameInfo = {{methodName, methodLiteral}};
            methodLiterals.emplace(recordName, std::move(methodNameInfo));
        }
    }

    PatchInfo patchInfo;
    patchInfo.patchFileName = patchFile->GetJSPandaFileDesc();
    patchInfo.patchMethodLiterals = std::move(methodLiterals);
    return patchInfo;
}
}  // namespace panda::ecmascript