/**
 * Copyright (c) 2024-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 "lambdaLowering.h"

#include "checker/types/ets/etsTupleType.h"
#include "compiler/lowering/scopesInit/scopesInitPhase.h"
#include "compiler/lowering/util.h"
#include "generated/signatures.h"
#include "util/es2pandaMacros.h"
#include "util/nameMangler.h"

namespace ark::es2panda::compiler {

// The generating scheme of the invoke signature and body depends on the target usage of it. These are the possible
// types of invoke methods that we need to handle separately. Cases, where optional parameter(s) and rest parameter are
// both present will be handled by the CONVENTIONAL method. This is specialized, when the minimum argument number isn't
// equal to the argument number of the signature, and signature has rest parameter.
enum class InvokeType : std::uint8_t {
    CONVENTIONAL = 0U,                  // 0-16 arity invoke signatures
    INVOKE_N = 1U << 0U,                // 16+ arity invokeN signature
    OPTIONAL_PARAM_NO_REST = 1U << 1U,  // 0-16 arity invoke signatures with optional params (16+ arity will be invokeN)
};

struct LambdaInfo {
    ir::ClassDeclaration *calleeClass = nullptr;
    ir::TSInterfaceDeclaration *calleeInterface = nullptr;
    ir::ScriptFunction *enclosingFunction = nullptr;
    util::StringView name = "";
    util::StringView originalFuncName = "";
    ArenaSet<varbinder::Variable *> *capturedVars = nullptr;
    ir::Expression *callReceiver = nullptr;
    bool isFunctionReference = false;
    checker::ETSObjectType *objType = nullptr;
    ir::TSTypeParameterInstantiation *funcRefTypeParams = nullptr;
    bool isFunctionAsync = false;
};

struct CalleeMethodInfo {
    util::StringView calleeName;
    ir::AstNode *body = nullptr;
    checker::Type *forcedReturnType = nullptr;
    ir::ModifierFlags auxModifierFlags = ir::ModifierFlags::NONE;
    ir::ScriptFunctionFlags auxFunctionFlags = ir::ScriptFunctionFlags::NONE;
};

struct LambdaClassInvokeInfo {
    checker::Signature *lambdaSignature = nullptr;
    ir::MethodDefinition *callee = nullptr;
    ir::ClassDefinition *classDefinition = nullptr;
    checker::Substitution *substitution = nullptr;
    size_t arity = 0;
    util::StringView restParameterIdentifier = "";
    util::StringView restArgumentIdentifier = "";
    ArenaVector<util::UString> *argNames = nullptr;
    InvokeType invokeType = InvokeType::CONVENTIONAL;
};

static std::pair<ir::AstNode *, ir::ScriptFunction *> FindEnclosingClassAndFunction(ir::AstNode *ast)
{
    ir::ScriptFunction *function = nullptr;
    for (ir::AstNode *curr = ast->Parent(); curr != nullptr; curr = curr->Parent()) {
        if (curr->IsClassDeclaration() || curr->IsTSInterfaceDeclaration()) {
            return {curr, function};
        }
        if (curr->IsScriptFunction()) {
            function = curr->AsScriptFunction();
        }
    }
    ES2PANDA_UNREACHABLE();
}

static bool IsInsideObjectLiteralMethod(const ir::AstNode *ast)
{
    bool foundMethod = false;
    for (const ir::AstNode *curr = ast->Parent(); curr != nullptr; curr = curr->Parent()) {
        if (curr->IsProperty() && curr->AsProperty()->IsMethod()) {
            foundMethod = true;
        }
        if (foundMethod && curr->IsObjectExpression()) {
            return true;
        }
    }
    return false;
}

static ir::AstNode const *FindIfNeedThis(const ir::ArrowFunctionExpression *lambda, const checker::ETSChecker *checker)
{
    const auto *lambdaClass = ContainingClass(lambda);
    return lambda->FindChild([&checker, &lambdaClass](const ir::AstNode *ast) {
        if ((ast->IsThisExpression() || ast->IsSuperExpression()) &&
            checker->Relation()->IsIdenticalTo(lambdaClass, ContainingClass(ast))) {
            return !IsInsideObjectLiteralMethod(ast);
        }
        return false;
    });
}

static size_t g_calleeCount = 0;
static std::mutex g_calleeCountMutex {};

// Make calleeCount behaviour predictable
static void ResetCalleeCount()
{
    std::lock_guard lock(g_calleeCountMutex);
    g_calleeCount = 0;
}

void ResetCalleeCountOutside()
{
    std::lock_guard lock(g_calleeCountMutex);
    g_calleeCount = 0;
}

static util::StringView CreateCalleeName(ArenaAllocator *allocator)
{
    std::lock_guard lock(g_calleeCountMutex);
    auto name = util::UString(
        util::StringView(util::NameMangler::GetInstance()->CreateMangledNameForLambdaInvoke(g_calleeCount++)),
        allocator);
    return name.View();
}

static void ProcessTypeParameterProperties(checker::ETSTypeParameter *oldTypeParam,
                                           checker::ETSTypeParameter *newTypeParam,
                                           ir::TSTypeParameter *newTypeParamNode, checker::Substitution *substitution,
                                           public_lib::Context *ctx)
{
    auto *checker = ctx->GetChecker()->AsETSChecker();
    auto *allocator = ctx->allocator;
    if (auto *oldConstraint = oldTypeParam->GetConstraintType(); oldConstraint != nullptr) {
        auto *newConstraint = oldConstraint->Substitute(checker->Relation(), substitution);
        newTypeParam->SetConstraintType(newConstraint);
        auto *newConstraintNode = allocator->New<ir::OpaqueTypeNode>(newConstraint, allocator);
        ES2PANDA_ASSERT(newConstraintNode != nullptr);
        newTypeParamNode->SetConstraint(newConstraintNode);
        newConstraintNode->SetParent(newTypeParamNode);
    }

    if (auto *oldDefault = oldTypeParam->GetDefaultType(); oldDefault != nullptr) {
        auto *newDefault = oldDefault->Substitute(checker->Relation(), substitution);
        newTypeParam->SetDefaultType(newDefault);
        auto *newDefaultNode = allocator->New<ir::OpaqueTypeNode>(newDefault, allocator);
        ES2PANDA_ASSERT(newDefaultNode != nullptr);
        newTypeParamNode->SetDefaultType(newDefaultNode);
        newDefaultNode->SetParent(newTypeParamNode);
    }
}

// NOTE (smartin): The two methods 'CreateNewTypeParamVectors' and 'SetConstraintTypeAndDefaultTypeForTypeParams'
// contain very similar logic that can be found in 'CloneTypeParamsForClass'. Merge these later if possible
static void FillNewTypeParamVectors(ArenaAllocator *allocator, std::vector<checker::ETSTypeParameter *> &newTypeParams,
                                    ArenaVector<ir::TSTypeParameter *> &newTypeParamNodes,
                                    const checker::Signature *const lambdaSig,
                                    checker::Substitution *const substitution)
{
    for (auto *ix : lambdaSig->TypeParams()) {
        auto *oldTypeParam = ix->AsETSTypeParameter();
        auto *newTypeParamId = allocator->New<ir::Identifier>(oldTypeParam->Name(), allocator);
        auto *newTypeParamNode = util::NodeAllocator::ForceSetParent<ir::TSTypeParameter>(allocator, newTypeParamId,
                                                                                          nullptr, nullptr, allocator);
        auto *newTypeParam = allocator->New<checker::ETSTypeParameter>();
        newTypeParam->SetDeclNode(newTypeParamNode);

        auto *newTypeParamDecl = allocator->New<varbinder::TypeParameterDecl>(newTypeParamId->Name());
        newTypeParamDecl->BindNode(newTypeParamNode);
        auto *newTypeParamVar =
            allocator->New<varbinder::LocalVariable>(newTypeParamDecl, varbinder::VariableFlags::TYPE_PARAMETER);

        newTypeParamVar->SetTsType(newTypeParam);
        newTypeParamId->SetVariable(newTypeParamVar);

        newTypeParams.push_back(newTypeParam);
        newTypeParamNodes.push_back(newTypeParamNode);
        substitution->emplace(oldTypeParam, newTypeParam);
    }
}

static void SetConstraintTypeAndDefaultTypeForTypeParams(public_lib::Context *ctx,
                                                         const std::vector<checker::ETSTypeParameter *> &newTypeParams,
                                                         const ArenaVector<ir::TSTypeParameter *> &newTypeParamNodes,
                                                         const checker::Signature *const lambdaSig,
                                                         const checker::Substitution *const substitution)
{
    auto *allocator = ctx->allocator;
    auto *checker = ctx->GetChecker()->AsETSChecker();

    for (size_t ix = 0; ix < lambdaSig->TypeParams().size(); ix++) {
        auto *oldTypeParam = lambdaSig->TypeParams()[ix]->AsETSTypeParameter();

        if (auto *oldConstraint = oldTypeParam->GetConstraintType(); oldConstraint != nullptr) {
            auto *newConstraint = oldConstraint->Substitute(checker->Relation(), substitution);
            newTypeParams[ix]->SetConstraintType(newConstraint);
            newTypeParamNodes[ix]->SetConstraint(allocator->New<ir::OpaqueTypeNode>(newConstraint, allocator));
            newTypeParamNodes[ix]->Constraint()->SetParent(newTypeParamNodes[ix]);
        }
        if (auto *oldDefault = oldTypeParam->GetDefaultType(); oldDefault != nullptr) {
            auto *newDefault = oldDefault->Substitute(checker->Relation(), substitution);
            newTypeParams[ix]->SetDefaultType(newDefault);
            newTypeParamNodes[ix]->SetDefaultType(allocator->New<ir::OpaqueTypeNode>(newDefault, allocator));
            newTypeParamNodes[ix]->DefaultType()->SetParent(newTypeParamNodes[ix]);
        }
    }
}

static ir::TSTypeParameterDeclaration *CloneTypeParamsForSignature(public_lib::Context *ctx,
                                                                   LambdaClassInvokeInfo *lciInfo,
                                                                   bool isGenericFunctionInstantiated)
{
    ir::TSTypeParameterDeclaration *oldIrTypeParams = lciInfo->callee->Function()->TypeParams();
    checker::Signature *lambdaSig = lciInfo->lambdaSignature;
    // NOTE (smartin): the first condition can be deleted, once the generic lambdas generate correct invoke function
    // into the global scope (currently we don't generate type params for them)
    if (oldIrTypeParams == nullptr || lambdaSig->TypeParams().empty() || isGenericFunctionInstantiated) {
        return nullptr;
    }

    auto *allocator = ctx->allocator;
    auto newTypeParams = std::vector<checker::ETSTypeParameter *> {};
    auto newTypeParamNodes = ArenaVector<ir::TSTypeParameter *>(allocator->Adapter());

    FillNewTypeParamVectors(allocator, newTypeParams, newTypeParamNodes, lambdaSig, lciInfo->substitution);
    SetConstraintTypeAndDefaultTypeForTypeParams(ctx, newTypeParams, newTypeParamNodes, lambdaSig,
                                                 lciInfo->substitution);

    auto *newIrTypeParams = util::NodeAllocator::ForceSetParent<ir::TSTypeParameterDeclaration>(
        allocator, std::move(newTypeParamNodes), oldIrTypeParams->RequiredParams());

    return newIrTypeParams;
}

static std::pair<ir::TSTypeParameterDeclaration *, checker::Substitution> CloneTypeParamsForClass(
    public_lib::Context *ctx, ir::TSTypeParameterDeclaration *oldIrTypeParams, ir::ScriptFunction *enclosingFunction,
    varbinder::Scope *enclosingScope)
{
    if (oldIrTypeParams == nullptr) {
        return {nullptr, {}};
    }

    auto *allocator = ctx->allocator;

    auto *newScope = allocator->New<varbinder::LocalScope>(allocator, enclosingScope);
    auto newTypeParams = std::vector<checker::ETSTypeParameter *> {};
    auto newTypeParamNodes = ArenaVector<ir::TSTypeParameter *>(allocator->Adapter());
    auto substitution = checker::Substitution {};

    for (size_t ix = 0; ix < oldIrTypeParams->Params().size(); ix++) {
        auto *oldTypeParamNode = oldIrTypeParams->Params()[ix];
        auto *oldTypeParam = enclosingFunction->Signature()->TypeParams()[ix]->AsETSTypeParameter();
        auto *newTypeParamId = allocator->New<ir::Identifier>(oldTypeParamNode->Name()->Name(), allocator);
        auto *newTypeParamNode = util::NodeAllocator::ForceSetParent<ir::TSTypeParameter>(allocator, newTypeParamId,
                                                                                          nullptr, nullptr, allocator);
        auto *newTypeParam = allocator->New<checker::ETSTypeParameter>();
        auto *newTypeParamDecl = allocator->New<varbinder::TypeParameterDecl>(newTypeParamId->Name());
        auto *newTypeParamVar =
            allocator->New<varbinder::LocalVariable>(newTypeParamDecl, varbinder::VariableFlags::TYPE_PARAMETER);
        ES2PANDA_ASSERT(newTypeParam != nullptr && newScope != nullptr && newTypeParamDecl != nullptr &&
                        newTypeParamVar != nullptr);
        newTypeParam->SetDeclNode(newTypeParamNode);

        newTypeParamDecl->BindNode(newTypeParamNode);
        newTypeParamVar->SetTsType(newTypeParam);
        newScope->InsertBinding(newTypeParamId->Name(), newTypeParamVar);
        newTypeParamId->SetVariable(newTypeParamVar);

        newTypeParams.push_back(newTypeParam);
        newTypeParamNodes.push_back(newTypeParamNode);
        substitution.emplace(oldTypeParam, newTypeParam);
    }

    for (size_t ix = 0; ix < oldIrTypeParams->Params().size(); ix++) {
        auto *oldTypeParam = enclosingFunction->Signature()->TypeParams()[ix]->AsETSTypeParameter();
        ProcessTypeParameterProperties(oldTypeParam, newTypeParams[ix], newTypeParamNodes[ix], &substitution, ctx);
    }

    auto *newIrTypeParams = util::NodeAllocator::ForceSetParent<ir::TSTypeParameterDeclaration>(
        allocator, std::move(newTypeParamNodes), oldIrTypeParams->RequiredParams());
    ES2PANDA_ASSERT(newIrTypeParams != nullptr);
    newIrTypeParams->SetScope(newScope);

    return {newIrTypeParams, std::move(substitution)};
}

static std::pair<ir::TSTypeParameterDeclaration *, checker::Substitution> CloneTypeParamsForClassFromClass(
    public_lib::Context *ctx, ir::ClassDefinition *classDef, varbinder::Scope *enclosingScope)
{
    auto *oldIrTypeParams = classDef->TypeParams();
    if (oldIrTypeParams == nullptr) {
        return {nullptr, {}};
    }

    auto *allocator = ctx->allocator;

    auto *newScope = allocator->New<varbinder::LocalScope>(allocator, enclosingScope);
    auto newTypeParams = ArenaVector<checker::ETSTypeParameter *>(allocator->Adapter());
    auto newTypeParamNodes = ArenaVector<ir::TSTypeParameter *>(allocator->Adapter());
    auto substitution = checker::Substitution {};

    for (size_t ix = 0; ix < oldIrTypeParams->Params().size(); ix++) {
        auto *oldTypeParamNode = oldIrTypeParams->Params()[ix];

        auto *classType = classDef->TsType()->AsETSObjectType();
        auto *oldTypeParam = classType->TypeArguments()[ix]->AsETSTypeParameter();

        auto *newTypeParamId = allocator->New<ir::Identifier>(oldTypeParamNode->Name()->Name(), allocator);

        auto *newTypeParamNode = util::NodeAllocator::ForceSetParent<ir::TSTypeParameter>(allocator, newTypeParamId,
                                                                                          nullptr, nullptr, allocator);

        auto *newTypeParam = allocator->New<checker::ETSTypeParameter>();
        auto *newTypeParamDecl = allocator->New<varbinder::TypeParameterDecl>(newTypeParamId->Name());
        auto *newTypeParamVar =
            allocator->New<varbinder::LocalVariable>(newTypeParamDecl, varbinder::VariableFlags::TYPE_PARAMETER);

        ES2PANDA_ASSERT(newTypeParam != nullptr && newScope != nullptr && newTypeParamDecl != nullptr &&
                        newTypeParamVar != nullptr);

        newTypeParam->SetDeclNode(newTypeParamNode);
        newTypeParamDecl->BindNode(newTypeParamNode);
        newTypeParamVar->SetTsType(newTypeParam);
        newScope->InsertBinding(newTypeParamId->Name(), newTypeParamVar);
        newTypeParamId->SetVariable(newTypeParamVar);

        newTypeParams.push_back(newTypeParam);
        newTypeParamNodes.push_back(newTypeParamNode);
        substitution.emplace(oldTypeParam, newTypeParam);
    }

    for (size_t ix = 0; ix < oldIrTypeParams->Params().size(); ix++) {
        auto *classType = classDef->TsType()->AsETSObjectType();
        auto *oldTypeParam = classType->TypeArguments()[ix]->AsETSTypeParameter();

        ProcessTypeParameterProperties(oldTypeParam, newTypeParams[ix], newTypeParamNodes[ix], &substitution, ctx);
    }

    auto *newIrTypeParams = util::NodeAllocator::ForceSetParent<ir::TSTypeParameterDeclaration>(
        allocator, std::move(newTypeParamNodes), oldIrTypeParams->RequiredParams());
    ES2PANDA_ASSERT(newIrTypeParams != nullptr);
    newIrTypeParams->SetScope(newScope);

    return {newIrTypeParams, std::move(substitution)};
}

using ParamsAndVarMap =
    std::pair<ArenaVector<ir::Expression *>, ArenaMap<varbinder::Variable *, varbinder::Variable *>>;

inline static varbinder::Variable *InitNewParameterVariable(varbinder::VarBinder *varBinder,
                                                            ir::ETSParameterExpression *param,
                                                            checker::Type *newParamType,
                                                            varbinder::ParamScope *paramScope)
{
    ES2PANDA_ASSERT(param != nullptr);
    auto *var = varBinder->AddParamDecl(param);
    var->SetTsType(newParamType);
    var->SetScope(paramScope);
    param->SetVariable(var);
    param->SetTsType(newParamType);
    return var;
}

ParamsAndVarMap CreateLambdaCalleeParameters(public_lib::Context *ctx, ir::ArrowFunctionExpression *lambda,
                                             ArenaSet<varbinder::Variable *> const &captured,
                                             varbinder::ParamScope *paramScope, checker::Substitution *substitution)
{
    auto allocator = ctx->allocator;
    auto checker = ctx->GetChecker()->AsETSChecker();
    auto varBinder = ctx->GetChecker()->VarBinder();
    auto resParams = ArenaVector<ir::Expression *>(allocator->Adapter());
    auto varMap = ArenaMap<varbinder::Variable *, varbinder::Variable *>(allocator->Adapter());

    auto paramLexScope = varbinder::LexicalScope<varbinder::ParamScope>::Enter(varBinder, paramScope);

    for (auto capturedVar : captured) {
        auto *newType = capturedVar->TsType()->Substitute(checker->Relation(), substitution);
        auto newId = util::NodeAllocator::ForceSetParent<ir::Identifier>(
            allocator, capturedVar->Name(), allocator->New<ir::OpaqueTypeNode>(newType, allocator), allocator);
        auto param =
            util::NodeAllocator::ForceSetParent<ir::ETSParameterExpression>(allocator, newId, false, allocator);
        auto *var = InitNewParameterVariable(varBinder, param, newType, paramScope);
        resParams.push_back(param);
        varMap[capturedVar] = var;
    }

    for (auto *oldParam : lambda->Function()->Params()) {
        auto *oldParamType = oldParam->AsETSParameterExpression()->Ident()->TsType();
        auto *newParamType = oldParamType->Substitute(checker->Relation(), substitution);
        auto *newParam = oldParam->AsETSParameterExpression()->Clone(allocator, nullptr);
        ES2PANDA_ASSERT(newParam != nullptr);

        if (newParam->IsOptional()) {
            newParam->SetOptional(false);
            newParamType = checker->CreateETSUnionType({newParamType, checker->GlobalETSUndefinedType()});
        }

        newParam->SetTypeAnnotation(allocator->New<ir::OpaqueTypeNode>(newParamType, allocator));
        auto *var = InitNewParameterVariable(varBinder, newParam, newParamType, paramScope);
        newParam->Ident()->SetTsType(newParamType);
        if (newParam->IsRestParameter()) {
            newParam->TypeAnnotation()->SetParent(newParam->Spread());
            newParam->Spread()->SetTsType(newParamType);
        } else {
            newParam->TypeAnnotation()->SetParent(newParam->Ident());
        }
        resParams.push_back(newParam);
        varMap[oldParam->AsETSParameterExpression()->Variable()] = var;

        if (newParam->TypeAnnotation()->IsETSFunctionType()) {
            // Parameter can be a function with other parameters inside
            // Restart varbinder to set correct scopes for inner parameters
            InitScopesPhaseETS::RunExternalNode(newParam->TypeAnnotation(), varBinder);
        }
    }

    return {resParams, varMap};
}

static void ProcessCalleeMethodBody(ir::AstNode *body, checker::ETSChecker *checker, varbinder::Scope *paramScope,
                                    checker::Substitution *substitution,
                                    ArenaMap<varbinder::Variable *, varbinder::Variable *> const &varMap)
{
    if (body == nullptr) {
        return;
    }
    body->Scope()->SetParent(paramScope);
    body->IterateRecursively([&](ir::AstNode *node) {
        if (node->IsIdentifier()) {
            auto *id = node->AsIdentifier();
            if (auto ref = varMap.find(id->Variable()); ref != varMap.end()) {
                id->SetVariable(ref->second);
                id->Check(checker);
            }
        }
        if (substitution == nullptr) {
            return;
        }
        if (node->IsTyped() && node->AsTyped()->TsType() != nullptr) {
            node->AsTyped()->SetTsType(node->AsTyped()->TsType()->Substitute(checker->Relation(), substitution));
            if (node->IsTSNonNullExpression()) {
                auto expr = node->AsTSNonNullExpression();
                expr->SetOriginalType(expr->OriginalType()->Substitute(checker->Relation(), substitution));
            }
        }
        if (node->IsCallExpression()) {
            node->AsCallExpression()->SetSignature(
                node->AsCallExpression()->Signature()->Substitute(checker->Relation(), substitution));
        }
        if (node->IsETSNewClassInstanceExpression()) {
            node->AsETSNewClassInstanceExpression()->SetSignature(
                node->AsETSNewClassInstanceExpression()->Signature()->Substitute(checker->Relation(), substitution));
        }
        if (node->IsScriptFunction()) {
            node->AsScriptFunction()->SetSignature(
                node->AsScriptFunction()->Signature()->Substitute(checker->Relation(), substitution));
        }
        if (node->IsVariableDeclarator()) {
            auto *id = node->AsVariableDeclarator()->Id();
            id->Variable()->SetTsType(id->Variable()->TsType()->Substitute(checker->Relation(), substitution));
        }
    });
}

static ir::MethodDefinition *CheckCalleeMethodCtx(public_lib::Context *ctx, LambdaInfo const *info,
                                                  ir::ScriptFunction *func, ir::MethodDefinition *method)
{
    auto *varBinder = ctx->GetChecker()->VarBinder()->AsETSBinder();
    auto bctx = info->calleeClass != nullptr
                    ? varbinder::BoundContext {varBinder->GetRecordTable(), info->calleeClass->Definition(), true}
                    : varbinder::BoundContext {varBinder->GetRecordTable(), info->calleeInterface, true};
    varBinder->ResolveReferencesForScopeWithContext(func, func->Scope());
    auto *objType = info->calleeClass != nullptr ? info->calleeClass->Definition()->TsType()->AsETSObjectType()
                                                 : info->calleeInterface->TsType()->AsETSObjectType();
    auto checkerStatus =
        info->calleeClass != nullptr ? checker::CheckerStatus::IN_CLASS : checker::CheckerStatus::IN_INTERFACE;
    auto checkerCtx = checker::SavedCheckerContext(ctx->GetChecker(), checkerStatus, objType);
    method->Check(ctx->GetChecker()->AsETSChecker());
    return method;
}

static ir::MethodDefinition *SetUpCalleeMethod(public_lib::Context *ctx, LambdaInfo const *info,
                                               CalleeMethodInfo const *cmInfo, ir::ScriptFunction *func,
                                               varbinder::Scope *scopeForMethod)
{
    auto *allocator = ctx->allocator;
    auto *varBinder = ctx->GetChecker()->VarBinder()->AsETSBinder();

    auto *objType = info->calleeClass != nullptr ? info->calleeClass->Definition()->TsType()->AsETSObjectType()
                                                 : info->calleeInterface->TsType()->AsETSObjectType();
    auto *funcScope = func->Scope();
    auto *paramScope = funcScope->ParamScope();
    auto isStatic = ((info->callReceiver != nullptr || info->calleeInterface != nullptr) ? ir::ModifierFlags::NONE
                                                                                         : ir::ModifierFlags::STATIC);
    auto modifierFlags = ir::ModifierFlags::PUBLIC | isStatic | cmInfo->auxModifierFlags;

    auto *calleeNameId = allocator->New<ir::Identifier>(cmInfo->calleeName, allocator);
    func->SetIdent(calleeNameId);
    calleeNameId->SetParent(func);

    auto *calleeNameClone = calleeNameId->Clone(allocator, nullptr);
    auto *funcExpr = util::NodeAllocator::ForceSetParent<ir::FunctionExpression>(allocator, func);
    auto *method = util::NodeAllocator::ForceSetParent<ir::MethodDefinition>(
        allocator, ir::MethodDefinitionKind::METHOD, calleeNameClone, funcExpr, modifierFlags, allocator, false);
    if (info->calleeClass != nullptr) {
        info->calleeClass->Definition()->EmplaceBody(method);
        method->SetParent(info->calleeClass->Definition());
    } else {
        info->calleeInterface->Body()->Body().emplace_back(method);
        method->SetParent(info->calleeInterface->Body());
    }

    auto *var =
        std::get<1>(varBinder->NewVarDecl<varbinder::FunctionDecl>(func->Start(), allocator, cmInfo->calleeName, func));
    var->AddFlag(varbinder::VariableFlags::METHOD);
    var->SetScope(scopeForMethod);
    func->Id()->SetVariable(var);
    ES2PANDA_ASSERT(method->Id());
    method->Id()->SetVariable(var);
    if (info->callReceiver != nullptr) {
        auto paramScopeCtx = varbinder::LexicalScope<varbinder::FunctionParamScope>::Enter(varBinder, paramScope);
        varBinder->AddMandatoryParam(varbinder::TypedBinder::MANDATORY_PARAM_THIS);
        objType->AddProperty<checker::PropertyType::INSTANCE_METHOD>(var->AsLocalVariable());
    } else {
        objType->AddProperty<checker::PropertyType::STATIC_METHOD>(var->AsLocalVariable());
    }

    return CheckCalleeMethodCtx(ctx, info, func, method);
}

std::pair<ir::TSTypeParameterDeclaration *, checker::Substitution> CloneTypeParameters(public_lib::Context *ctx,
                                                                                       LambdaInfo const *info,
                                                                                       varbinder::Scope *enclosingScope)
{
    auto *oldTypeParams = (info->enclosingFunction != nullptr) ? info->enclosingFunction->TypeParams() : nullptr;
    auto [newTypeParams, subst0] = CloneTypeParamsForClass(ctx, oldTypeParams, info->enclosingFunction, enclosingScope);

    if (newTypeParams == nullptr && info->callReceiver == nullptr &&
        info->calleeClass->Definition()->TypeParams() != nullptr) {
        std::tie(newTypeParams, subst0) =
            CloneTypeParamsForClassFromClass(ctx, info->calleeClass->Definition(), enclosingScope);
    }

    return {newTypeParams, std::move(subst0)};
}

using ISS = ir::ScriptFunction::ScriptFunctionData;
static ir::MethodDefinition *CreateCalleeMethod(public_lib::Context *ctx, ir::ArrowFunctionExpression *lambda,
                                                LambdaInfo const *info, CalleeMethodInfo const *cmInfo)
{
    auto *allocator = ctx->allocator;
    auto *varBinder = ctx->GetChecker()->VarBinder()->AsETSBinder();
    auto *checker = ctx->GetChecker()->AsETSChecker();

    auto *classScope = info->calleeClass != nullptr ? info->calleeClass->Definition()->Scope()->AsClassScope()
                                                    : info->calleeInterface->Scope()->AsClassScope();

    auto enclosingScope =
        info->callReceiver != nullptr ? classScope->InstanceMethodScope() : classScope->StaticMethodScope();

    auto [newTypeParams, subst0] = CloneTypeParameters(ctx, info, enclosingScope);

    auto &substitution = subst0;  // NOTE(gogabr): needed to capture in a lambda later.
    auto *scopeForMethod = newTypeParams != nullptr ? newTypeParams->Scope() : enclosingScope;

    auto lexScope = varbinder::LexicalScope<varbinder::LocalScope>::Enter(varBinder, enclosingScope);
    auto paramScope = allocator->New<varbinder::FunctionParamScope>(allocator, scopeForMethod);

    auto [params, vMap] = CreateLambdaCalleeParameters(ctx, lambda, *info->capturedVars, paramScope, &substitution);
    auto varMap = std::move(vMap);

    auto arrowReturnType = lambda->TsType()->AsETSFunctionType()->ArrowSignature()->ReturnType();
    auto *alternative = arrowReturnType->Substitute(checker->Relation(), &substitution);
    auto *returnType = cmInfo->forcedReturnType != nullptr ? cmInfo->forcedReturnType : alternative;
    auto returnTypeAnnotation = allocator->New<ir::OpaqueTypeNode>(returnType, allocator);

    auto modifierFlags = cmInfo->auxModifierFlags | ir::ModifierFlags::PUBLIC |
                         (info->callReceiver != nullptr ? ir::ModifierFlags::NONE : ir::ModifierFlags::STATIC);

    auto func = util::NodeAllocator::ForceSetParent<ir::ScriptFunction>(
        allocator, allocator,
        ISS {cmInfo->body,
             ir::FunctionSignature(newTypeParams, std::move(params), returnTypeAnnotation,
                                   lambda->Function()->HasReceiver()),
             ir::ScriptFunctionFlags::METHOD | cmInfo->auxFunctionFlags, modifierFlags});
    if (lambda->HasAstNodeFlags(ir::AstNodeFlags::NO_DEBUG_LINE_INFO)) {
        func->AddAstNodeFlags(ir::AstNodeFlags::NO_DEBUG_LINE_INFO);
    }
    auto *funcScope = cmInfo->body == nullptr ? allocator->New<varbinder::FunctionScope>(allocator, paramScope)
                                              : cmInfo->body->Scope()->AsFunctionScope();
    ES2PANDA_ASSERT(funcScope);
    auto *tsType =
        info->calleeClass != nullptr ? info->calleeClass->Definition()->TsType() : info->calleeInterface->TsType();
    funcScope->BindName(tsType->AsETSObjectType()->AssemblerName());
    func->SetScope(funcScope);
    ProcessCalleeMethodBody(cmInfo->body, checker, paramScope, &substitution, varMap);

    for (auto *param : func->Params()) {
        param->SetParent(func);
    }

    // Bind the scopes
    funcScope->BindNode(func);
    paramScope->BindNode(func);
    funcScope->AssignParamScope(paramScope);
    paramScope->BindFunctionScope(funcScope);

    /* NOTE(gogabr): Why does function scope need to replicate bindings from param scope?.
       Keeping it for now.
    */
    for (auto [ov, nv] : varMap) {
        ES2PANDA_ASSERT(ov->Name() == nv->Name());
        funcScope->EraseBinding(ov->Name());
        funcScope->InsertBinding(ov->Name(), nv);
    }

    return SetUpCalleeMethod(ctx, info, cmInfo, func, scopeForMethod);
}

static ir::MethodDefinition *CreateCalleeStackfull(public_lib::Context *ctx, ir::ArrowFunctionExpression *lambda,
                                                   LambdaInfo const *info)
{
    ES2PANDA_ASSERT(!ctx->config->options->IsStacklessCoros());

    auto *allocator = ctx->allocator;
    auto *checker = ctx->GetChecker()->AsETSChecker();
    auto *body = lambda->Function()->Body()->AsBlockStatement();
    const bool isAsync = lambda->Function()->IsAsyncFunc();

    auto calleeName = info->name;
    if (isAsync) {
        calleeName = (util::UString {checker::ETSChecker::GetAsyncImplName(info->name), allocator}).View();
    }

    checker::Type *forcedReturnType = nullptr;
    if (isAsync) {
        forcedReturnType = checker->GlobalETSAnyType();
    }

    CalleeMethodInfo cmInfo;
    cmInfo.calleeName = calleeName;
    cmInfo.body = body;
    cmInfo.forcedReturnType = forcedReturnType;
    if (isAsync) {
        cmInfo.auxFunctionFlags = ir::ScriptFunctionFlags::ASYNC_IMPL;
    }
    auto *method = CreateCalleeMethod(ctx, lambda, info, &cmInfo);

    if (isAsync) {
        CalleeMethodInfo cmInfoAsync;
        cmInfoAsync.calleeName = info->name;
        cmInfoAsync.body = nullptr;
        cmInfoAsync.forcedReturnType = nullptr;
        cmInfoAsync.auxModifierFlags = ir::ModifierFlags::NATIVE;
        cmInfoAsync.auxFunctionFlags = ir::ScriptFunctionFlags::ASYNC;
        auto *asyncMethod = CreateCalleeMethod(ctx, lambda, info, &cmInfoAsync);
        asyncMethod->Function()->SetAsyncPairMethod(method->Function());
        return asyncMethod;
    }

    return method;
}

static ir::MethodDefinition *CreateCalleeStackless(public_lib::Context *ctx, ir::ArrowFunctionExpression *lambda,
                                                   LambdaInfo const *info)
{
    ES2PANDA_ASSERT(ctx->config->options->IsStacklessCoros());

    auto calleeName = info->name;
    CalleeMethodInfo cmInfo;
    cmInfo.calleeName = calleeName;
    cmInfo.body = lambda->Function()->Body()->AsBlockStatement();
    cmInfo.forcedReturnType = nullptr;
    if (lambda->Function()->IsAsyncFunc()) {
        cmInfo.auxFunctionFlags = ir::ScriptFunctionFlags::ASYNC;
    }

    auto *method = CreateCalleeMethod(ctx, lambda, info, &cmInfo);

    ES2PANDA_ASSERT(method);

    return method;
}

static ir::MethodDefinition *CreateCallee(public_lib::Context *ctx, ir::ArrowFunctionExpression *lambda,
                                          LambdaInfo const *info)
{
    if (ctx->config->options->IsStacklessCoros()) {
        return CreateCalleeStackless(ctx, lambda, info);
    }

    return CreateCalleeStackfull(ctx, lambda, info);
}

// The name "=t" used in extension methods has special meaning for the code generator;
// avoid it as parameter and field name in our generated code.
static util::StringView AvoidMandatoryThis(util::StringView name)
{
    return (name == varbinder::TypedBinder::MANDATORY_PARAM_THIS) ? "$extensionThis" : name;
}

static void CreateLambdaClassFields(public_lib::Context *ctx, ir::ClassDefinition *classDefinition,
                                    LambdaInfo const *info, checker::Substitution *substitution)
{
    auto *allocator = ctx->allocator;
    auto *parser = ctx->parser->AsETSParser();
    auto *checker = ctx->GetChecker()->AsETSChecker();
    auto props = ArenaVector<ir::AstNode *>(allocator->Adapter());

    checker::Type *objectType = info->objType != nullptr
                                    ? info->objType
                                    : (info->calleeClass != nullptr ? info->calleeClass->Definition()->TsType()
                                                                    : info->calleeInterface->TsType());

    if (info->callReceiver != nullptr) {
        auto *outerThisDeclaration = parser->CreateFormattedClassFieldDefinition(
            "@@I1: @@T2", "$this", objectType->Substitute(checker->Relation(), substitution));
        outerThisDeclaration->SetRange(info->callReceiver->Range());
        props.push_back(outerThisDeclaration);
    }

    for (auto *captured : *info->capturedVars) {
        auto *varDeclaration = parser->CreateFormattedClassFieldDefinition(
            "@@I1: @@T2", AvoidMandatoryThis(captured->Name()),
            captured->TsType()->Substitute(checker->Relation(), substitution));
        varDeclaration->SetRange(captured->Declaration()->Node()->Range());
        props.push_back(varDeclaration);
    }

    classDefinition->AddProperties(std::move(props));
}

static void CreateLambdaClassConstructor(public_lib::Context *ctx, ir::ClassDefinition *classDefinition,
                                         LambdaInfo const *info, checker::Substitution *substitution)
{
    auto *allocator = ctx->allocator;
    auto *parser = ctx->parser->AsETSParser();
    auto *checker = ctx->GetChecker()->AsETSChecker();

    auto params = ArenaVector<ir::Expression *>(allocator->Adapter());
    auto makeParam = [checker, allocator, substitution, &params](util::StringView name, checker::Type *type) {
        auto *substitutedType = type->Substitute(checker->Relation(), substitution);
        auto *id = util::NodeAllocator::ForceSetParent<ir::Identifier>(
            allocator, name, allocator->New<ir::OpaqueTypeNode>(substitutedType, allocator), allocator);
        auto *param = util::NodeAllocator::ForceSetParent<ir::ETSParameterExpression>(allocator, id, false, allocator);
        params.push_back(param);
    };

    checker::Type *objectType = info->objType != nullptr
                                    ? info->objType
                                    : (info->calleeClass != nullptr ? info->calleeClass->Definition()->TsType()
                                                                    : info->calleeInterface->TsType());

    if (info->callReceiver != nullptr) {
        makeParam("$this", objectType);
    }
    for (auto *var : *info->capturedVars) {
        makeParam(AvoidMandatoryThis(var->Name()), var->TsType());
    }

    auto bodyStmts = ArenaVector<ir::Statement *>(allocator->Adapter());
    auto makeStatement = [&parser, &bodyStmts](util::StringView name) {
        auto adjustedName = AvoidMandatoryThis(name);
        bodyStmts.push_back(parser->CreateFormattedStatement("this.@@I1 = @@I2", adjustedName, adjustedName));
    };
    if (info->callReceiver != nullptr) {
        makeStatement("$this");
    }
    for (auto *var : *info->capturedVars) {
        makeStatement(var->Name());
    }

    auto *body = util::NodeAllocator::ForceSetParent<ir::BlockStatement>(allocator, allocator, std::move(bodyStmts));

    auto *constructorId = allocator->New<ir::Identifier>("constructor", allocator);

    auto *func = util::NodeAllocator::ForceSetParent<ir::ScriptFunction>(
        allocator, allocator,
        ir::ScriptFunction::ScriptFunctionData {body, ir::FunctionSignature(nullptr, std::move(params), nullptr),
                                                ir::ScriptFunctionFlags::CONSTRUCTOR |
                                                    ir::ScriptFunctionFlags::IMPLICIT_SUPER_CALL_NEEDED});
    ES2PANDA_ASSERT(func);
    func->SetIdent(constructorId);
    auto *funcExpr = util::NodeAllocator::ForceSetParent<ir::FunctionExpression>(allocator, func);

    auto *ctor = util::NodeAllocator::ForceSetParent<ir::MethodDefinition>(
        allocator, ir::MethodDefinitionKind::CONSTRUCTOR, constructorId->Clone(allocator, nullptr), funcExpr,
        ir::ModifierFlags::NONE, allocator, false);

    classDefinition->EmplaceBody(ctor);
    ctor->SetParent(classDefinition);
}

static std::string GetArrayReallocationStringFixedArray(std::size_t startIdx)
{
    // NOTE(vpukhov): this is a clear null-safety violation (when elementType->IsETSReferenceType()) that should be
    // rewitten with a runtime intrinsic
    std::stringstream statements;
    statements << "let @@I1: int = @@I2.length > " << startIdx << " ? (@@I3.length - " << startIdx << ") : 0;";
    statements << "let @@I4 = @@E5;";
    statements << "let @@I6 = @@E7;";
    statements << "for (let i: int = 0; i < @@I8; i = i + 1) {";
    statements << "    @@I9[i] = @@I10[i + " << startIdx << "] as @@T11;";
    statements << "}";
    return statements.str();
}

static std::string GetArrayReallocationStringResizableArray(std::size_t startIdx)
{
    std::stringstream statements;
    statements << "let @@I1: int = @@I2.length > " << startIdx << " ? (@@I3.length - " << startIdx << ") : 0;";
    statements << "let @@I4: Array<@@T5> = @@E6;";
    statements << "for (let i: int = 0; i < @@I7; i = i + 1) {";
    statements << "    @@I8.$_set(i, @@I9[i + " << startIdx << "] as @@T10);";
    statements << "}";
    return statements.str();
}

// NOTE(vpukhov): requires the optimization based on the array type
// CC-OFFNXT(G.FUN.01, huge_method) solid logic
static ArenaVector<ark::es2panda::ir::Statement *> CreateRestArgumentsArrayReallocation(
    public_lib::Context *ctx, LambdaClassInvokeInfo const *lciInfo, size_t startIdx)
{
    if (!lciInfo->lambdaSignature->HasRestParameter() ||
        lciInfo->lambdaSignature->RestVar()->TsType()->IsETSTupleType()) {
        return ArenaVector<ir::Statement *>(ctx->allocator->Adapter());
    }

    auto *allocator = ctx->allocator;
    auto *parser = ctx->parser->AsETSParser();
    auto *checker = ctx->GetChecker()->AsETSChecker();

    auto *restParameterType = lciInfo->lambdaSignature->RestVar()->TsType();
    auto *restParameterSubstituteType = restParameterType->Substitute(checker->Relation(), lciInfo->substitution);
    auto *elementType = checker->GetElementTypeOfArray(restParameterSubstituteType);
    bool const isValueArray =
        restParameterSubstituteType->IsETSArrayType() && restParameterSubstituteType->AsETSArrayType()->IsValueArray();

    ir::Statement *args = nullptr;
    if (restParameterSubstituteType->IsETSArrayType()) {
        auto restParameterLen = Gensym(allocator);
        auto tmpArray = GenName(allocator).View();
        args = parser->CreateFormattedStatement(
            GetArrayReallocationStringFixedArray(startIdx), restParameterLen, lciInfo->restParameterIdentifier,
            lciInfo->restParameterIdentifier, tmpArray,
            CreateUninitializedFixedArray(ctx, restParameterLen->Clone(allocator, nullptr),
                                          checker->CreateETSArrayType(elementType, isValueArray)),
            lciInfo->restArgumentIdentifier, tmpArray, restParameterLen->Clone(allocator, nullptr),
            lciInfo->restArgumentIdentifier, lciInfo->restParameterIdentifier, elementType);
    } else {
        ES2PANDA_ASSERT(restParameterSubstituteType->IsETSResizableArrayType() ||
                        restParameterSubstituteType->IsETSReadonlyArrayType());
        auto *arrayelementType = checker->GetElementTypeOfArray(restParameterSubstituteType);
        auto restParameterLen = Gensym(allocator);
        args = parser->CreateFormattedStatement(
            GetArrayReallocationStringResizableArray(startIdx), restParameterLen, lciInfo->restParameterIdentifier,
            lciInfo->restParameterIdentifier, lciInfo->restArgumentIdentifier, arrayelementType,
            CreateUninitializedResizableArray(ctx, restParameterLen->Clone(allocator, nullptr),
                                              checker->CreateETSResizableArrayType(arrayelementType)),
            restParameterLen->Clone(allocator, nullptr), lciInfo->restArgumentIdentifier,
            lciInfo->restParameterIdentifier, arrayelementType);
    }
    ES2PANDA_ASSERT(args != nullptr);
    return ArenaVector<ir::Statement *>(args->AsBlockStatement()->Statements());
}

static void CreateInvokeMethodRestParameter(public_lib::Context *ctx, LambdaClassInvokeInfo *lciInfo,
                                            ArenaVector<ir::Expression *> *params)
{
    auto *allocator = ctx->allocator;
    auto *checker = ctx->GetChecker()->AsETSChecker();

    auto *restIdent = Gensym(allocator);
    ES2PANDA_ASSERT(restIdent != nullptr);
    lciInfo->restParameterIdentifier = restIdent->Name();
    auto *spread = allocator->New<ir::SpreadElement>(ir::AstNodeType::REST_ELEMENT, allocator, restIdent);
    ES2PANDA_ASSERT(spread != nullptr);
    auto *arr = checker->CreateETSArrayType(checker->GlobalETSAnyType(), false);

    auto *typeAnnotation = allocator->New<ir::OpaqueTypeNode>(arr, allocator);

    spread->SetTypeAnnotation(typeAnnotation);
    spread->SetTsType(arr);
    restIdent->SetTsType(arr);
    auto *param = allocator->New<ir::ETSParameterExpression>(spread, nullptr, allocator);

    restIdent->SetParent(spread);
    spread->SetParent(param);
    params->push_back(param);
}

static ir::Expression *SetRestIdentOfCallArguments(public_lib::Context *ctx, LambdaClassInvokeInfo const *lciInfo)
{
    auto *allocator = ctx->allocator;

    auto restType = lciInfo->lambdaSignature->RestVar()->TsType();
    if (restType->IsETSTupleType()) {
        ArenaVector<ir::Expression *> tupleElements(allocator->Adapter());
        // NOLINTNEXTLINE (bugprone-too-small-loop-variable)
        for (std::uint16_t i = 0; i < restType->AsETSTupleType()->GetTupleSize(); ++i) {
            auto ident = allocator->New<ir::Identifier>(lciInfo->restParameterIdentifier, allocator);
            auto number = allocator->New<ir::NumberLiteral>(lexer::Number(i));
            auto indexed = util::NodeAllocator::ForceSetParent<ir::MemberExpression>(
                allocator, ident, number, ir::MemberExpressionKind::ELEMENT_ACCESS, true, false);

            auto typeNode =
                allocator->New<ir::OpaqueTypeNode>(restType->AsETSTupleType()->GetTupleTypesList()[i], allocator);
            auto cast = util::NodeAllocator::ForceSetParent<ir::TSAsExpression>(allocator, indexed, typeNode, false);
            tupleElements.push_back(cast);
        }
        auto arrayExpr =
            util::NodeAllocator::ForceSetParent<ir::ArrayExpression>(allocator, std::move(tupleElements), allocator);
        auto *spread = util::NodeAllocator::ForceSetParent<ir::SpreadElement>(
            allocator, ir::AstNodeType::SPREAD_ELEMENT, allocator, arrayExpr);
        return spread;
    }
    auto *restIdent =
        util::NodeAllocator::ForceSetParent<ir::Identifier>(allocator, lciInfo->restArgumentIdentifier, allocator);
    if (restType->IsETSArrayType()) {
        auto *spread = allocator->New<ir::SpreadElement>(ir::AstNodeType::SPREAD_ELEMENT, allocator, restIdent);
        restIdent->SetParent(spread);
        return spread;
    }
    ES2PANDA_ASSERT(restType->IsETSResizableArrayType() || restType->IsETSReadonlyArrayType());
    auto *spread = allocator->New<ir::SpreadElement>(ir::AstNodeType::SPREAD_ELEMENT, allocator, restIdent);
    restIdent->SetParent(spread);
    return spread;
}

static ir::Expression *GetInvokeCallArgumentAtIdx(public_lib::Context *ctx, LambdaClassInvokeInfo const *lciInfo,
                                                  size_t idx, bool wrapToObject)
{
    auto *allocator = ctx->allocator;
    auto *parser = ctx->parser->AsETSParser();
    auto *checker = ctx->GetChecker()->AsETSChecker();

    const auto *lambdaParam = lciInfo->lambdaSignature->Params().at(idx);
    if (idx < lciInfo->arity) {
        const auto argName = lambdaParam->Name();
        auto *type = lambdaParam->TsType()->Substitute(checker->Relation(), lciInfo->substitution);
        return wrapToObject ? parser->CreateFormattedExpression("@@I1 as @@T2", argName, type)
                            : allocator->New<ir::Identifier>(argName, allocator);
    }

    if ((lciInfo->invokeType != InvokeType::OPTIONAL_PARAM_NO_REST) && !lciInfo->lambdaSignature->HasRestParameter()) {
        return allocator->New<ir::UndefinedLiteral>();
    }

    // In this case, optional parameter(s) and/or rest parameter are present in the signature
    ES2PANDA_ASSERT(idx >= lciInfo->arity);
    ES2PANDA_ASSERT((*lciInfo->argNames).size() > (idx - lciInfo->arity));
    return parser->CreateFormattedExpression("@@I1", (*lciInfo->argNames)[idx - lciInfo->arity]);
}

static void AddCapturedVarsToCallArguments(public_lib::Context *ctx, LambdaInfo const *info,
                                           ArenaVector<ir::Expression *> &callArguments)
{
    auto *parser = ctx->parser->AsETSParser();
    for (const auto *const captured : *info->capturedVars) {
        auto *arg = parser->CreateFormattedExpression("this.@@I1", AvoidMandatoryThis(captured->Name()));
        callArguments.push_back(arg);
    }
}

static ArenaVector<ir::Expression *> CreateCallArgumentsForLambdaClassInvoke(public_lib::Context *ctx,
                                                                             LambdaInfo const *info,
                                                                             LambdaClassInvokeInfo const *lciInfo,
                                                                             bool wrapToObject)
{
    auto *allocator = ctx->allocator;

    auto callArguments = ArenaVector<ir::Expression *>(allocator->Adapter());
    AddCapturedVarsToCallArguments(ctx, info, callArguments);

    for (size_t idx = 0; idx < lciInfo->lambdaSignature->ArgCount(); ++idx) {
        callArguments.push_back(GetInvokeCallArgumentAtIdx(ctx, lciInfo, idx, wrapToObject));
    }

    if (!lciInfo->lambdaSignature->HasRestParameter()) {
        return callArguments;
    }
    callArguments.push_back(SetRestIdentOfCallArguments(ctx, lciInfo));
    return callArguments;
}

static ArenaVector<ir::Expression *> CreateCallArgumentsForLambdaClassInvokeN(public_lib::Context *ctx,
                                                                              LambdaInfo const *info,
                                                                              LambdaClassInvokeInfo const *lciInfo)
{
    auto *allocator = ctx->allocator;
    auto *parser = ctx->parser->AsETSParser();
    auto *checker = ctx->GetChecker()->AsETSChecker();

    auto callArguments = ArenaVector<ir::Expression *>(allocator->Adapter());
    AddCapturedVarsToCallArguments(ctx, info, callArguments);

    ES2PANDA_ASSERT(lciInfo->argNames->size() ==
                    lciInfo->lambdaSignature->ArgCount() - lciInfo->lambdaSignature->MinArgCount());

    for (size_t idx = 0; idx < lciInfo->lambdaSignature->MinArgCount(); ++idx) {
        const auto *lambdaParam = lciInfo->lambdaSignature->Params().at(idx);
        auto argName = lciInfo->restParameterIdentifier;
        auto *type = lambdaParam->TsType()->Substitute(checker->Relation(), lciInfo->substitution);
        auto *arg = parser->CreateFormattedExpression("@@I1[" + std::to_string(idx) + "] as @@T2", argName, type);
        callArguments.push_back(arg);
    }

    for (size_t nameIdx = 0, idx = lciInfo->lambdaSignature->MinArgCount(); idx < lciInfo->lambdaSignature->ArgCount();
         ++idx, ++nameIdx) {
        auto *arg = parser->CreateFormattedExpression("@@I1", (*lciInfo->argNames)[nameIdx]);
        callArguments.push_back(arg);
    }

    if (!lciInfo->lambdaSignature->HasRestParameter()) {
        return callArguments;
    }

    callArguments.push_back(SetRestIdentOfCallArguments(ctx, lciInfo));
    return callArguments;
}

static void SetTypeParamsForInvokeSignature(public_lib::Context *ctx, LambdaInfo const *info,
                                            LambdaClassInvokeInfo const *lciInfo, ir::CallExpression *call)
{
    auto *allocator = ctx->allocator;

    const auto &origCallTypeParams = info->funcRefTypeParams->Params();
    auto typeArgs = ArenaVector<ir::TypeNode *>(allocator->Adapter());
    for (auto *tp : origCallTypeParams) {
        typeArgs.push_back(allocator->New<ir::OpaqueTypeNode>(
            tp->TsType()->Substitute(ctx->GetChecker()->Relation(), lciInfo->substitution), allocator));
    }
    auto *typeArg =
        util::NodeAllocator::ForceSetParent<ir::TSTypeParameterInstantiation>(allocator, std::move(typeArgs));
    call->SetTypeParams(typeArg);
    typeArg->SetParent(call);
}

static ir::CallExpression *CreateCallForLambdaClassInvoke(public_lib::Context *ctx, LambdaInfo const *info,
                                                          LambdaClassInvokeInfo const *lciInfo, bool wrapToObject)
{
    auto *allocator = ctx->allocator;
    auto *parser = ctx->parser->AsETSParser();

    auto callArguments = [&lciInfo, &ctx, &info, &wrapToObject]() {
        switch (lciInfo->invokeType) {
            case InvokeType::CONVENTIONAL:
                return CreateCallArgumentsForLambdaClassInvoke(ctx, info, lciInfo, wrapToObject);
            case InvokeType::INVOKE_N:
                return CreateCallArgumentsForLambdaClassInvokeN(ctx, info, lciInfo);
            case InvokeType::OPTIONAL_PARAM_NO_REST:
                return CreateCallArgumentsForLambdaClassInvoke(ctx, info, lciInfo, true);
        }
        ES2PANDA_UNREACHABLE();
    }();

    ir::Expression *const calleeReceiver =
        info->callReceiver != nullptr
            ? parser->CreateFormattedExpression("this.@@I1", "$this")
            : lciInfo->callee->Parent()->AsClassDefinition()->Ident()->Clone(allocator, nullptr);

    auto *calleeMemberExpr = util::NodeAllocator::ForceSetParent<ir::MemberExpression>(
        allocator, calleeReceiver, lciInfo->callee->Key()->Clone(allocator, nullptr)->AsExpression(),
        ir::MemberExpressionKind::PROPERTY_ACCESS, false, false);

    auto *call = util::NodeAllocator::ForceSetParent<ir::CallExpression>(allocator, calleeMemberExpr,
                                                                         std::move(callArguments), nullptr, false);

    // NOTE (smartin): the condition would be better to check the size of the signature's type parameters. But currently
    // generic lambdas don't allocate type parameters for they global invoke function, so fix this when the generation
    // will be corrected
    if (lciInfo->callee->Function()->TypeParams() != nullptr && info->funcRefTypeParams == nullptr) {
        auto origCallTypeParams = lciInfo->lambdaSignature->TypeParams();
        auto typeArgs = ArenaVector<ir::TypeNode *>(allocator->Adapter());
        for (auto *tp : origCallTypeParams) {
            typeArgs.push_back(allocator->New<ir::OpaqueTypeNode>(
                tp->Substitute(ctx->GetChecker()->Relation(), lciInfo->substitution), allocator));
        }
        auto *typeArg =
            util::NodeAllocator::ForceSetParent<ir::TSTypeParameterInstantiation>(allocator, std::move(typeArgs));
        call->SetTypeParams(typeArg);
        typeArg->SetParent(call);
    } else if (info->funcRefTypeParams != nullptr) {
        SetTypeParamsForInvokeSignature(ctx, info, lciInfo, call);
    }

    if (lciInfo->classDefinition->TypeParams() != nullptr) {
        auto typeArgs = ArenaVector<ir::TypeNode *>(allocator->Adapter());
        for (auto *tp : lciInfo->classDefinition->TypeParams()->Params()) {
            typeArgs.push_back(
                allocator->New<ir::OpaqueTypeNode>(tp->Name()->AsIdentifier()->Variable()->TsType(), allocator));
        }
        auto *typeArg =
            util::NodeAllocator::ForceSetParent<ir::TSTypeParameterInstantiation>(allocator, std::move(typeArgs));
        call->SetTypeParams(typeArg);
        typeArg->SetParent(call);
    }

    return call;
}

static void AddReturnStmtToInvokeBodyStatements(public_lib::Context *ctx, LambdaClassInvokeInfo *lciInfo,
                                                ir::CallExpression *call,
                                                ArenaVector<ark::es2panda::ir::Statement *> &bodyStmts)
{
    auto *allocator = ctx->allocator;
    auto *parser = ctx->parser->AsETSParser();
    const auto *checker = ctx->GetChecker()->AsETSChecker();
    auto *anyType = checker->GlobalETSAnyType();

    if (!lciInfo->lambdaSignature->ReturnType()->IsETSUndefinedType()) {
        auto *returnExpr = parser->CreateFormattedExpression("@@E1 as @@T2", call, anyType);
        auto *returnStmt = util::NodeAllocator::ForceSetParent<ir::ReturnStatement>(allocator, returnExpr);
        bodyStmts.push_back(returnStmt);
        return;
    }

    auto *callStmt = util::NodeAllocator::ForceSetParent<ir::ExpressionStatement>(allocator, call);
    bodyStmts.push_back(callStmt);
    auto *returnStmt =
        util::NodeAllocator::ForceSetParent<ir::ReturnStatement>(allocator, allocator->New<ir::UndefinedLiteral>());
    bodyStmts.push_back(returnStmt);
}

static void AddRestParameterDestructuringToInvokeBodyStatements(public_lib::Context *ctx,
                                                                LambdaClassInvokeInfo *lciInfo,
                                                                ArenaVector<ir::Statement *> &bodyStmts)
{
    auto *allocator = ctx->allocator;
    auto *parser = ctx->parser->AsETSParser();
    auto *checker = ctx->GetChecker()->AsETSChecker();

    ES2PANDA_ASSERT(lciInfo->restParameterIdentifier != "");
    auto *tempVarNames = allocator->New<ArenaVector<util::UString>>(allocator->Adapter());
    const std::size_t stopIdx = lciInfo->lambdaSignature->ArgCount() - lciInfo->arity;

    // If this loop runs, then the signature has optional parameter(s) and maybe rest parameter too. In the latter
    // case, the first optional parameter number of elements (that is the value of 'stopIdx') of the rest parameter is
    // needed to be passed into the optional parameter indexes of the called function.
    for (size_t idx = 0; idx < stopIdx; ++idx) {
        const auto *lambdaParam = lciInfo->lambdaSignature->Params().at(idx + lciInfo->arity);
        auto argName = lciInfo->restParameterIdentifier;
        auto *type = lambdaParam->TsType()->Substitute(checker->Relation(), lciInfo->substitution);
        std::stringstream stream;
        stream << "let @@I1 = @@I2.length > " << idx << " ? @@I3[" << idx << "] as @@T4 : undefined;";
        tempVarNames->push_back(GenName(allocator));
        auto *stmt = parser->CreateFormattedStatement(stream.str(), tempVarNames->back(), argName, argName, type);
        bodyStmts.push_back(stmt);
    }

    lciInfo->argNames = tempVarNames;
}

static ir::BlockStatement *CreateLambdaClassInvokeBody(public_lib::Context *ctx, LambdaInfo const *info,
                                                       LambdaClassInvokeInfo *lciInfo)
{
    auto *allocator = ctx->allocator;

    auto bodyStmts =
        CreateRestArgumentsArrayReallocation(ctx, lciInfo, lciInfo->lambdaSignature->ArgCount() - lciInfo->arity);

    const bool hasOptionalParam = lciInfo->invokeType == InvokeType::OPTIONAL_PARAM_NO_REST;
    if (hasOptionalParam || lciInfo->lambdaSignature->HasRestParameter()) {
        AddRestParameterDestructuringToInvokeBodyStatements(ctx, lciInfo, bodyStmts);
    }

    auto *call = CreateCallForLambdaClassInvoke(ctx, info, lciInfo, true);
    AddReturnStmtToInvokeBodyStatements(ctx, lciInfo, call, bodyStmts);

    return util::NodeAllocator::ForceSetParent<ir::BlockStatement>(allocator, allocator, std::move(bodyStmts));
}

static void CreateLambdaClassInvokeMethod(public_lib::Context *ctx, LambdaInfo const *info,
                                          LambdaClassInvokeInfo *lciInfo, util::StringView methodName)
{
    auto *allocator = ctx->allocator;
    const auto *checker = ctx->GetChecker()->AsETSChecker();
    auto *anyType = checker->GlobalETSAnyType();

    auto *invokeSigTypeParams = CloneTypeParamsForSignature(ctx, lciInfo, info->funcRefTypeParams != nullptr);

    auto params = ArenaVector<ir::Expression *>(allocator->Adapter());
    for (size_t idx = 0; idx < lciInfo->arity; ++idx) {
        const auto *lparam = lciInfo->lambdaSignature->Params().at(idx);
        auto *id = util::NodeAllocator::ForceSetParent<ir::Identifier>(
            allocator, lparam->Name(), allocator->New<ir::OpaqueTypeNode>(anyType, allocator), allocator);
        auto *param = util::NodeAllocator::ForceSetParent<ir::ETSParameterExpression>(allocator, id, false, allocator);
        params.push_back(param);
    }

    if (lciInfo->lambdaSignature->HasRestParameter() || (lciInfo->invokeType == InvokeType::OPTIONAL_PARAM_NO_REST)) {
        CreateInvokeMethodRestParameter(ctx, lciInfo, &params);
        lciInfo->restArgumentIdentifier = GenName(allocator).View();
    }

    auto *returnType2 = allocator->New<ir::OpaqueTypeNode>(anyType, allocator);
    const bool hasReceiver = lciInfo->lambdaSignature->HasSignatureFlag(checker::SignatureFlags::EXTENSION_FUNCTION);
    auto functionFlag = ir::ScriptFunctionFlags::METHOD;
    auto *const newBody = CreateLambdaClassInvokeBody(ctx, info, lciInfo);

    auto *func = util::NodeAllocator::ForceSetParent<ir::ScriptFunction>(
        allocator, allocator,
        ir::ScriptFunction::ScriptFunctionData {
            newBody, ir::FunctionSignature(invokeSigTypeParams, std::move(params), returnType2, hasReceiver),
            functionFlag});

    auto *invokeId = allocator->New<ir::Identifier>(methodName, allocator);
    func->SetIdent(invokeId);

    auto *funcExpr = util::NodeAllocator::ForceSetParent<ir::FunctionExpression>(allocator, func);

    auto *invokeIdClone = invokeId->Clone(allocator, nullptr);
    auto *invokeMethod = util::NodeAllocator::ForceSetParent<ir::MethodDefinition>(
        allocator, ir::MethodDefinitionKind::METHOD, invokeIdClone, funcExpr, ir::ModifierFlags::NONE, allocator,
        false);
    ES2PANDA_ASSERT(!invokeMethod->IsStatic());

    lciInfo->classDefinition->EmplaceBody(invokeMethod);
    invokeMethod->SetParent(lciInfo->classDefinition);
}

static ir::BlockStatement *CreateLambdaClassInvokeNBody(public_lib::Context *ctx, LambdaInfo const *info,
                                                        LambdaClassInvokeInfo *lciInfo)
{
    auto *allocator = ctx->allocator;
    auto *checker = ctx->GetChecker()->AsETSChecker();
    auto *parser = ctx->parser->AsETSParser();

    auto bodyStmts = CreateRestArgumentsArrayReallocation(ctx, lciInfo, lciInfo->lambdaSignature->MinArgCount());
    auto *tempVarNames = allocator->New<ArenaVector<util::UString>>(allocator->Adapter());

    for (size_t idx = lciInfo->lambdaSignature->MinArgCount(); idx < lciInfo->arity; ++idx) {
        auto *lambdaParam = lciInfo->lambdaSignature->Params().at(idx);
        const auto argName = lciInfo->restParameterIdentifier;
        auto *type = lambdaParam->TsType()->Substitute(checker->Relation(), lciInfo->substitution);
        std::stringstream stream;
        stream << "let @@I1 = @@I2.length > " << idx << " ? @@I3[" << idx << "] as @@T4 : undefined;";
        tempVarNames->push_back(GenName(allocator));
        auto *stmt = parser->CreateFormattedStatement(stream.str(), tempVarNames->back(), argName, argName, type);
        bodyStmts.push_back(stmt);
    }

    lciInfo->argNames = tempVarNames;
    auto *call = CreateCallForLambdaClassInvoke(ctx, info, lciInfo, false);
    AddReturnStmtToInvokeBodyStatements(ctx, lciInfo, call, bodyStmts);

    return util::NodeAllocator::ForceSetParent<ir::BlockStatement>(allocator, allocator, std::move(bodyStmts));
}

static void CreateLambdaClassInvokeN(public_lib::Context *ctx, LambdaInfo const *info, LambdaClassInvokeInfo *lciInfo)
{
    auto *allocator = ctx->allocator;
    auto *checker = ctx->GetChecker()->AsETSChecker();
    auto *anyType = checker->GlobalETSAnyType();

    auto params = ArenaVector<ir::Expression *>(allocator->Adapter());
    CreateInvokeMethodRestParameter(ctx, lciInfo, &params);

    if (lciInfo->lambdaSignature->HasRestParameter()) {
        lciInfo->restArgumentIdentifier = GenName(allocator).View();
    }

    auto *returnType = allocator->New<ir::OpaqueTypeNode>(anyType, allocator);
    const bool hasReceiver = lciInfo->lambdaSignature->HasSignatureFlag(checker::SignatureFlags::EXTENSION_FUNCTION);
    const auto functionFlag = ir::ScriptFunctionFlags::METHOD;
    auto *func = util::NodeAllocator::ForceSetParent<ir::ScriptFunction>(
        allocator, allocator,
        ir::ScriptFunction::ScriptFunctionData {
            CreateLambdaClassInvokeNBody(ctx, info, lciInfo),
            ir::FunctionSignature(nullptr, std::move(params), returnType, hasReceiver), functionFlag});

    auto *invokeId = allocator->New<ir::Identifier>("unsafeCall", allocator);
    func->SetIdent(invokeId);

    auto *funcExpr = util::NodeAllocator::ForceSetParent<ir::FunctionExpression>(allocator, func);

    auto *invokeIdClone = invokeId->Clone(allocator, nullptr);
    auto *invokeMethod = util::NodeAllocator::ForceSetParent<ir::MethodDefinition>(
        allocator, ir::MethodDefinitionKind::METHOD, invokeIdClone, funcExpr, ir::ModifierFlags::NONE, allocator,
        false);
    ES2PANDA_ASSERT(!invokeMethod->IsStatic());

    lciInfo->classDefinition->EmplaceBody(invokeMethod);
    invokeMethod->SetParent(lciInfo->classDefinition);
}

static checker::ETSObjectType *FunctionTypeToLambdaProviderType(checker::ETSChecker *checker,
                                                                checker::Signature *signature)
{
    if (signature->RestVar() != nullptr) {
        ES2PANDA_ASSERT(checker->GlobalBuiltinLambdaType(signature->ArgCount(), true));
        return checker->GlobalBuiltinLambdaType(signature->ArgCount(), true)->AsETSObjectType();
    }
    return checker->GlobalBuiltinLambdaType(signature->ArgCount(), false)->AsETSObjectType();
}

static util::StringView GetInvokeMethodNameStringView(public_lib::Context *ctx, const std::size_t arity,
                                                      const bool hasRestParam)
{
    auto *checker = ctx->GetChecker()->AsETSChecker();

    return util::UString {checker->FunctionalInterfaceInvokeName(arity, hasRestParam), ctx->allocator}.View();
}

// The `invoke` and `invoke0` of extension lambda class has two `this` identifier in parameter scope,
// first one is the lambdaClass itself and second one is the receiver class,
// the true `this` of the `invoke` and `invoke0` functionScope is the lambdaClass.
static void CorrectTheTrueThisForExtensionLambda(public_lib::Context *ctx, ir::ClassDeclaration *lambdaClass,
                                                 size_t arity, bool hasRestParam)
{
    auto *classScope = lambdaClass->Definition()->Scope();
    std::vector<varbinder::Variable *> invokeFuncsOfLambda {};
    auto invokeName = GetInvokeMethodNameStringView(ctx, arity, hasRestParam);
    invokeFuncsOfLambda.emplace_back(
        classScope->FindLocal(compiler::Signatures::LAMBDA_OBJECT_INVOKE, varbinder::ResolveBindingOptions::METHODS));
    invokeFuncsOfLambda.emplace_back(classScope->FindLocal(invokeName, varbinder::ResolveBindingOptions::METHODS));
    for (auto *invokeFuncOfLambda : invokeFuncsOfLambda) {
        if (invokeFuncOfLambda == nullptr) {
            continue;
        }
        auto *scriptFunc = invokeFuncOfLambda->Declaration()
                               ->AsFunctionDecl()
                               ->Node()
                               ->AsMethodDefinition()
                               ->Value()
                               ->AsFunctionExpression()
                               ->Function();
        if (!scriptFunc->Signature()->HasSignatureFlag(checker::SignatureFlags::EXTENSION_FUNCTION)) {
            ES2PANDA_ASSERT(!scriptFunc->IsExtensionMethod());
            continue;
        }
        ES2PANDA_ASSERT(scriptFunc->IsExtensionMethod());
        auto *functionScope = scriptFunc->Scope();
        auto *functionParamScope = scriptFunc->Scope()->ParamScope();
        auto *theTrueThisVar = functionParamScope->Params()[0];
        auto &bindings = const_cast<varbinder::Scope::VariableMap &>(functionScope->Bindings());
        bindings.erase(varbinder::ETSBinder::MANDATORY_PARAM_THIS);
        bindings.insert({varbinder::ETSBinder::MANDATORY_PARAM_THIS, theTrueThisVar});
    }
}

static ir::ClassDeclaration *CreateEmptyLambdaClassDeclaration(public_lib::Context *ctx, LambdaInfo const *info,
                                                               ir::TSTypeParameterDeclaration *newTypeParams,
                                                               checker::ETSObjectType *fnInterface,
                                                               checker::ETSObjectType *lambdaProviderClass)
{
    auto *allocator = ctx->allocator;
    auto *parser = ctx->parser->AsETSParser();
    auto *varBinder = ctx->GetChecker()->VarBinder()->AsETSBinder();

    auto lambdaClassName = util::UString {
        std::string_view {util::NameMangler::GetInstance()->CreateMangledNameForLambdaObject(info->name)}, allocator};

    ES2PANDA_ASSERT(lambdaProviderClass);
    auto providerTypeNode = allocator->New<ir::OpaqueTypeNode>(lambdaProviderClass, allocator);
    auto classIdent = allocator->New<ir::Identifier>(lambdaClassName.View(), allocator);

    std::stringstream ss;
    if (!info->originalFuncName.Empty()) {
        ss << "@" << ARKRUNTIME_IMPORT_ALIAS_PREFIX << "annotation." << Signatures::NAMED_FUNCTION_OBJECT
           << "({name: \"" << info->originalFuncName << "\"})";
    }

    if (info->isFunctionAsync) {
        ss << "@" << ARKRUNTIME_IMPORT_ALIAS_PREFIX << "annotation." << Signatures::ASYNC_FUNCTION_OBJECT << "()";
    }

    std::vector<ir::AstNode *> statementParams;

    if (fnInterface == nullptr) {
        ss << " final class @@I1 extends @@T2 {}";
        statementParams = {classIdent, providerTypeNode};
    } else {
        ss << " final class @@I1 extends @@T2 implements @@T3 {}";
        auto fnInterfaceTypeNode = allocator->New<ir::OpaqueTypeNode>(fnInterface, allocator);
        statementParams = {classIdent, providerTypeNode, fnInterfaceTypeNode};
    }

    auto *classDeclaration = parser->CreateFormattedTopLevelStatement(ss.str(), statementParams)->AsClassDeclaration();
    auto *classDefinition = classDeclaration->Definition();

    // Adjust the class definition compared to what the parser gives.
    classDefinition->ClearBody();  // remove the default empty constructor
    classDefinition->AddModifier(ir::ModifierFlags::PUBLIC | ir::ModifierFlags::FUNCTIONAL);
    if (newTypeParams != nullptr) {
        classDefinition->SetTypeParams(newTypeParams);
        newTypeParams->SetParent(classDefinition);
    }

    auto *program = varBinder->GetRecordTable()->Program();
    program->Ast()->AddStatement(classDeclaration);
    classDeclaration->SetParent(program->Ast());

    return classDeclaration;
}

static void SetModifiersForFunctionReference(ir::ClassDefinition *classDefinition, ir::MethodDefinition *callee,
                                             LambdaInfo const *info)
{
    if (info->isFunctionReference) {
        ES2PANDA_ASSERT(callee->Function());
        classDefinition->SetFunctionalReferenceReferencedMethod(callee);
        classDefinition->SetModifiers(classDefinition->Modifiers() |
                                      ir::ClassDefinitionModifiers::FUNCTIONAL_REFERENCE);
    }
}

static void GenerateRestInvokeForOptionalParams(public_lib::Context *ctx, LambdaInfo const *info,
                                                LambdaClassInvokeInfo &lciInfo)
{
    // For the arity == signature->ArgCount() case, it's already implemented in the corresponding LambdaN
    // class in stdlib. Signatures with rest parameters are already handled.
    if ((lciInfo.arity == lciInfo.lambdaSignature->ArgCount()) || lciInfo.lambdaSignature->HasRestParameter()) {
        return;
    }

    auto restInvokeMethodName = GetInvokeMethodNameStringView(ctx, lciInfo.arity, true);

    lciInfo.invokeType = InvokeType::OPTIONAL_PARAM_NO_REST;
    CreateLambdaClassInvokeMethod(ctx, info, &lciInfo, restInvokeMethodName);
    lciInfo.invokeType = InvokeType::CONVENTIONAL;
}

static void GenerateSmallerArityRestInvokesForLambdaN(public_lib::Context *ctx, LambdaInfo const *info,
                                                      LambdaClassInvokeInfo &lciInfo)
{
    auto *checker = ctx->GetChecker()->AsETSChecker();
    const auto *const signature = lciInfo.lambdaSignature;

    for (size_t arity = signature->MinArgCount(); arity < checker->GlobalBuiltinFunctionTypeVariadicThreshold();
         ++arity) {
        lciInfo.arity = arity;
        if (signature->HasRestParameter()) {
            const auto invokeMethodName = GetInvokeMethodNameStringView(ctx, arity, signature->HasRestParameter());
            CreateLambdaClassInvokeMethod(ctx, info, &lciInfo, invokeMethodName);
        } else {
            GenerateRestInvokeForOptionalParams(ctx, info, lciInfo);
        }
    }
}

static void GenerateInvokesForLambdaN(public_lib::Context *ctx, LambdaInfo const *info, LambdaClassInvokeInfo &lciInfo)
{
    GenerateSmallerArityRestInvokesForLambdaN(ctx, info, lciInfo);
    lciInfo.arity = lciInfo.lambdaSignature->ArgCount();
    lciInfo.invokeType = InvokeType::INVOKE_N;
    CreateLambdaClassInvokeN(ctx, info, &lciInfo);
    lciInfo.invokeType = InvokeType::CONVENTIONAL;
}

static void GenerateInvokesForDefinedArityLambda(public_lib::Context *ctx, LambdaInfo const *info,
                                                 LambdaClassInvokeInfo &lciInfo)
{
    const auto *const signature = lciInfo.lambdaSignature;

    for (size_t arity = signature->MinArgCount(); arity <= signature->ArgCount(); ++arity) {
        lciInfo.arity = arity;
        const auto invokeMethodName = GetInvokeMethodNameStringView(ctx, arity, signature->HasRestParameter());
        CreateLambdaClassInvokeMethod(ctx, info, &lciInfo, invokeMethodName);
        GenerateRestInvokeForOptionalParams(ctx, info, lciInfo);
    }
}

static ir::ClassDeclaration *CreateLambdaClass(public_lib::Context *ctx, checker::ETSFunctionType *fntype,
                                               ir::MethodDefinition *callee, const LambdaInfo *info)
{
    auto *checker = ctx->GetChecker()->AsETSChecker();
    auto *varBinder = ctx->GetChecker()->VarBinder()->AsETSBinder();

    auto [newTypeParams, subst0] = CloneTypeParameters(ctx, info, ctx->parserProgram->GlobalClassScope());
    auto &substitution = subst0;  // NOTE(gogabr): needed to capture in a lambda later.

    auto signature = fntype->ArrowSignature();
    auto fnInterface = fntype->Substitute(checker->Relation(), &substitution)->ArrowToFunctionalInterface(checker);
    auto lambdaProviderClass =
        FunctionTypeToLambdaProviderType(checker, signature)->Substitute(checker->Relation(), &substitution);

    auto lexScope = varbinder::LexicalScope<varbinder::Scope>::Enter(varBinder, ctx->parserProgram->GlobalClassScope());

    auto classDeclaration = CreateEmptyLambdaClassDeclaration(
        ctx, info, newTypeParams, signature->MinArgCount() != signature->ArgCount() ? fnInterface : nullptr,
        lambdaProviderClass);
    auto classDefinition = classDeclaration->Definition();
    SetModifiersForFunctionReference(classDefinition, callee, info);

    CreateLambdaClassFields(ctx, classDefinition, info, &substitution);
    CreateLambdaClassConstructor(ctx, classDefinition, info, &substitution);

    LambdaClassInvokeInfo lciInfo;
    lciInfo.callee = callee;
    lciInfo.classDefinition = classDefinition;
    lciInfo.substitution = &substitution;
    lciInfo.lambdaSignature = signature;

    if (signature->ArgCount() < checker->GlobalBuiltinFunctionTypeVariadicThreshold()) {
        GenerateInvokesForDefinedArityLambda(ctx, info, lciInfo);
    } else {
        GenerateInvokesForLambdaN(ctx, info, lciInfo);
    }

    InitScopesPhaseETS::RunExternalNode(classDeclaration, varBinder);
    varBinder->ResolveReferencesForScopeWithContext(classDeclaration, varBinder->TopScope());
    classDeclaration->Check(checker);
    CorrectTheTrueThisForExtensionLambda(ctx, classDeclaration, signature->MinArgCount(),
                                         signature->HasRestParameter());
    return classDeclaration;
}

static ir::ETSNewClassInstanceExpression *CreateConstructorCall(public_lib::Context *ctx,
                                                                ir::TypedAstNode *lambdaOrFuncRef,
                                                                ir::ClassDeclaration *lambdaClass,
                                                                const LambdaInfo *info)
{
    auto *allocator = ctx->allocator;
    auto *varBinder = ctx->GetChecker()->VarBinder()->AsETSBinder();
    auto *checker = ctx->GetChecker()->AsETSChecker();

    auto args = ArenaVector<ir::Expression *>(allocator->Adapter());
    if (info->callReceiver != nullptr) {
        args.push_back(info->callReceiver);
    }
    for (auto captured : *info->capturedVars) {
        auto *id = allocator->New<ir::Identifier>(captured->Name(), allocator);
        id->SetRange(captured->Declaration()->Node()->Range());
        args.push_back(id);
    }

    checker::ETSObjectType *constructedType = lambdaClass->Definition()->TsType()->AsETSObjectType();
    if (info->enclosingFunction != nullptr) {
        constructedType = constructedType->SubstituteArguments(
            checker->Relation(),
            (info->enclosingFunction->Signature()->TypeParams().empty() && info->callReceiver == nullptr)
                ? info->calleeClass->Definition()->TsType()->AsETSObjectType()->TypeArguments()
                : info->enclosingFunction->Signature()->TypeParams());
    }
    auto *newExpr = util::NodeAllocator::ForceSetParent<ir::ETSNewClassInstanceExpression>(
        allocator, allocator->New<ir::OpaqueTypeNode>(constructedType, allocator), std::move(args));
    auto *lambdaOrFuncRefParent = lambdaOrFuncRef->Parent();
    ES2PANDA_ASSERT(newExpr);
    newExpr->SetParent(lambdaOrFuncRefParent);
    // NOTE(dslynko, #19869): Required for correct debug-info generation
    ES2PANDA_ASSERT(newExpr);
    newExpr->SetRange(lambdaOrFuncRefParent != nullptr ? lambdaOrFuncRefParent->Range() : lambdaOrFuncRef->Range());

    auto *nearestScope = NearestScope(lambdaOrFuncRef);
    auto lexScope = varbinder::LexicalScope<varbinder::Scope>::Enter(varBinder, nearestScope);
    varBinder->ResolveReferencesForScopeWithContext(newExpr, nearestScope);

    checker::Type *objectType =
        info->calleeClass != nullptr ? info->calleeClass->Definition()->TsType() : info->calleeInterface->TsType();
    auto checkerCtx = checker::SavedCheckerContext(ctx->GetChecker(), checker::CheckerStatus::IN_CLASS,
                                                   objectType->AsETSObjectType());
    auto scopeCtx = checker::ScopeContext(ctx->GetChecker(), nearestScope);
    newExpr->Check(checker);

    // NOTE: We need to set back the TsType, which is ETSFunctionType, to ensure to insert correct invoke calls.
    ES2PANDA_ASSERT(lambdaOrFuncRef->TsType()->IsETSFunctionType());
    newExpr->SetTsType(lambdaOrFuncRef->TsType());

    return newExpr;
}

static ir::AstNode *ConvertLambda(public_lib::Context *ctx, ir::ArrowFunctionExpression *lambda)
{
    auto *allocator = ctx->allocator;
    auto *checker = ctx->GetChecker()->AsETSChecker();

    lambda->Check(checker);
    ES2PANDA_ASSERT(lambda->TsType()->IsETSFunctionType());

    LambdaInfo info;
    ir::AstNode *enclosingClass = nullptr;
    std::tie(enclosingClass, info.enclosingFunction) = FindEnclosingClassAndFunction(lambda);
    if (enclosingClass->IsClassDeclaration()) {
        info.calleeClass = enclosingClass->AsClassDeclaration();
    } else {
        info.calleeInterface = enclosingClass->AsTSInterfaceDeclaration();
    }
    info.name = CreateCalleeName(allocator);

    if ((lambda->Parent() != nullptr) && lambda->Parent()->IsVariableDeclarator()) {
        info.originalFuncName = lambda->Parent()->AsVariableDeclarator()->Id()->AsIdentifier()->Name();
    } else if ((lambda->Parent() != nullptr) && lambda->Parent()->IsClassProperty()) {
        info.originalFuncName = lambda->Parent()->AsClassProperty()->Id()->Name();
    } else if ((lambda->Parent() != nullptr) && lambda->Parent()->IsAssignmentExpression() &&
               lambda->Parent()->AsAssignmentExpression()->Left()->IsIdentifier()) {
        info.originalFuncName = lambda->Parent()->AsAssignmentExpression()->Left()->AsIdentifier()->Name();
    }

    auto capturedVars = FindCaptured(allocator, lambda);
    info.capturedVars = &capturedVars;
    if (auto *thisOrSuper = FindIfNeedThis(lambda, checker); thisOrSuper != nullptr) {
        info.callReceiver = allocator->New<ir::ThisExpression>();
        info.callReceiver->SetRange(lambda->Parent()->Range());
    } else if (info.calleeInterface != nullptr) {
        info.callReceiver = allocator->New<ir::ThisExpression>();
    }
    info.isFunctionReference = false;
    info.isFunctionAsync = lambda->Function()->IsAsyncFunc();

    auto *callee = CreateCallee(ctx, lambda, &info);
    auto *lambdaType = lambda->TsType()->AsETSFunctionType();
    auto *lambdaClass = CreateLambdaClass(ctx, lambdaType, callee, &info);
    return CreateConstructorCall(ctx, lambda, lambdaClass, &info);
}

static ir::ScriptFunction *GetWrappingLambdaParentFunction(public_lib::Context *ctx, ir::Expression *funcRef,
                                                           checker::Signature *signature)
{
    auto *allocator = ctx->allocator;
    ArenaVector<ir::Expression *> params {allocator->Adapter()};
    for (auto *p : signature->Params()) {
        params.push_back(util::NodeAllocator::ForceSetParent<ir::ETSParameterExpression>(
            allocator,
            allocator->New<ir::Identifier>(p->Name(), allocator->New<ir::OpaqueTypeNode>(p->TsType(), allocator),
                                           allocator),
            false, allocator));
    }
    auto *func = util::NodeAllocator::ForceSetParent<ir::ScriptFunction>(
        allocator, allocator,
        ir::ScriptFunction::ScriptFunctionData {
            nullptr,
            ir::FunctionSignature {nullptr, std::move(params),
                                   allocator->New<ir::OpaqueTypeNode>(signature->ReturnType(), allocator)},
            ir::ScriptFunctionFlags::ARROW});
    ES2PANDA_ASSERT(func != nullptr);
    ArenaVector<ir::Statement *> bodyStmts {allocator->Adapter()};
    ArenaVector<ir::Expression *> callArgs {allocator->Adapter()};

    for (auto *p : func->Params()) {
        ir::Identifier *clone = p->AsETSParameterExpression()->Ident()->Clone(allocator, nullptr);
        ES2PANDA_ASSERT(clone != nullptr);
        if (clone->IsIdentifier() && (clone->IsReference(ScriptExtension::ETS)) &&
            (clone->TypeAnnotation() != nullptr)) {
            clone->SetTsTypeAnnotation(nullptr);
        }
        callArgs.push_back(clone);
    }
    auto *callExpr = util::NodeAllocator::ForceSetParent<ir::CallExpression>(allocator, funcRef, std::move(callArgs),
                                                                             nullptr, false);
    ir::Statement *stmt;
    if (signature->ReturnType()->IsETSUndefinedType()) {
        stmt = util::NodeAllocator::ForceSetParent<ir::ExpressionStatement>(allocator, callExpr);
    } else {
        stmt = util::NodeAllocator::ForceSetParent<ir::ReturnStatement>(allocator, callExpr);
    }
    bodyStmts.push_back(stmt);
    func->SetBody(util::NodeAllocator::ForceSetParent<ir::BlockStatement>(allocator, allocator, std::move(bodyStmts)));
    ES2PANDA_ASSERT(func->Body());
    func->Body()->SetParent(func);
    return func;
}

static ir::ArrowFunctionExpression *CreateWrappingLambda(public_lib::Context *ctx, ir::Expression *funcRef)
{
    auto *allocator = ctx->allocator;
    auto *varBinder = ctx->GetChecker()->VarBinder()->AsETSBinder();
    ES2PANDA_ASSERT(funcRef->TsType()->IsETSArrowType());
    auto signature = funcRef->TsType()->AsETSFunctionType()->ArrowSignature();

    auto *parent = funcRef->Parent();
    auto *func = GetWrappingLambdaParentFunction(ctx, funcRef, signature);

    auto *lambda = util::NodeAllocator::ForceSetParent<ir::ArrowFunctionExpression>(allocator, func, allocator);
    ES2PANDA_ASSERT(lambda);
    lambda->SetParent(parent);

    auto *nearestScope = NearestScope(lambda);
    auto lexScope = varbinder::LexicalScope<varbinder::Scope>::Enter(varBinder, nearestScope);
    InitScopesPhaseETS::RunExternalNode(lambda, varBinder);
    varBinder->ResolveReferencesForScopeWithContext(lambda, nearestScope);

    auto [enclosingClass, _] = FindEnclosingClassAndFunction(parent);
    auto *tsType = enclosingClass->IsClassDeclaration() ? enclosingClass->AsClassDeclaration()->Definition()->TsType()
                                                        : enclosingClass->AsTSInterfaceDeclaration()->TsType();

    auto checkerCtx =
        checker::SavedCheckerContext(ctx->GetChecker(), checker::CheckerStatus::IN_CLASS, tsType->AsETSObjectType());
    auto scopeCtx = checker::ScopeContext(ctx->GetChecker(), nearestScope);
    lambda->Check(ctx->GetChecker()->AsETSChecker());

    return lambda;
}

static LambdaInfo GenerateLambdaInfoForFunctionReference(public_lib::Context *ctx, ir::Expression *funcRef,
                                                         ir::MethodDefinition *method)
{
    auto *allocator = ctx->allocator;
    LambdaInfo info;

    if (method->Parent()->Parent()->IsClassDeclaration()) {
        info.calleeClass = method->Parent()->Parent()->AsClassDeclaration();
        info.calleeClass->Check(ctx->GetChecker()->AsETSChecker());
    } else if (method->Parent()->Parent()->IsTSInterfaceDeclaration()) {
        info.calleeInterface = method->Parent()->Parent()->AsTSInterfaceDeclaration();
        info.calleeInterface->Check(ctx->GetChecker()->AsETSChecker());
    } else {
        ES2PANDA_UNREACHABLE();
    }

    info.enclosingFunction = nullptr;
    info.name = CreateCalleeName(allocator);
    info.originalFuncName = method->Id()->Name();
    info.isFunctionAsync = method->IsAsync();
    info.capturedVars = allocator->New<ArenaSet<varbinder::Variable *>>(allocator->Adapter());
    info.isFunctionReference = true;

    if (method->IsStatic()) {
        info.callReceiver = nullptr;
    } else {
        ES2PANDA_ASSERT(funcRef->IsMemberExpression());
        info.callReceiver = funcRef->AsMemberExpression()->Object();
    }

    if (funcRef->IsMemberExpression()) {
        info.objType = funcRef->AsMemberExpression()->ObjType();
    }

    if (funcRef->Parent()->IsETSGenericInstantiatedNode()) {
        info.funcRefTypeParams = funcRef->Parent()->AsETSGenericInstantiatedNode()->TypeParams();
    }

    return info;
}

static ir::AstNode *ConvertFunctionReference(public_lib::Context *ctx, ir::Expression *funcRef)
{
    ES2PANDA_ASSERT(funcRef->IsIdentifier() ||
                    (funcRef->IsMemberExpression() &&
                     funcRef->AsMemberExpression()->Kind() == ir::MemberExpressionKind::PROPERTY_ACCESS &&
                     funcRef->AsMemberExpression()->Property()->IsIdentifier()));
    varbinder::Variable *var;
    if (funcRef->IsIdentifier()) {
        var = funcRef->AsIdentifier()->Variable();
    } else {
        auto *mexpr = funcRef->AsMemberExpression();
        var = mexpr->PropVar();
        ES2PANDA_ASSERT(var != nullptr);
    }

    ES2PANDA_ASSERT(var->Declaration()->Node()->IsMethodDefinition());
    auto *method = var->Declaration()->Node()->AsMethodDefinition();

    if (method->IsPrivate() || method->IsProtected()) {
        // Direct reference to method will be impossible from the lambda class, so replace func ref with a lambda
        // that will translate to a proxy method
        auto *lam = CreateWrappingLambda(ctx, funcRef);
        return lam == nullptr ? funcRef : ConvertLambda(ctx, lam);
    }

    const LambdaInfo info = GenerateLambdaInfoForFunctionReference(ctx, funcRef, method);

    auto *funcRefType = funcRef->TsType();
    if (funcRef->Parent()->IsETSGenericInstantiatedNode()) {
        // The lambda classes are generated postorder, so the function reference will be seen before the instantiation
        // node. To generate correct code, we need to check if there exist a corresponding instantiation node (which is
        // always it's parent) for this function ref
        funcRefType = funcRef->Parent()->AsTyped()->TsType();
    }

    ES2PANDA_ASSERT(funcRefType->IsETSArrowType());
    auto *lambdaClass = CreateLambdaClass(ctx, funcRefType->AsETSFunctionType(), method, &info);
    auto *constructorCall = CreateConstructorCall(ctx, funcRef, lambdaClass, &info);
    ES2PANDA_ASSERT(constructorCall);
    if (constructorCall->TsType()->IsETSObjectType()) {
        constructorCall->TsType()->AsETSObjectType()->AddObjectFlag(checker::ETSObjectFlags::FUNCTIONAL_REFERENCE);
    }

    return constructorCall;
}

static bool IsVariableOriginalAccessor(const varbinder::Variable *var)
{
    return checker::ETSChecker::IsVariableGetterSetter(var) && !(checker::ETSChecker::IsVariableExtensionAccessor(var));
}

static varbinder::Variable *GetNodeOrPropertyVariable(ir::AstNode const *node)
{
    if (auto *const variable = node->Variable(); variable != nullptr) {
        return variable;
    }

    if (!node->IsMemberExpression()) {
        return nullptr;
    }

    auto *const memberExpr = node->AsMemberExpression();
    if (!memberExpr->HasMemberKind(ir::MemberExpressionKind::PROPERTY_ACCESS)) {
        return nullptr;
    }

    return memberExpr->Property()->Variable();
}

static bool IsFunctionOrMethodCall(checker::ETSChecker *checker, ir::CallExpression const *node)
{
    auto const *callee = node->Callee();
    if (callee->TsType() != nullptr && callee->TsType()->IsETSExtensionFuncHelperType()) {
        return true;
    }

    // NOTE: Skip if invoke pattern Union.<method>()
    // Not skip if invoke pattern Union.<field>() where field is of ETSArrowType
    if (callee->IsMemberExpression()) {
        auto me = callee->AsMemberExpression();
        ES2PANDA_ASSERT(me->TsType() != nullptr);
        if (me->Object()->TsType() != nullptr && checker->GetApparentType(me->Object()->TsType())->IsETSUnionType() &&
            me->TsType()->IsETSMethodType()) {
            return true;
        }
    }

    auto *const var = GetNodeOrPropertyVariable(callee);
    return var != nullptr && !IsVariableOriginalAccessor(var) && (var->Flags() & varbinder::VariableFlags::METHOD) != 0;
}

static bool IsTypeErrorCall(ir::CallExpression const *node)
{
    auto const *callee = node->Callee();
    ES2PANDA_ASSERT(callee->TsType() != nullptr);
    return callee->TsType()->IsTypeError();
}

static ir::AstNode *TransformTupleSpread(public_lib::Context *ctx, ir::CallExpression *call)
{
    auto *allocator = ctx->allocator;
    auto *checker = ctx->GetChecker()->AsETSChecker();
    ArenaVector<ir::Expression *> newArgs(allocator->Adapter());
    bool modified = false;

    for (auto *arg : call->Arguments()) {
        if (!arg->IsSpreadElement() || !arg->TsType()->IsETSTupleType()) {
            newArgs.push_back(arg);
            continue;
        }
        modified = true;

        std::stringstream ss;
        auto *genSymIdent = Gensym(allocator);
        ss << "let @@I1: @@T2 = @@E3;";
        ss << "@@E4 as FixedArray<Any>";

        ArenaVector<ir::Expression *> tupleElements(allocator->Adapter());
        for (std::size_t idx = 0U; idx < arg->TsType()->AsETSTupleType()->GetTupleSize(); ++idx) {
            auto *ident = genSymIdent->Clone(allocator, nullptr);
            auto *number = allocator->New<ir::NumberLiteral>(lexer::Number(static_cast<uint64_t>(idx)));
            auto *indexed = util::NodeAllocator::ForceSetParent<ir::MemberExpression>(
                allocator, ident, number, ir::MemberExpressionKind::ELEMENT_ACCESS, true, false);
            tupleElements.push_back(indexed);
        }
        auto arrayExpr =
            util::NodeAllocator::ForceSetParent<ir::ArrayExpression>(allocator, std::move(tupleElements), allocator);

        auto typeNode = util::NodeAllocator::ForceSetParent<ir::OpaqueTypeNode>(allocator, arg->TsType(), allocator);

        auto *blockExpression = ctx->parser->AsETSParser()->CreateFormattedExpression(
            ss.str(), genSymIdent, typeNode, arg->AsSpreadElement()->Argument(), arrayExpr);

        auto *spreadElement = util::NodeAllocator::ForceSetParent<ir::SpreadElement>(
            allocator, ir::AstNodeType::SPREAD_ELEMENT, allocator, blockExpression);
        newArgs.push_back(spreadElement);
        spreadElement->SetParent(call);

        CheckLoweredNode(checker->VarBinder()->AsETSBinder(), checker, spreadElement);
    }
    if (modified) {
        call->Arguments() = std::move(newArgs);
    }
    return call;
}

// CC-OFFNXT(G.FUN.01, huge_method, huge_method[C++]) solid logic
static ir::AstNode *InsertInvokeCall(public_lib::Context *ctx, ir::CallExpression *call)
{
    auto *allocator = ctx->allocator;
    auto *checker = ctx->GetChecker()->AsETSChecker();

    auto *oldCallee = call->Callee();
    auto *oldType = checker->GetApparentType(oldCallee->TsType());
    ES2PANDA_ASSERT(oldType != nullptr);
    const size_t arity = call->Arguments().size();
    auto *ifaceType = oldType->AsETSFunctionType()->ArrowToFunctionalInterfaceDesiredArity(checker, arity);
    ES2PANDA_ASSERT(ifaceType != nullptr);

    /* Pull out substituted call signature */
    checker::Signature *callSig = ifaceType->GetFunctionalInterfaceInvokeType()->CallSignatures()[0];
    ES2PANDA_ASSERT(callSig != nullptr);
    util::StringView invokeMethodName = callSig->Function()->Id()->Name();

    auto *prop =
        ifaceType->GetProperty(invokeMethodName, checker::PropertySearchFlags::SEARCH_INSTANCE_METHOD |
                                                     checker::PropertySearchFlags::DISALLOW_SYNTHETIC_METHOD_CREATION |
                                                     checker::PropertySearchFlags::SEARCH_IN_BASE |
                                                     checker::PropertySearchFlags::SEARCH_IN_INTERFACES);
    ES2PANDA_ASSERT(prop != nullptr);
    auto *invoke0Id = allocator->New<ir::Identifier>(invokeMethodName, allocator);
    ES2PANDA_ASSERT(invoke0Id != nullptr);
    invoke0Id->SetTsType(prop->TsType());
    invoke0Id->SetVariable(prop);

    auto *newCallee = util::NodeAllocator::ForceSetParent<ir::MemberExpression>(
        allocator, oldCallee, invoke0Id, ir::MemberExpressionKind::PROPERTY_ACCESS, false, false);
    ES2PANDA_ASSERT(newCallee != nullptr);
    newCallee->SetTsType(prop->TsType());
    newCallee->SetObjectType(ifaceType);

    call->SetCallee(newCallee);
    call->SetSignature(callSig);

    return TransformTupleSpread(ctx, call);
}

static bool IsRedirectingConstructorCall(ir::CallExpression *expr)
{
    return expr->Callee()->IsThisExpression() || expr->Callee()->IsSuperExpression();
}

static bool IsInCalleePosition(const ir::Expression *expr)
{
    return expr->Parent()->IsCallExpression() && expr->Parent()->AsCallExpression()->Callee() == expr;
}

static bool IsEnumFunctionCall(const ir::Identifier *const id)
{
    if (id->Parent() != nullptr && id->Parent()->IsMemberExpression()) {
        const auto *const expr = id->Parent()->AsMemberExpression();
        if (expr->Object()->TsType()->IsETSEnumType()) {
            return true;
        }
    }

    return false;
}

[[nodiscard]] static bool IsValidFunctionDeclVar(const varbinder::Variable *const var) noexcept
{
    // If a function is accessor or defined in object literal, then no need to build lambda class.
    return var != nullptr && var->Declaration() != nullptr && var->Declaration()->IsFunctionDecl() &&
           var->Declaration()->Node()->IsMethodDefinition() &&
           !var->TsType()->HasTypeFlag(checker::TypeFlag::GETTER_SETTER);
}

static bool IsOverloadedName(const ir::Expression *const expr)
{
    if ((!expr->IsIdentifier() && !expr->IsMemberExpression()) || !expr->Parent()->IsOverloadDeclaration()) {
        return false;
    }

    auto overloadedList = expr->Parent()->AsOverloadDeclaration()->OverloadedList();
    return std::any_of(overloadedList.begin(), overloadedList.end(),
                       [&expr](const ir::Expression *overloadedName) { return overloadedName == expr; });
}

[[nodiscard]] static bool IsMethodInLiteral(ir::ArrowFunctionExpression const *const expr) noexcept
{
    return expr->Parent()->IsProperty() && expr->Parent()->AsProperty()->TsType()->IsETSMethodType();
}

static bool IsConvertibleFunctionReference(const ir::AstNode *node)
{
    if (node->IsIdentifier()) {
        auto *id = node->AsIdentifier();
        auto *var = id->Variable();
        // We are running this lowering only for ETS files
        // so it is correct to pass ETS extension here to isReference()
        return (id->IsReference(ScriptExtension::ETS) && id->TsType() != nullptr && id->TsType()->IsETSFunctionType() &&
                !IsInCalleePosition(id) && !IsEnumFunctionCall(id) && IsValidFunctionDeclVar(var) &&
                !IsOverloadedName(id));
    }

    if (node->IsMemberExpression()) {
        auto *mexpr = node->AsMemberExpression();
        if (mexpr->Kind() == ir::MemberExpressionKind::PROPERTY_ACCESS && mexpr->TsType() != nullptr &&
            mexpr->TsType()->IsETSFunctionType() && mexpr->Object()->TsType()->IsETSObjectType() &&
            mexpr->PropVar() != nullptr && !mexpr->PropVar()->HasFlag(varbinder::VariableFlags::DYNAMIC)) {
            ES2PANDA_ASSERT(mexpr->Property()->IsIdentifier());
            return IsValidFunctionDeclVar(mexpr->PropVar()) && !IsInCalleePosition(mexpr) && !IsOverloadedName(mexpr);
        }
    }

    return false;
}

static ir::AstNode *BuildLambdaClassWhenNeeded(public_lib::Context *ctx, ir::AstNode *node)
{
    if (node->IsArrowFunctionExpression() && !IsMethodInLiteral(node->AsArrowFunctionExpression())) {
        return ConvertLambda(ctx, node->AsArrowFunctionExpression());
    }

    if (node->IsETSGenericInstantiatedNode()) {
        // Because of postorder iteration, the explicitly instantiated function reference was handled, we can replace
        // the instantiation node with the already instantiated one
        auto *nodeParent = node->Parent();
        node = node->AsETSGenericInstantiatedNode()->GetExpression();
        node->SetParent(nodeParent);
    }

    if (IsConvertibleFunctionReference(node)) {
        ES2PANDA_ASSERT(node->IsExpression());
        return ConvertFunctionReference(ctx, node->AsExpression());
    }

    return node;
}

static void SetLoweredType(ir::AstNode *node, checker::Type *loweredType)
{
    if (node == nullptr) {
        return;
    }

    if (node->IsTyped()) {
        node->AsTyped()->SetTsType(loweredType);
    }

    if (node->Variable() != nullptr) {
        node->Variable()->SetTsType(loweredType);
    }

    if (node->IsMemberExpression()) {
        auto *const memberExpr = node->AsMemberExpression();
        if (memberExpr->HasMemberKind(ir::MemberExpressionKind::PROPERTY_ACCESS)) {
            SetLoweredType(memberExpr->Property(), loweredType);
        }
        return;
    }

    if (node->IsSpreadElement()) {
        SetLoweredType(node->AsSpreadElement()->Argument(), loweredType);
        return;
    }

    if (node->IsClassProperty()) {
        SetLoweredType(node->AsClassProperty()->Key(), loweredType);
    }
}

static bool IsLowerableBindingOwner(ir::AstNode *node)
{
    return GetNodeOrPropertyVariable(node) != nullptr;
}

static bool CanSetLoweredTypeOnAnnotationParent(ir::AstNode *node)
{
    return IsLowerableBindingOwner(node) || node->IsSpreadElement() || node->IsClassProperty();
}

static void SetLoweredTypeAnnotationOwnerType(ir::AstNode *typeAnnotation, checker::Type *loweredType)
{
    auto *const parent = typeAnnotation->Parent();
    if (parent == nullptr || !CanSetLoweredTypeOnAnnotationParent(parent)) {
        return;
    }

    SetLoweredType(parent, loweredType);
}

static ir::AstNode *LowerTypeNodeIfNeeded(public_lib::Context *ctx, ir::AstNode *node)
{
    if (!node->IsExpression() || !node->AsExpression()->IsTypeNode()) {
        return node;
    }

    auto type = node->AsExpression()->AsTypeNode()->TsType();
    if (type == nullptr || !type->IsETSArrowType()) {
        return node;
    }

    auto allocator = ctx->allocator;
    auto checker = ctx->GetChecker()->AsETSChecker();
    auto *functionalInterface = type->AsETSFunctionType()->ArrowToFunctionalInterface(checker);

    SetLoweredTypeAnnotationOwnerType(node, functionalInterface);
    auto newTypeNode = allocator->New<ir::OpaqueTypeNode>(functionalInterface, allocator);
    newTypeNode->SetParent(node->Parent());
    return newTypeNode;
}

bool LambdaConversionPhase::PerformForProgram(parser::Program *program)
{
    auto *varBinder = Context()->GetChecker()->VarBinder()->AsETSBinder();
    varbinder::RecordTableContext bctx {varBinder, program == Context()->parserProgram ? nullptr : program};
    parser::SavedFormattingFileName savedFormattingName(Context()->parser->AsETSParser(), "lambda-conversion");

    // For reproducibility of results when several compilation sessions are executed during
    // the same process's lifetime.
    if (program == Context()->parserProgram &&
        (Context()->config->options->GetCompilationMode() < CompilationMode::SIMULTANEOUS)) {
        ResetCalleeCount();
    }

    program->Ast()->TransformChildrenRecursivelyPostorder(
        [ctx = Context()](ir::AstNode *node) { return BuildLambdaClassWhenNeeded(ctx, node); }, Name());

    program->Ast()->TransformChildrenRecursivelyPreorder(
        [ctx = Context()](ir::AstNode *node) { return LowerTypeNodeIfNeeded(ctx, node); }, Name());

    auto insertInvokeIfNeeded = [ctx = Context()](ir::AstNode *node) {
        if (node->IsCallExpression() &&
            !IsFunctionOrMethodCall(ctx->GetChecker()->AsETSChecker(), node->AsCallExpression()) &&
            !IsRedirectingConstructorCall(node->AsCallExpression()) && !IsTypeErrorCall(node->AsCallExpression())) {
            return InsertInvokeCall(ctx, node->AsCallExpression());
        }
        return node;
    };

    // at this moment, the AST in subexpressions is not consistent, so the preorder is chosen
    program->Ast()->TransformChildrenRecursivelyPreorder(insertInvokeIfNeeded, Name());

    return true;
}

bool LambdaConversionPhase::PostconditionForProgram(parser::Program const *program)
{
    return !program->Ast()->IsAnyChild([](ir::AstNode const *node) {
        return node->IsArrowFunctionExpression() && !IsMethodInLiteral(node->AsArrowFunctionExpression());
    });
}

}  // namespace ark::es2panda::compiler