/**
 * Copyright (c) 2021-2026 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 "callExpression.h"

#include <compiler/core/compilerContext.h>
#include <compiler/core/pandagen.h>
#include <ir/base/classDefinition.h>
#include <ir/base/scriptFunction.h>
#include <ir/base/spreadElement.h>
#include <ir/expressions/chainExpression.h>
#include <ir/expressions/memberExpression.h>
#include <ir/ts/tsAsExpression.h>
#include <ir/ts/tsNonNullExpression.h>
#include <ir/ts/tsTypeAssertion.h>
#include <ir/ts/tsTypeParameterInstantiation.h>

namespace panda::es2panda::ir {

void CallExpression::Iterate(const NodeTraverser &cb) const
{
    cb(callee_);

    if (typeParams_) {
        cb(typeParams_);
    }

    for (auto *it : arguments_) {
        cb(it);
    }
}

void CallExpression::Dump(ir::AstDumper *dumper) const
{
    dumper->Add({{"type", "CallExpression"},
                 {"callee", callee_},
                 {"arguments", arguments_},
                 {"optional", optional_},
                 {"typeParameters", AstDumper::Optional(typeParams_)}});
}

compiler::VReg CallExpression::CreateSpreadArguments(compiler::PandaGen *pg) const
{
    compiler::VReg argsObj = pg->AllocReg();
    pg->CreateArray(this, arguments_, argsObj);

    return argsObj;
}

void CallExpression::CompileSuperCallWithSpreadArgs(compiler::PandaGen *pg) const
{
    compiler::RegScope paramScope(pg);
    const ir::ScriptFunction *constructorFunc = util::Helpers::GetContainingConstructor(this);
    CHECK_NOT_NULL(constructorFunc);
    // For super call in default constructor.
    if (constructorFunc->HasFlag(ir::ScriptFunctionFlags::GENERATED_CONSTRUCTOR)) {
        // Use callruntime.supercallforwardallargs to optimize super call in default constructor since api13.
        if (pg->Binder()->Program()->TargetApiVersion() >= util::Helpers::SUPER_CALL_OPT_MIN_SUPPORTED_API_VERSION) {
            compiler::VReg funcObj = pg->AllocReg();
            pg->GetFunctionObject(this);
            pg->StoreAccumulator(this, funcObj);
            pg->SuperCallForwardAllArgs(this, funcObj);
        } else {
            compiler::VReg argsObj = pg->AllocReg();
            arguments_[0]->AsSpreadElement()->Argument()->Compile(pg);
            pg->StoreAccumulator(this, argsObj);
            pg->GetFunctionObject(this);
            pg->SuperCallSpread(this, argsObj);
        }
    } else {
        compiler::VReg argsObj = CreateSpreadArguments(pg);
        pg->GetFunctionObject(this);
        pg->SuperCallSpread(this, argsObj);
    }
}

void CallExpression::CompileSuperCall(compiler::PandaGen *pg, bool containsSpread) const
{
    if (containsSpread) {
        CompileSuperCallWithSpreadArgs(pg);
    } else {
        compiler::RegScope paramScope(pg);
        compiler::VReg argStart {};

        if (arguments_.empty()) {
            argStart = pg->AllocReg();
            pg->LoadConst(this, compiler::Constant::JS_UNDEFINED);
            pg->StoreAccumulator(this, argStart);
        } else {
            argStart = pg->NextReg();
        }

        for (const auto *it : arguments_) {
            compiler::VReg arg = pg->AllocReg();
            it->Compile(pg);
            pg->StoreAccumulator(it, arg);
        }

        pg->SuperCall(this, argStart, arguments_.size());
    }

    compiler::VReg newThis = pg->AllocReg();
    pg->StoreAccumulator(this, newThis);

    pg->GetThis(this);
    pg->ThrowIfSuperNotCorrectCall(this, 1);

    pg->LoadAccumulator(this, newThis);
    pg->SetThis(this);

    const auto *classDef = util::Helpers::GetClassDefiniton(util::Helpers::GetContainingConstructor(this));
    if (classDef->NeedInstanceInitializer()) {
        auto thisReg = pg->AllocReg();
        pg->MoveVreg(this, thisReg, newThis);

        auto [level, slot] = pg->Scope()->Find(classDef->InstanceInitializer()->Key());
        pg->LoadLexicalVar(this, level, slot);

        pg->CallInit(this, thisReg);
        pg->GetThis(this);
    }
}

void CallExpression::Compile(compiler::PandaGen *pg) const
{
    const ir::Expression *realCallee = callee_;
    while (realCallee->IsTSNonNullExpression() || realCallee->IsTSAsExpression() || realCallee->IsTSTypeAssertion()) {
        if (realCallee->IsTSNonNullExpression()) {
            realCallee = realCallee->AsTSNonNullExpression()->Expr();
        } else if (realCallee->IsTSAsExpression()) {
            realCallee = realCallee->AsTSAsExpression()->Expr();
        } else if (realCallee->IsTSTypeAssertion()) {
            realCallee = realCallee->AsTSTypeAssertion()->GetExpression();
        }
    }

    if (realCallee->IsCallExpression() || realCallee->IsNewExpression()) {
        if (pg->TryCompileFunctionCallOrNewExpression(realCallee)) {
            return;
        }
    }

    compiler::RegScope rs(pg);
    bool containsSpread = util::Helpers::ContainSpreadElement(arguments_);

    if (callee_->IsSuperExpression()) {
        CompileSuperCall(pg, containsSpread);
        return;
    }

    compiler::VReg callee = pg->AllocReg();
    bool hasThis = false;
    compiler::VReg thisReg {};
    bool isLdName = false;
    bool isSupportCallWithName = pg->Context()->EnableCallableName();

    if (realCallee->IsMemberExpression()) {
        hasThis = true;
        thisReg = pg->AllocReg();

        compiler::RegScope mrs(pg);
        realCallee->AsMemberExpression()->Compile(pg, thisReg);
        isLdName = !realCallee->AsMemberExpression()->IsComputed() &&
                   realCallee->AsMemberExpression()->Property()->IsIdentifier();
    } else if (realCallee->IsChainExpression()) {
        hasThis = realCallee->AsChainExpression()->GetExpression()->IsMemberExpression();
        if (hasThis) {
            // Guaranteed by implementation in callThis, thisVReg is always the next register of callee.
            thisVReg_ = callee + 1;
        }
        realCallee->AsChainExpression()->Compile(pg);
    } else {
        realCallee->Compile(pg);
    }

    if (realCallee->IsMemberExpression()) {
        pg->StoreAccumulator(realCallee->AsMemberExpression()->Property(), callee);
    } else {
        pg->StoreAccumulator(this, callee);
    }
    pg->GetOptionalChain()->CheckNullish(optional_, callee);

    if (containsSpread) {
        if (!hasThis) {
            thisReg = pg->AllocReg();
            pg->LoadConst(this, compiler::Constant::JS_UNDEFINED);
            pg->StoreAccumulator(this, thisReg);
        }

        compiler::VReg argsObj = CreateSpreadArguments(pg);
        pg->CallSpread(this, callee, thisReg, argsObj);
        return;
    }

    for (const auto *it : arguments_) {
        it->Compile(pg);
        compiler::VReg arg = pg->AllocReg();
        pg->StoreAccumulator(it, arg);
    }

    if (hasThis) {
        /*
         * To obtain more accurate line number information in the MemberExpression scenario,
         * bind CallThis to the property node instead of the entire callee.
         * especially for cases involving async stack tracing.
         */
        if (realCallee->IsMemberExpression()) {
            if (isLdName && isSupportCallWithName) {
                pg->CallThisWithName(realCallee->AsMemberExpression()->Property()->AsIdentifier(), callee,
                                     static_cast<int64_t>(arguments_.size() + 1),
                                     realCallee->AsMemberExpression()->Property()->AsIdentifier()->Name());
                return;
            }
            pg->CallThis(realCallee->AsMemberExpression()->Property(), callee,
                         static_cast<int64_t>(arguments_.size() + 1));
            return;
        }
        if (isLdName && isSupportCallWithName) {
            pg->CallThisWithName(this, callee, static_cast<int64_t>(arguments_.size() + 1),
                                 realCallee->AsMemberExpression()->Property()->AsIdentifier()->Name());
            return;
        }
        pg->CallThis(this, callee, static_cast<int64_t>(arguments_.size() + 1));
        return;
    }

    pg->Call(this, callee, arguments_.size());
}


void CallExpression::UpdateSelf(const NodeUpdater &cb, [[maybe_unused]] binder::Binder *binder)
{
    callee_ = std::get<ir::AstNode *>(cb(callee_))->AsExpression();

    if (typeParams_) {
        typeParams_ = std::get<ir::AstNode *>(cb(typeParams_))->AsTSTypeParameterInstantiation();
    }

    for (auto iter = arguments_.begin(); iter != arguments_.end(); iter++) {
        *iter = std::get<ir::AstNode *>(cb(*iter))->AsExpression();
    }
}

}  // namespace panda::es2panda::ir