/**
 * Copyright (c) 2025-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 "restTupleLowering.h"
#include "checker/ETSchecker.h"
#include "ir/base/scriptFunction.h"
#include "compiler/lowering/util.h"
#include "checker/types/type.h"
#include "varbinder/ETSBinder.h"
#include "varbinder/variable.h"
#include "compiler/lowering/scopesInit/scopesInitPhase.h"

#include <vector>

namespace ark::es2panda::compiler {

static bool MethodDefinitionHasRestTuple(const ir::AstNode *def)
{
    auto pred = [](const auto *param) {
        return param->IsETSParameterExpression() && param->AsETSParameterExpression()->IsRestParameter() &&
               param->AsETSParameterExpression()->TypeAnnotation() &&
               param->AsETSParameterExpression()->TypeAnnotation()->IsETSTuple();
    };

    bool isScriptFunction = def->IsMethodDefinition() && (def->AsMethodDefinition()->Value() != nullptr) &&
                            def->AsMethodDefinition()->Value()->AsFunctionExpression()->Function()->IsScriptFunction();
    if (isScriptFunction) {
        auto params =
            def->AsMethodDefinition()->Value()->AsFunctionExpression()->Function()->AsScriptFunction()->Params();
        return std::any_of(params.begin(), params.end(), pred);
    }
    return false;
}

static bool IsClassDefinitionWithTupleRest(ir::AstNode *node)
{
    bool isClassDefinition = node->IsClassDefinition();
    if (isClassDefinition) {
        auto definitions = node->AsClassDefinition()->Body();
        return std::any_of(definitions.begin(), definitions.end(),
                           [](const auto *def) { return MethodDefinitionHasRestTuple(def); });
    }

    return false;
}

static ir::Expression *CreateMemberOrThisExpression(public_lib::Context *ctx, ir::Expression *funcExpr,
                                                    ir::AstNode *definition)
{
    auto *allocator = ctx->allocator;

    if (definition->IsConstructor()) {
        return ctx->AllocNode<ir::ThisExpression>();
    }

    auto scriptFunc = funcExpr->AsFunctionExpression()->Function()->AsScriptFunction();

    ir::Expression *ident = nullptr;

    if (definition->AsMethodDefinition()->IsStatic()) {
        auto *parentClass = util::Helpers::FindAncestorGivenByType(definition, ir::AstNodeType::CLASS_DEFINITION);
        ES2PANDA_ASSERT(parentClass != nullptr);
        ident = parentClass->AsClassDefinition()->Ident()->AsIdentifier()->Clone(allocator, parentClass->Parent());
    } else {
        ident = ctx->AllocNode<ir::ThisExpression>();
    }

    auto *newPropertyId = scriptFunc->Id()->Clone(allocator, nullptr);
    auto *memberExpr = ctx->AllocNode<ir::MemberExpression>(ident, newPropertyId,
                                                            ir::MemberExpressionKind::PROPERTY_ACCESS, false, false);

    return memberExpr;
}

static ir::TSTypeParameterInstantiation *CreateTypeParameterInstantiation(
    public_lib::Context *ctx, ir::TSTypeParameterDeclaration *paramDeclaration)
{
    auto const allocator = ctx->allocator;

    if (paramDeclaration == nullptr || paramDeclaration->Params().empty()) {
        return nullptr;
    }
    ArenaVector<ir::TypeNode *> selfParams(allocator->Adapter());
    ir::ETSTypeReferencePart *referencePart = nullptr;

    for (const auto &param : paramDeclaration->Params()) {
        auto *identRef = util::NodeAllocator::ForceSetParent<ir::Identifier>(
            allocator, param->AsTSTypeParameter()->Name()->Name(), allocator);

        referencePart = util::NodeAllocator::ForceSetParent<ir::ETSTypeReferencePart>(allocator, identRef, nullptr,
                                                                                      nullptr, allocator);

        auto *typeReference =
            util::NodeAllocator::ForceSetParent<ir::ETSTypeReference>(allocator, referencePart, allocator);

        selfParams.push_back(typeReference);
    }

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

static ir::CallExpression *CreateNewCallExpression(public_lib::Context *ctx, ir::Expression *funcExpr,
                                                   ir::AstNode *definition, ir::TSAsExpression *asExpression)
{
    auto *allocator = ctx->allocator;

    ArenaVector<ir::Expression *> callArguments({}, allocator->Adapter());
    for (auto arg : funcExpr->AsFunctionExpression()->Function()->AsScriptFunction()->Params()) {
        if (!arg->AsETSParameterExpression()->IsRestParameter()) {
            auto *id = arg->AsETSParameterExpression()->Ident()->Clone(allocator, nullptr);
            id->SetTsTypeAnnotation(nullptr);
            callArguments.push_back(id);
        } else {
            auto spreadElement =
                ctx->AllocNode<ir::SpreadElement>(ir::AstNodeType::SPREAD_ELEMENT, allocator, asExpression);
            callArguments.push_back(spreadElement);
        }
    }

    // Create member expression if method is not a constructor
    ir::Expression *memberExpr = CreateMemberOrThisExpression(ctx, funcExpr, definition);

    ir::TSTypeParameterInstantiation *typeParamInst = nullptr;
    if (funcExpr->AsFunctionExpression()->Function()->AsScriptFunction()->TypeParams() != nullptr) {
        auto typeParams = funcExpr->AsFunctionExpression()
                              ->Function()
                              ->AsScriptFunction()
                              ->TypeParams()
                              ->AsTSTypeParameterDeclaration();
        typeParamInst = CreateTypeParameterInstantiation(ctx, typeParams);
    }

    auto *newCallExpr = ctx->AllocNode<ir::CallExpression>(memberExpr, std::move(callArguments), typeParamInst, false);

    for (auto *arg : newCallExpr->Arguments()) {
        arg->SetParent(newCallExpr);
    }

    return newCallExpr;
}

static ArenaVector<ir::Expression *> CreateFunctionRestParams(public_lib::Context *ctx, ir::AstNode *funcExpr)
{
    auto *allocator = ctx->allocator;

    ArenaVector<ir::Expression *> params {allocator->Adapter()};
    for (auto param : funcExpr->AsFunctionExpression()->Function()->AsScriptFunction()->Params()) {
        if (param->AsETSParameterExpression()->IsRestParameter()) {
            for (auto tupleTypeAnno :
                 param->AsETSParameterExpression()->TypeAnnotation()->AsETSTuple()->GetTupleTypeAnnotationsList()) {
                ir::Identifier *id = Gensym(allocator);
                auto *newParam = ctx->AllocNode<ir::ETSParameterExpression>(id, false, allocator);
                auto newAnnotation = tupleTypeAnno->Clone(allocator, id);
                id->SetTsTypeAnnotation(newAnnotation);
                params.push_back(newParam);
            }
        }
    }
    return params;
}

static ArenaVector<ir::Expression *> CreateFunctionNormalParams(public_lib::Context *ctx, ir::AstNode *funcExpr)
{
    auto *allocator = ctx->allocator;

    ArenaVector<ir::Expression *> params {allocator->Adapter()};
    for (auto param : funcExpr->AsFunctionExpression()->Function()->AsScriptFunction()->Params()) {
        if (!param->AsETSParameterExpression()->IsRestParameter()) {
            auto newParam = param->AsETSParameterExpression()->Clone(allocator, nullptr);
            params.push_back(newParam);
        }
    }
    return params;
}

static ArenaVector<ir::Expression *> MergeParams(public_lib::Context *ctx,
                                                 const ArenaVector<ir::Expression *> &newNormalParams,
                                                 const ArenaVector<ir::Expression *> &newRestParams)
{
    auto *allocator = ctx->allocator;

    ArenaVector<ir::Expression *> params {allocator->Adapter()};
    for (auto newNormalParam : newNormalParams) {
        params.push_back(newNormalParam);
    }
    for (auto newRestParam : newRestParams) {
        params.push_back(newRestParam);
    }
    return params;
}

static ir::ArrayExpression *CreateArrayExpression(public_lib::Context *ctx,
                                                  const ArenaVector<ir::Expression *> &newRestParams)
{
    auto *allocator = ctx->allocator;

    ArenaVector<ir::Expression *> elementsInit(ctx->Allocator()->Adapter());
    ArenaVector<ir::Expression *> elements(ctx->Allocator()->Adapter());

    auto *arrayExpr = ctx->AllocNode<ir::ArrayExpression>(std::move(elementsInit), ctx->Allocator());
    ES2PANDA_ASSERT(arrayExpr != nullptr);
    for (auto tupleElementAnno : newRestParams) {
        auto &tupleElementName = tupleElementAnno->AsETSParameterExpression()->Ident()->AsIdentifier()->Name();
        ir::Expression *arg = ctx->AllocNode<ir::Identifier>(tupleElementName, allocator);
        ES2PANDA_ASSERT(arg != nullptr);
        arg->SetParent(arrayExpr);
        elements.push_back(arg);
    }
    arrayExpr->SetElements(std::move(elements));
    return arrayExpr;
}

static ir::TSTypeParameterDeclaration *CreateNewParameterDeclaration(public_lib::Context *ctx,
                                                                     ir::TSTypeParameterDeclaration *paramDeclaration)
{
    auto const allocator = ctx->allocator;
    if (paramDeclaration == nullptr || paramDeclaration->Params().empty()) {
        return nullptr;
    }

    ArenaVector<ir::TSTypeParameter *> typeParams(allocator->Adapter());

    auto parentParams = paramDeclaration->Params();
    std::for_each(parentParams.begin(), parentParams.end(), [&typeParams, allocator](ir::TSTypeParameter *par) {
        ir::Identifier *ident = par->Name()->Clone(allocator, nullptr)->AsIdentifier();
        auto *constraint =
            par->Constraint() != nullptr ? par->Constraint()->Clone(allocator, nullptr)->AsTypeNode() : nullptr;
        auto *defaultType =
            par->DefaultType() != nullptr ? par->DefaultType()->Clone(allocator, nullptr)->AsTypeNode() : nullptr;
        auto *typeParam = util::NodeAllocator::ForceSetParent<ir::TSTypeParameter>(allocator, ident, constraint,
                                                                                   defaultType, allocator);
        typeParams.push_back(typeParam);
    });
    size_t paramsSize = typeParams.size();
    return util::NodeAllocator::ForceSetParent<ir::TSTypeParameterDeclaration>(allocator, std::move(typeParams),
                                                                               paramsSize);
}

static ir::ScriptFunction *CreateNewScriptFunction(public_lib::Context *ctx, ir::ScriptFunction *scriptFunc,
                                                   ArenaVector<ir::Expression *> newParams)
{
    auto *allocator = ctx->allocator;

    ArenaVector<ir::Statement *> statements(allocator->Adapter());
    auto *body = ctx->AllocNode<ir::BlockStatement>(allocator, std::move(statements));
    ir::TypeNode *newReturnTypeAnno = nullptr;
    if (scriptFunc->ReturnTypeAnnotation() != nullptr) {
        newReturnTypeAnno = scriptFunc->ReturnTypeAnnotation()->Clone(allocator, nullptr);
    }
    auto *newParamDeclaration = CreateNewParameterDeclaration(ctx, scriptFunc->TypeParams());

    auto *newScriptFunc = ctx->AllocNode<ir::ScriptFunction>(
        allocator, ir::ScriptFunction::ScriptFunctionData {
                       body, ir::FunctionSignature(newParamDeclaration, std::move(newParams), newReturnTypeAnno),
                       scriptFunc->Flags() | ir::ScriptFunctionFlags::SYNTHETIC});
    ES2PANDA_ASSERT(newScriptFunc != nullptr);
    newScriptFunc->AddModifier(scriptFunc->AsScriptFunction()->Modifiers());

    if (scriptFunc->HasAnnotations()) {
        newScriptFunc->SetAnnotations(scriptFunc->Annotations());
    }

    ir::Identifier *newScriptFuncId = scriptFunc->Id()->Clone(allocator, newScriptFunc);
    newScriptFunc->SetIdent(newScriptFuncId);

    return newScriptFunc;
}

static ArenaVector<ir::Statement *> CreateReturnOrExpressionStatement(public_lib::Context *ctx,
                                                                      ir::ScriptFunction *scriptFunc,
                                                                      ir::CallExpression *callExpr)
{
    ArenaVector<ir::Statement *> statements(ctx->Allocator()->Adapter());

    if (scriptFunc->ReturnTypeAnnotation() != nullptr) {
        auto returnStatement = ctx->AllocNode<ir::ReturnStatement>(callExpr);
        statements.insert(statements.end(), returnStatement);
    } else {
        auto expressionStatement = ctx->AllocNode<ir::ExpressionStatement>(callExpr);
        statements.insert(statements.end(), expressionStatement);
        callExpr->SetParent(expressionStatement);
    }
    return statements;
}

static ir::MethodDefinition *CreateNewMethodDefinition(public_lib::Context *ctx, ir::MethodDefinition *definition,
                                                       ir::FunctionExpression *function)
{
    auto *allocator = ctx->allocator;

    auto *methodKey = definition->AsMethodDefinition()->Key()->AsIdentifier()->Clone(allocator, nullptr);
    auto *const methodDef =
        ctx->AllocNode<ir::MethodDefinition>(definition->AsMethodDefinition()->Kind(), methodKey, function,
                                             definition->AsMethodDefinition()->Modifiers(), allocator, false);
    ES2PANDA_ASSERT(methodDef != nullptr);
    methodDef->SetParent(definition->Parent());

    return methodDef;
}

static void CreateNewMethod(public_lib::Context *ctx, ir::AstNode *node)
{
    auto *allocator = ctx->allocator;

    for (auto definition : node->AsClassDefinition()->Body()) {
        if (definition->IsMethodDefinition() && MethodDefinitionHasRestTuple(definition->AsMethodDefinition())) {
            auto funcExpr = definition->AsMethodDefinition()->Value();
            ir::ETSParameterExpression *restParam = funcExpr->AsFunctionExpression()
                                                        ->Function()
                                                        ->AsScriptFunction()
                                                        ->Params()
                                                        .back()
                                                        ->AsETSParameterExpression();
            ir::ScriptFunction *scriptFunc = funcExpr->AsFunctionExpression()->Function()->AsScriptFunction();

            ArenaVector<ir::Expression *> newNormalParams = CreateFunctionNormalParams(ctx, funcExpr);
            ArenaVector<ir::Expression *> newRestParams = CreateFunctionRestParams(ctx, funcExpr);
            ArenaVector<ir::Expression *> mergedParams = MergeParams(ctx, newNormalParams, newRestParams);

            ir::ScriptFunction *newScriptFunc = CreateNewScriptFunction(ctx, scriptFunc, mergedParams);

            ir::ArrayExpression *const newArrayExpr = CreateArrayExpression(ctx, newRestParams);

            ir::TypeNode *newTypeAnnotation = restParam->TypeAnnotation()->Clone(allocator, nullptr);

            auto *asExpression = ctx->AllocNode<ir::TSAsExpression>(newArrayExpr, newTypeAnnotation, false);

            auto *callExpr = CreateNewCallExpression(ctx, funcExpr, definition, asExpression);

            ArenaVector<ir::Statement *> statements = CreateReturnOrExpressionStatement(ctx, scriptFunc, callExpr);

            // Build new script function
            newScriptFunc->AsScriptFunction()->Body()->AsBlockStatement()->SetStatements(std::move(statements));

            // Build new functionExpression
            auto *function = ctx->AllocNode<ir::FunctionExpression>(newScriptFunc);
            function->SetParent(funcExpr->Parent());

            // Build new methodDefinition
            auto *const methodDef = CreateNewMethodDefinition(ctx, definition->AsMethodDefinition(), function);
            RefineSourceRanges(methodDef);

            node->AsClassDefinition()->EmplaceBody(methodDef);
        }
    }
}

bool RestTupleConstructionPhase::PerformForProgram(parser::Program *program)
{
    auto *const root = program->Ast();
    if (root == nullptr) {
        return true;
    }

    std::vector<ir::AstNode *> stack {};
    stack.emplace_back(root);

    while (!stack.empty()) {
        auto *const node = stack.back();
        stack.pop_back();

        if (IsClassDefinitionWithTupleRest(node)) {
            CreateNewMethod(Context(), node);
        }

        node->Iterate([&stack](ir::AstNode *child) {
            if (child != nullptr) {
                stack.emplace_back(child);
            }
        });
    }

    return true;
}

}  // namespace ark::es2panda::compiler