/*
 * Copyright (c) 2022-2025 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.
 */
import * as arkts from '@koalaui/libarkts';

import { factory } from './memo-factory';
import { AbstractVisitor, VisitorOptions } from '../common/abstract-visitor';
import {
    buildReturnTypeInfo,
    castParameters,
    findReturnTypeFromTypeAnnotation,
    isMemoETSParameterExpression,
    isMemoParametersDeclaration,
    isUnmemoizedInFunctionParams,
    MemoInfo,
    ParamInfo,
    PositionalIdTracker,
    ReturnTypeInfo,
    RuntimeNames,
} from './utils';
import { ReturnTransformer } from './return-transformer';

export interface ParameterTransformerOptions extends VisitorOptions {
    positionalIdTracker: PositionalIdTracker;
}

interface RewriteMemoInfo extends MemoInfo {
    rewritePeer: arkts.KNativePointer;
}

export class ParameterTransformer extends AbstractVisitor {
    private rewriteIdentifiers?: Map<arkts.KNativePointer, () => arkts.MemberExpression | arkts.Identifier>;
    private rewriteCalls?: Map<arkts.KNativePointer, (passArgs: arkts.Expression[]) => arkts.CallExpression>;
    private rewriteMemoInfos?: Map<arkts.KNativePointer, RewriteMemoInfo>;
    private rewriteThis?: boolean;
    private skipNode?: arkts.VariableDeclaration;
    private visited: Set<arkts.KNativePointer>;

    private positionalIdTracker: PositionalIdTracker;

    constructor(options: ParameterTransformerOptions) {
        super(options);
        this.positionalIdTracker = options.positionalIdTracker;
        this.visited = new Set();
    }

    reset(): void {
        super.reset();
        this.rewriteIdentifiers = undefined;
        this.rewriteCalls = undefined;
        this.rewriteMemoInfos = undefined;
        this.skipNode = undefined;
        this.visited.clear();
    }

    withThis(flag: boolean): ParameterTransformer {
        this.rewriteThis = flag;
        return this;
    }

    withParameters(parameters: ParamInfo[]): ParameterTransformer {
        this.rewriteCalls = new Map(
            parameters
                .filter(
                    (it) =>
                        it.param.ident?.typeAnnotation && (arkts.isETSFunctionType(it.param.ident.typeAnnotation) || arkts.isETSUnionType(it.param.ident.typeAnnotation))
                )
                .map((it) => {
                    return [
                        it.param.ident?.name.startsWith(RuntimeNames.GENSYM)
                            ? it.ident.originalPeer
                            : it.param.originalPeer,
                        (passArgs: arkts.Expression[]): arkts.CallExpression => {
                            return factory.createMemoParameterAccessCall(it.ident.name, passArgs);
                        },
                    ];
                })
        );
        this.rewriteIdentifiers = new Map(
            parameters.map((it) => {
                return [
                    it.param.ident?.name.startsWith(RuntimeNames.GENSYM)
                        ? it.ident.originalPeer
                        : it.param.originalPeer,
                    (): arkts.MemberExpression => {
                        return factory.createMemoParameterAccess(it.ident.name);
                    },
                ];
            })
        );
        this.rewriteMemoInfos = new Map(
            parameters.map((it) => {
                return [
                    it.param.ident!.name.startsWith(RuntimeNames.GENSYM)
                        ? it.ident.originalPeer
                        : it.param.originalPeer,
                    {
                        name: it.param.ident!.name,
                        rewritePeer: it.param.ident!.originalPeer,
                        isMemo: isMemoETSParameterExpression(it.param),
                    },
                ];
            })
        );
        return this;
    }

    skip(memoParametersDeclaration?: arkts.VariableDeclaration): ParameterTransformer {
        this.skipNode = memoParametersDeclaration;
        return this;
    }

    track(node: arkts.AstNode | undefined): void {
        if (!!node?.peer) {
            this.visited.add(node.peer);
        }
    }

    isTracked(node: arkts.AstNode | undefined): boolean {
        return !!node?.peer && this.visited.has(node.peer);
    }

    private updateArrowFunctionFromVariableDeclareInit(
        initializer: arkts.ArrowFunctionExpression,
        returnType: arkts.TypeNode | undefined
    ): arkts.ArrowFunctionExpression {
        const scriptFunction = initializer.function!;
        if (!scriptFunction.body || !arkts.isBlockStatement(scriptFunction.body)) {
            return initializer;
        }
        if (isUnmemoizedInFunctionParams(scriptFunction.params)) {
            return initializer;
        }
        const returnTypeInfo: ReturnTypeInfo = buildReturnTypeInfo(
            returnType ?? scriptFunction.returnTypeAnnotation,
            true
        );
        const [body, parameterIdentifiers, memoParametersDeclaration, syntheticReturnStatement] =
            factory.updateFunctionBody(
                scriptFunction.body,
                castParameters(scriptFunction.params),
                returnTypeInfo,
                this.positionalIdTracker.id()
            );
        const paramaterTransformer = new ParameterTransformer({
            positionalIdTracker: this.positionalIdTracker,
        });
        const returnTransformer = new ReturnTransformer();
        const afterParameterTransformer = paramaterTransformer
            .withParameters(parameterIdentifiers)
            .skip(memoParametersDeclaration)
            .visitor(body);
        const afterReturnTransformer = returnTransformer
            .skip(syntheticReturnStatement)
            .registerReturnTypeInfo(returnTypeInfo)
            .visitor(afterParameterTransformer);
        const updateScriptFunction = factory.updateScriptFunctionWithMemoParameters(
            scriptFunction,
            afterReturnTransformer,
            returnTypeInfo.node
        );
        paramaterTransformer.reset();
        returnTransformer.reset();
        this.track(updateScriptFunction.body);
        return arkts.factory.updateArrowFunctionExpression(initializer, updateScriptFunction, initializer.annotations);
    }

    private updateVariableDeclareInit<T extends arkts.AstNode>(
        initializer: T | undefined,
        returnType: arkts.TypeNode | undefined
    ): T | undefined {
        if (!initializer) {
            return undefined;
        }
        if (arkts.isConditionalExpression(initializer)) {
            return arkts.factory.updateConditionalExpression(
                initializer,
                initializer.test,
                this.updateVariableDeclareInit(initializer.consequent, returnType),
                this.updateVariableDeclareInit(initializer.alternate, returnType)
            ) as unknown as T;
        }
        if (arkts.isTSAsExpression(initializer)) {
            return arkts.factory.updateTSAsExpression(
                initializer,
                this.updateVariableDeclareInit(initializer.expr, returnType),
                factory.updateMemoTypeAnnotation(initializer.typeAnnotation),
                initializer.isConst
            ) as unknown as T;
        }
        if (arkts.isArrowFunctionExpression(initializer)) {
            return this.updateArrowFunctionFromVariableDeclareInit(initializer, returnType) as unknown as T;
        }
        return initializer;
    }

    private updateParamReDeclare(node: arkts.VariableDeclarator, memoInfo: RewriteMemoInfo): arkts.VariableDeclarator {
        const shouldUpdate: boolean = (node.id as arkts.Identifier).name !== memoInfo.name && memoInfo.isMemo;
        if (!shouldUpdate) {
            return node;
        }
        const decl = arkts.getPeerIdentifierDecl(memoInfo.rewritePeer);
        if (!decl || !arkts.isETSParameterExpression(decl)) {
            return node;
        }

        let typeAnnotation: arkts.TypeNode | undefined;
        if (
            !!(node.id as arkts.Identifier).typeAnnotation &&
            !(typeAnnotation = factory.updateMemoTypeAnnotation((node.id as arkts.Identifier).typeAnnotation))
        ) {
            console.error(`ETSFunctionType or ETSUnionType expected for @memo-variable-type ${(node.id as arkts.Identifier).name}`);
            throw 'Invalid @memo usage';
        }

        const returnType = findReturnTypeFromTypeAnnotation(decl.typeAnnotation);
        return arkts.factory.updateVariableDeclarator(
            node,
            node.flag,
            arkts.factory.updateIdentifier(node.id as arkts.Identifier, (node.id as arkts.Identifier).name, typeAnnotation),
            this.updateVariableDeclareInit(node.init, returnType)
        );
    }

    private updateVariableReDeclarationFromParam(node: arkts.VariableDeclaration): arkts.VariableDeclaration {
        return arkts.factory.updateVariableDeclaration(
            node,
            node.kind,
            node.declarators.map((declarator) => {
                if (this.rewriteMemoInfos?.has(declarator.id!.originalPeer)) {
                    const memoInfo = this.rewriteMemoInfos.get(declarator.id!.originalPeer)!;
                    return this.updateParamReDeclare(declarator, memoInfo);
                }
                if (!!declarator.init && arkts.isIdentifier(declarator.init)) {
                    const decl = arkts.getPeerIdentifierDecl(declarator.init.originalPeer);
                    if (decl && this.rewriteIdentifiers?.has(decl.peer)) {
                        return arkts.factory.updateVariableDeclarator(
                            declarator,
                            declarator.flag,
                            declarator.id,
                            this.rewriteIdentifiers.get(decl.peer)!()
                        );
                    }
                }
                return declarator;
            })
        );
    }

    private updateCallReDeclare(
        node: arkts.CallExpression,
        oriName: arkts.Identifier,
        memoInfo: RewriteMemoInfo
    ): arkts.CallExpression {
        const shouldUpdate: boolean = oriName.name !== memoInfo.name && memoInfo.isMemo;
        if (!shouldUpdate) {
            return node;
        }
        return factory.insertHiddenArgumentsToCall(node, this.positionalIdTracker.id(oriName.name));
    }

    visitor(beforeChildren: arkts.AstNode): arkts.AstNode {
        // TODO: temporary checking skip nodes by comparison with expected skip nodes
        // Should be fixed when update procedure implemented properly
        if (/* beforeChildren === this.skipNode */ isMemoParametersDeclaration(beforeChildren)) {
            return beforeChildren;
        }
        if (arkts.isVariableDeclaration(beforeChildren)) {
            return this.updateVariableReDeclarationFromParam(beforeChildren);
        }
        if (arkts.isCallExpression(beforeChildren) && arkts.isIdentifier(beforeChildren.callee)) {
            const decl = arkts.getPeerIdentifierDecl(beforeChildren.callee.originalPeer);
            if (decl && this.rewriteCalls?.has(decl.peer)) {
                const updateCall = this.rewriteCalls.get(decl.peer)!(
                    beforeChildren.arguments.map((it) => this.visitor(it) as arkts.Expression)
                );
                if (this.rewriteMemoInfos?.has(decl.peer)) {
                    const memoInfo = this.rewriteMemoInfos.get(decl.peer)!;
                    return this.updateCallReDeclare(updateCall, beforeChildren.callee, memoInfo);
                }
                return updateCall;
            }
        }
        const node = this.visitEachChild(beforeChildren);
        if (arkts.isIdentifier(node)) {
            const decl = arkts.getPeerIdentifierDecl(node.originalPeer);
            if (decl && this.rewriteIdentifiers?.has(decl.peer)) {
                return this.rewriteIdentifiers.get(decl.peer)!();
            }
        }
        if (arkts.isThisExpression(node) && this.rewriteThis) {
            return factory.createMemoParameterAccess(RuntimeNames.THIS);
        }
        return node;
    }
}