/*
 * 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 { BuiltInNames } from '../common/predefines';
import { isArrowFunctionAsValue } from '../collectors/memo-collectors/utils';
import { factory } from './memo-factory';
import {
    filterMemoSkipParams,
    findLocalReturnTypeFromTypeAnnotation,
    findUnmemoizedScopeInFunctionBody,
    fixGensymParams,
    getFunctionParamsBeforeUnmemoized,
    hasMemoAnnotation,
    isVoidType,
    mayAddLastReturn,
    parametrizedNodeHasReceiver,
    ParamInfo,
    PositionalIdTracker,
} from './utils';
import { InternalsTransformer } from './internal-transformer';
import { AstNodeCacheValueMetadata } from '../common/node-cache';
import { MetaDataCollector } from '../common/metadata-collector';

export interface CachedMetadata extends AstNodeCacheValueMetadata {
    internalsTransformer?: InternalsTransformer;
}

export class RewriteFactory {
    static rewriteTsAsExpression(node: arkts.TSAsExpression, metadata?: CachedMetadata): arkts.TSAsExpression {
        if (MetaDataCollector.getInstance().isDeclaration) {
            return node;
        }
        const newExpr = !!node.expr && arkts.isArrowFunctionExpression(node.expr)
            ? RewriteFactory.rewriteArrowFunction(node.expr)
            : node.expr;
        return arkts.factory.updateTSAsExpression(
            node,
            newExpr,
            RewriteFactory.rewriteType(node.typeAnnotation, metadata),
            node.isConst
        );
    }

    static rewriteUnionType(node: arkts.ETSUnionType, metadata?: CachedMetadata): arkts.ETSUnionType {
        return arkts.factory.updateETSUnionType(
            node,
            node.types.map((nodeType) => {
                if (arkts.isETSFunctionType(nodeType)) {
                    return RewriteFactory.rewriteFunctionType(nodeType, metadata);
                }
                if (arkts.isETSUnionType(nodeType)) {
                    return RewriteFactory.rewriteUnionType(nodeType, metadata);
                }
                if (arkts.isETSTypeReference(nodeType)) {
                    return RewriteFactory.rewriteETSTypeReference(nodeType, metadata);
                }
                return nodeType;
            })
        );
    }

    static rewriteFunctionType(node: arkts.ETSFunctionType, metadata?: CachedMetadata): arkts.ETSFunctionType {
        const canRewriteType = !metadata?.forbidTypeRewrite;
        const isWithinTypeParams = !!metadata?.isWithinTypeParams && hasMemoAnnotation(node);
        if (!canRewriteType && !isWithinTypeParams) {
            return node;
        }
        const hasReceiver = metadata?.hasReceiver ?? parametrizedNodeHasReceiver(node);
        return factory.updateFunctionTypeWithMemoParameters(node, hasReceiver);
    }

    static rewriteETSTypeReference(node: arkts.ETSTypeReference, metadata?: CachedMetadata): arkts.ETSTypeReference {
        if (!metadata?.isWithinTypeParams) {
            return node;
        }
        const part = node.part;
        if (!part) {
            return node;
        }
        const typeParams = part.typeParams;
        if (!typeParams) {
            return node;
        }
        const newTypeParams = arkts.factory.updateTSTypeParameterInstantiation(
            typeParams,
            typeParams.params.map((t) => RewriteFactory.rewriteType(t, metadata)!)
        );
        return arkts.factory.updateETSTypeReference(
            node,
            arkts.factory.updateETSTypeReferencePart(part, part.name, newTypeParams, part.previous)
        );
    }

    /**
     * @internal
     */
    static rewriteType(node: arkts.TypeNode | undefined, metadata?: CachedMetadata): arkts.TypeNode | undefined {
        let newNodeType = node;
        if (!!newNodeType && arkts.isETSFunctionType(newNodeType)) {
            newNodeType = RewriteFactory.rewriteFunctionType(newNodeType, metadata);
        } else if (!!newNodeType && arkts.isETSUnionType(newNodeType)) {
            newNodeType = RewriteFactory.rewriteUnionType(newNodeType, metadata);
        } else if (!!newNodeType && arkts.isETSTypeReference(newNodeType)) {
            return RewriteFactory.rewriteETSTypeReference(newNodeType, metadata);
        }
        return newNodeType;
    }

    static rewriteTypeAlias(
        node: arkts.TSTypeAliasDeclaration,
        metadata?: CachedMetadata
    ): arkts.TSTypeAliasDeclaration {
        if (!node.typeAnnotation) {
            return node;
        }
        const newNodeType = RewriteFactory.rewriteType(node.typeAnnotation)!;
        return arkts.factory.updateTSTypeAliasDeclaration(node, node.id, node.typeParams, newNodeType, node.annotations);
    }

    static rewriteParameter(
        node: arkts.ETSParameterExpression,
        metadata?: CachedMetadata
    ): arkts.ETSParameterExpression {
        if (!node.typeAnnotation && !node.initializer) {
            return node;
        }
        node.setTypeAnnotation(RewriteFactory.rewriteType(node.typeAnnotation, metadata));
        return node;
    }

    static rewriteProperty(node: arkts.Property, metadata?: CachedMetadata): arkts.Property {
        if (MetaDataCollector.getInstance().isDeclaration) {
            return node;
        }
        const value: arkts.Expression | undefined = node.value;
        if (!value) {
            return node;
        }
        if (arkts.isArrowFunctionExpression(value)) {
            node.setValue(RewriteFactory.rewriteArrowFunction(value, metadata));
        } else if (isArrowFunctionAsValue(value)) {
            node.setValue(RewriteFactory.rewriteTsAsExpression(value, metadata));
        }
        return node;
    }

    static rewriteClassProperty(node: arkts.ClassProperty, metadata?: CachedMetadata): arkts.ClassProperty {
        const newType = !!node.typeAnnotation ? RewriteFactory.rewriteType(node.typeAnnotation, metadata) : undefined;
        const newValue =
            !!node.value && arkts.isArrowFunctionExpression(node.value)
                ? RewriteFactory.rewriteArrowFunction(node.value, metadata)
                : node.value;
        return arkts.factory.updateClassProperty(node, node.key, newValue, newType, node.modifierFlags, node.isComputed, node.annotations);
    }

    static rewriteArrowFunction(
        node: arkts.ArrowFunctionExpression,
        metadata?: AstNodeCacheValueMetadata,
        expectReturn?: arkts.TypeNode
    ): arkts.ArrowFunctionExpression {
        return arkts.factory.updateArrowFunctionExpression(
            node,
            RewriteFactory.rewriteScriptFunction(node.function!, metadata, expectReturn),
            node.annotations
        );
    }

    /**
     * @internal
     */
    static rewriteScriptFunctionBody(
        node: arkts.ScriptFunction,
        body: arkts.BlockStatement,
        positionalIdTracker: PositionalIdTracker,
        callName?: string,
        hasReceiver?: boolean,
        expectReturn?: arkts.TypeNode
    ): arkts.BlockStatement {
        const _hasReceiver = hasReceiver ?? node.hasReceiver;
        const _callName = callName ?? node.id?.name;
        const parameters = getFunctionParamsBeforeUnmemoized(node.params, _hasReceiver);
        const declaredParams: ParamInfo[] = parameters.map((p) => {
            const param = p as arkts.ETSParameterExpression;
            return { ident: param.ident!, param };
        });
        const _gensymCount = fixGensymParams(declaredParams, body);
        if (findUnmemoizedScopeInFunctionBody(body, _gensymCount)) {
            return body;
        }
        const filteredParams = filterMemoSkipParams(declaredParams);
        const returnType =
            node.returnTypeAnnotation ??
            expectReturn ??
            arkts.factory.createETSPrimitiveType(arkts.Es2pandaPrimitiveType.PRIMITIVE_TYPE_VOID);
        const _isVoidReturn = isVoidType(returnType);
        const _returnType = _isVoidReturn ? arkts.factory.createETSUndefinedType() : returnType;
        const scopeDeclaration = factory.createScopeDeclaration(
            _returnType,
            positionalIdTracker.id(_callName),
            filteredParams.length
        );
        const memoParametersDeclaration = filteredParams.length
            ? factory.createMemoParameterDeclaration(filteredParams.map((p) => p.ident.name))
            : undefined;
        const syntheticReturnStatement = factory.createSyntheticReturnStatement(false);
        const unchangedCheck = factory.createIfStatementWithSyntheticReturnStatement(
            syntheticReturnStatement,
            _isVoidReturn
        );
        const lastReturn = mayAddLastReturn(body)
            ? factory.createWrappedReturnStatement(factory.createRecacheCall(), _isVoidReturn)
            : undefined;
        return arkts.factory.updateBlockStatement(body, [
            ...body.statements.slice(0, _gensymCount),
            scopeDeclaration,
            ...(!!memoParametersDeclaration ? [memoParametersDeclaration] : []),
            unchangedCheck,
            ...body.statements.slice(_gensymCount),
            ...(!!lastReturn ? [lastReturn] : []),
        ]);
    }

    static rewriteScriptFunction(
        node: arkts.ScriptFunction,
        metadata?: CachedMetadata,
        expectReturn?: arkts.TypeNode
    ): arkts.ScriptFunction {
        const _callName = metadata?.callName;
        const _hasReceiver = metadata?.hasReceiver ?? node.hasReceiver;
        const _isSetter = !!metadata?.isSetter;
        const _isGetter = !!metadata?.isGetter;
        const _hasMemoEntry = !!metadata?.hasMemoEntry;
        const _hasMemoIntrinsic = !!metadata?.hasMemoIntrinsic;
        const _internalsTransformer = metadata?.internalsTransformer;
        const _isWithinTypeParams = metadata?.isWithinTypeParams;
        const _isDecl = arkts.hasModifierFlag(node, arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_DECLARE);
        const newParams = prepareRewriteScriptFunctionParameters(
            node,
            _isSetter,
            _isGetter,
            _hasReceiver,
            _isWithinTypeParams
        );
        const newReturnType: arkts.TypeNode | undefined = prepareRewriteScriptFunctionReturnType(
            node,
            _isGetter,
            _hasReceiver,
            _isWithinTypeParams
        );
        const newBody: arkts.AstNode | undefined = prepareRewriteScriptFunctionBody(
            node,
            expectReturn,
            _internalsTransformer,
            _isDecl,
            _hasMemoEntry,
            _hasMemoIntrinsic,
            _callName,
            _hasReceiver,
            _isGetter,
            _isSetter
        );
        node.setParams(newParams);
        if (!!newReturnType) {
            node.setReturnTypeAnnotation(newReturnType);
        }
        if (!!newBody) {
            node.setBody(newBody);
        }
        return node;
    }

    static rewriteMethodDefinition(node: arkts.MethodDefinition, metadata?: CachedMetadata): arkts.MethodDefinition {
        const isSetter = node.kind === arkts.Es2pandaMethodDefinitionKind.METHOD_DEFINITION_KIND_SET;
        const isGetter = node.kind === arkts.Es2pandaMethodDefinitionKind.METHOD_DEFINITION_KIND_GET;
        const newNode = arkts.factory.updateMethodDefinition(
            node,
            node.kind,
            node.key,
            arkts.factory.createFunctionExpression(node.id?.clone(),
                RewriteFactory.rewriteScriptFunction(node.function!, {
                    callName: node.id?.name,
                    ...metadata,
                    isSetter,
                    isGetter,
                })
            ),
            node.modifierFlags,
            false,
            node.overloads
        );
        if (node.overloads.length > 0) {
            newNode.setOverloads(node.overloads.map((o) => RewriteFactory.rewriteMethodDefinition(o, metadata)));
        }
        return newNode;
    }

    static rewriteCallExpression(node: arkts.CallExpression, metadata?: CachedMetadata): arkts.CallExpression {
        const _hasMemoEntry = !!metadata?.hasMemoEntry;
        if (_hasMemoEntry) {
            return node;
        }
        const _hasReceiver = metadata?.hasReceiver;
        let _callName: string | undefined = metadata?.callName;
        if (!!_callName && arkts.isIdentifier(node.callee)) {
            _callName = node.callee.name;
        } else if (
            !!_callName &&
            arkts.isMemberExpression(node.callee) &&
            arkts.isIdentifier(node.callee.property)
        ) {
            _callName = node.callee.property.name;
        }
        return factory.insertHiddenArgumentsToCall(
            node,
            PositionalIdTracker.getInstance(arkts.getFileName()).id(_callName),
            _hasReceiver
        );
    }

    static rewriteIdentifier(
        node: arkts.Identifier,
        metadata?: CachedMetadata
    ): arkts.Identifier | arkts.MemberExpression {
        if (MetaDataCollector.getInstance().isDeclaration) {
            return node;
        }
        if (
            !node.name.startsWith(BuiltInNames.GENSYM_INTRINSIC_PREFIX) && 
            !node.name.startsWith(BuiltInNames.GENSYM_UI_PREFIX)
        ) {
            return factory.createMemoParameterAccess(node.name);
        }
        return node;
    }

    static rewriteReturnStatement(
        node: arkts.ReturnStatement,
        metadata?: CachedMetadata
    ): arkts.ReturnStatement | arkts.BlockStatement {
        if (MetaDataCollector.getInstance().isDeclaration) {
            return node;
        }
        return factory.createWrappedReturnStatement(factory.createRecacheCall(node.argument), !node.argument);
    }

    static rewriteVariableDeclarator(
        node: arkts.VariableDeclarator,
        metadata?: CachedMetadata
    ): arkts.VariableDeclarator {
        if (MetaDataCollector.getInstance().isDeclaration) {
            return node;
        }
        const expectReturnType = findLocalReturnTypeFromTypeAnnotation((node.id as arkts.Identifier).typeAnnotation);
        const variableType = RewriteFactory.rewriteType((node.id as arkts.Identifier).typeAnnotation);
        let initializer = node.init;
        if (!!initializer && arkts.isConditionalExpression(initializer) && !!initializer.alternate) {
            let alternate = initializer.alternate;
            if (arkts.isTSAsExpression(alternate)) {
                alternate = arkts.factory.updateTSAsExpression(
                    alternate,
                    !!alternate.expr && arkts.isArrowFunctionExpression(alternate.expr)
                        ? RewriteFactory.rewriteArrowFunction(alternate.expr, metadata, expectReturnType)
                        : alternate.expr,
                    RewriteFactory.rewriteType(alternate.typeAnnotation),
                    alternate.isConst
                );
            } else if (arkts.isArrowFunctionExpression(alternate)) {
                alternate = RewriteFactory.rewriteArrowFunction(alternate, metadata, expectReturnType);
            }
            initializer = arkts.factory.updateConditionalExpression(
                initializer,
                initializer.test,
                initializer.consequent,
                alternate
            );
        } else if (!!initializer && arkts.isArrowFunctionExpression(initializer)) {
            initializer = RewriteFactory.rewriteArrowFunction(initializer, metadata, expectReturnType);
        }
        return arkts.factory.updateVariableDeclarator(
            node,
            node.flag,
            arkts.factory.updateIdentifier(node.id as arkts.Identifier, (node.id as arkts.Identifier).name, variableType),
            initializer
        );
    }
}

export function prepareRewriteScriptFunctionParameters(
    node: arkts.ScriptFunction,
    isSetter?: boolean,
    isGetter?: boolean,
    hasReceiver?: boolean,
    isWithinTypeParams?: boolean
): readonly arkts.Expression[] {
    let newParams: readonly arkts.Expression[] = node.params;
    if (!isSetter && !isGetter) {
        newParams = factory.createHiddenParameterIfNotAdded(node.params, node.hasReceiver);
    } else if (isSetter && node.params.length > 0) {
        const metadata = { isWithinTypeParams };
        if (hasReceiver && node.params.length === 2) {
            newParams = [
                node.params.at(0)!,
                RewriteFactory.rewriteParameter(node.params.at(1)! as arkts.ETSParameterExpression, metadata),
            ];
        } else {
            newParams = [RewriteFactory.rewriteParameter(node.params.at(0)! as arkts.ETSParameterExpression, metadata)];
        }
    }
    return newParams;
}

export function prepareRewriteScriptFunctionReturnType(
    node: arkts.ScriptFunction,
    isGetter?: boolean,
    hasReceiver?: boolean,
    isWithinTypeParams?: boolean
): arkts.TypeNode | undefined {
    let newReturnType: arkts.TypeNode | undefined = node.returnTypeAnnotation;
    if (!!node.returnTypeAnnotation && isGetter) {
        newReturnType = RewriteFactory.rewriteType(node.returnTypeAnnotation, { hasReceiver, isWithinTypeParams });
    }
    return newReturnType;
}

function prepareRewriteScriptFunctionBody(
    node: arkts.ScriptFunction,
    expectReturn?: arkts.TypeNode,
    internalsTransformer?: InternalsTransformer,
    isDecl?: boolean,
    hasMemoEntry?: boolean,
    hasMemoIntrinsic?: boolean,
    callName?: string,
    hasReceiver?: boolean,
    isGetter?: boolean,
    isSetter?: boolean
): arkts.AstNode | undefined {
    if (MetaDataCollector.getInstance().isDeclaration) {
        return node.body;
    }
    if (isGetter || isSetter || isDecl || !node.body || !arkts.isBlockStatement(node.body)) {
        return node.body;
    }

    let newBody: arkts.AstNode | undefined;
    const positionalIdTracker = PositionalIdTracker.getInstance(arkts.getFileName());
    newBody = internalsTransformer?.visitor(node.body) ?? node.body;
    if (!hasMemoEntry && !hasMemoIntrinsic) {
        newBody = RewriteFactory.rewriteScriptFunctionBody(
            node,
            newBody as arkts.BlockStatement,
            positionalIdTracker,
            callName,
            hasReceiver,
            expectReturn
        );
    }
    return newBody;
}

export const rewriteByType = new Map<arkts.Es2pandaAstNodeType, (node: any, ...args: any[]) => arkts.AstNode>([
    [arkts.Es2pandaAstNodeType.AST_NODE_TYPE_ETS_UNION_TYPE, RewriteFactory.rewriteUnionType],
    [arkts.Es2pandaAstNodeType.AST_NODE_TYPE_ETS_FUNCTION_TYPE, RewriteFactory.rewriteFunctionType],
    [arkts.Es2pandaAstNodeType.AST_NODE_TYPE_ETS_TYPE_REFERENCE, RewriteFactory.rewriteETSTypeReference],
    [arkts.Es2pandaAstNodeType.AST_NODE_TYPE_TS_TYPE_ALIAS_DECLARATION, RewriteFactory.rewriteTypeAlias],
    [arkts.Es2pandaAstNodeType.AST_NODE_TYPE_ETS_PARAMETER_EXPRESSION, RewriteFactory.rewriteParameter],
    [arkts.Es2pandaAstNodeType.AST_NODE_TYPE_CLASS_PROPERTY, RewriteFactory.rewriteClassProperty],
    [arkts.Es2pandaAstNodeType.AST_NODE_TYPE_ARROW_FUNCTION_EXPRESSION, RewriteFactory.rewriteArrowFunction],
    [arkts.Es2pandaAstNodeType.AST_NODE_TYPE_SCRIPT_FUNCTION, RewriteFactory.rewriteScriptFunction],
    [arkts.Es2pandaAstNodeType.AST_NODE_TYPE_METHOD_DEFINITION, RewriteFactory.rewriteMethodDefinition],
    [arkts.Es2pandaAstNodeType.AST_NODE_TYPE_CALL_EXPRESSION, RewriteFactory.rewriteCallExpression],
    [arkts.Es2pandaAstNodeType.AST_NODE_TYPE_IDENTIFIER, RewriteFactory.rewriteIdentifier],
    [arkts.Es2pandaAstNodeType.AST_NODE_TYPE_RETURN_STATEMENT, RewriteFactory.rewriteReturnStatement],
    [arkts.Es2pandaAstNodeType.AST_NODE_TYPE_VARIABLE_DECLARATOR, RewriteFactory.rewriteVariableDeclarator],
    [arkts.Es2pandaAstNodeType.AST_NODE_TYPE_PROPERTY, RewriteFactory.rewriteProperty],
    [arkts.Es2pandaAstNodeType.AST_NODE_TYPE_TS_AS_EXPRESSION, RewriteFactory.rewriteTsAsExpression],
]);