/*
 * Copyright (c) 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 { backingField, coerceToAstNode, expectName, flatVisitMethodWithOverloads } from '../../common/arkts-utils';
import { DecoratorNames, NodeCacheNames } from '../../common/predefines';
import { checkIsPropertyCanBeNonNull, createGetter, createSetter, generateThisBacking, hasDecorator, removeDecorator } from './utils';
import {
    BasePropertyTranslator,
    InnerClassPropertyCachedTranslator,
    InnerClassPropertyTranslator,
    InnerClassPropertyTypes,
    PropertyCachedTranslator,
    PropertyCachedTranslatorOptions,
    PropertyTranslator,
    PropertyTranslatorOptions,
} from './base';
import { factory } from './factory';
import { addMemoAnnotation, findCanAddMemoFromTypeAnnotation } from '../../collectors/memo-collectors/utils';
import { PropertyCache } from './cache/propertyCache';
import { CustomComponentInnerClassPropertyInfo } from '../../collectors/ui-collectors/records';
import { CacheFactory as BuilderLambdaCacheFactory } from '../builder-lambda-translators/cache-factory';
import { BuilderParamClassPropertyValueCache } from '../memo-collect-cache';
import { CustomComponentInfo } from '../utils';
import { AstNodeCacheValueMetadata, NodeCacheFactory } from '../../common/node-cache';

function initializeStructWithBuilderLambdaProperty(
    this: BasePropertyTranslator,
    newName: string,
    originalName: string,
    isLastBuilderParam?: boolean,
    rewriteFn?: <T extends arkts.AstNode>(node: T) => T,
): arkts.Statement | undefined {
    const mutableThis: arkts.Expression = generateThisBacking(newName);
    let value = this.property.value ?? arkts.factory.createUndefinedLiteral();
    if (!!value && arkts.isArrowFunctionExpression(value)) {
        value = rewriteFn?.(value) ?? value;
        BuilderParamClassPropertyValueCache.getInstance().collect({ node: value });
    }
    const initialValue = value ?? arkts.factory.createUndefinedLiteral();
    if (isLastBuilderParam) {
        const initializersAccess = factory.createBlockStatementForOptionalExpression(
            arkts.factory.createIdentifier('initializers'),
            originalName,
            { isNonNull: this.initializeOptions?.isRequired }
        );
        const trailingClosure = arkts.factory.createBinaryExpression(
            initializersAccess,
            arkts.factory.createIdentifier('content'),
            arkts.Es2pandaTokenType.TOKEN_TYPE_PUNCTUATOR_NULLISH_COALESCING
        );
        const explicitParam = initializersAccess.clone();
        return arkts.factory.createExpressionStatement(
            arkts.factory.createAssignmentExpression(
                mutableThis,
                arkts.factory.createBinaryExpression(
                    trailingClosure,
                    arkts.factory.createBinaryExpression(
                        explicitParam,
                        initialValue,
                        arkts.Es2pandaTokenType.TOKEN_TYPE_PUNCTUATOR_NULLISH_COALESCING
                    ),
                    arkts.Es2pandaTokenType.TOKEN_TYPE_PUNCTUATOR_NULLISH_COALESCING
                ),
                arkts.Es2pandaTokenType.TOKEN_TYPE_PUNCTUATOR_SUBSTITUTION
            )
        );
    } else {
        const initializersAccess = factory.createBlockStatementForOptionalExpression(
            arkts.factory.createIdentifier('initializers'),
            originalName,
            { isNonNull: this.initializeOptions?.isRequired }
        );
        return arkts.factory.createExpressionStatement(
            arkts.factory.createAssignmentExpression(
                mutableThis,
                arkts.factory.createBinaryExpression(
                    initializersAccess,
                    initialValue,
                    arkts.Es2pandaTokenType.TOKEN_TYPE_PUNCTUATOR_NULLISH_COALESCING
                ),
                arkts.Es2pandaTokenType.TOKEN_TYPE_PUNCTUATOR_SUBSTITUTION
            )
        );
    }
}

function fieldWithBuilderLambdaProperty(
    this: BasePropertyTranslator,
    fieldFn: (name: string) => arkts.ClassProperty,
    newName: string
): arkts.ClassProperty {
    if (
        !!this.propertyType &&
        (arkts.isETSFunctionType(this.propertyType) || arkts.isETSUnionType(this.propertyType))
    ) {
        addMemoAnnotation(this.propertyType);
    }
    const field = fieldFn(newName);
    NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(field);
    return field;
}

function getterWithBuilderLambdaProperty(
    this: BasePropertyTranslator,
    newName: string,
    originalName: string,
    isNonNull?: boolean
): arkts.MethodDefinition {
    const getter: arkts.MethodDefinition = createGetter(
        originalName,
        this.propertyType,
        generateThisBacking(newName, false, isNonNull),
        true
    );
    NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(getter);
    return getter;
}

function setterWithBuilderLambdaProperty(
    this: BasePropertyTranslator,
    newName: string,
    originalName: string
): arkts.MethodDefinition {
    const thisSetValue: arkts.Expression = generateThisBacking(newName, false, false);
    const right: arkts.Identifier = arkts.factory.createIdentifier('value');
    const setter: arkts.MethodDefinition = createSetter(originalName, this.propertyType, thisSetValue, right, true);
    NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(setter);
    return setter;
}

function updateStateMethodInInnerClass<T extends InnerClassPropertyTypes>(
    this: InnerClassPropertyTranslator<T>,
    method: arkts.MethodDefinition
): arkts.MethodDefinition {
    if (!this.decorator) {
        throw new Error('interface property does not have any decorator.');
    }
    if (method.kind === arkts.Es2pandaMethodDefinitionKind.METHOD_DEFINITION_KIND_GET) {
        const type: arkts.TypeNode | undefined = method.function.returnTypeAnnotation;
        if (!!type && (arkts.isETSFunctionType(type) || arkts.isETSUnionType(type))) {
            addMemoAnnotation(type);
        }
        removeDecorator(method, this.decorator);
        NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(method, { isGetter: true });
    } else if (method.kind === arkts.Es2pandaMethodDefinitionKind.METHOD_DEFINITION_KIND_SET) {
        const param = method.function.params[0] as arkts.ETSParameterExpression;
        const type = param.typeAnnotation;
        if (!!type && (arkts.isETSFunctionType(type) || arkts.isETSUnionType(type))) {
            addMemoAnnotation(type);
        }
        removeDecorator(method, this.decorator);
        NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(method, { isSetter: true });
    }
    return method;
}

function updateStatePropertyInInnerClass<T extends InnerClassPropertyTypes>(
    this: InnerClassPropertyTranslator<T>,
    property: arkts.ClassProperty
): arkts.ClassProperty {
    if (!this.decorator) {
        throw new Error('interface property does not have any decorator.');
    }
    const type: arkts.TypeNode | undefined = property.typeAnnotation;
    if (!!type && arkts.isETSFunctionType(type) && findCanAddMemoFromTypeAnnotation(type).canAddMemo) {
        addMemoAnnotation(type);
    }
    removeDecorator(property, this.decorator);
    return property;
}

/**
 * @deprecated
 */
export class BuilderParamTranslator extends PropertyTranslator {
    protected hasInitializeStruct: boolean = true;
    protected hasUpdateStruct: boolean = false;
    protected hasToRecord: boolean = false;
    protected hasField: boolean = true;
    protected hasGetter: boolean = true;
    protected hasSetter: boolean = true;

    constructor(options: PropertyTranslatorOptions) {
        super(options);
        const isRequired = hasDecorator(this.property, DecoratorNames.REQUIRE);
        this.initializeOptions = {
            isRequired,
            shouldCheckNonNull: !isRequired
        };
    }

    initializeStruct(
        newName: string,
        originalName: string,
        metadata?: AstNodeCacheValueMetadata
    ): arkts.Statement | undefined {
        const isLastBuilderParam: boolean = this.structInfo.lastBuilderParam === originalName;
        return initializeStructWithBuilderLambdaProperty.bind(this)(newName, originalName, isLastBuilderParam);
    }

    field(newName: string, originalName?: string, metadata?: AstNodeCacheValueMetadata): arkts.ClassProperty {
        return fieldWithBuilderLambdaProperty.bind(this)(super.field.bind(this), newName);
    }

    getter(newName: string, originalName: string, metadata?: AstNodeCacheValueMetadata): arkts.MethodDefinition {
        return getterWithBuilderLambdaProperty.bind(this)(newName, originalName);
    }

    setter(newName: string, originalName: string, metadata?: AstNodeCacheValueMetadata): arkts.MethodDefinition {
        return setterWithBuilderLambdaProperty.bind(this)(newName, originalName);
    }
}

export class BuilderParamCachedTranslator extends PropertyCachedTranslator {
    protected hasInitializeStruct: boolean = true;
    protected hasUpdateStruct: boolean = false;
    protected hasToRecord: boolean = false;
    protected hasField: boolean = true;
    protected hasGetter: boolean = true;
    protected hasSetter: boolean = true;

    constructor(options: PropertyCachedTranslatorOptions) {
        super(options);
        const isRequired = this.propertyInfo.annotationInfo?.hasRequire;
        this.initializeOptions = {
            isRequired,
            shouldCheckNonNull: !isRequired
        };
        this.initializeOptions.canDefinitelyBeNonNull = checkIsPropertyCanBeNonNull(
            this.property,
            this.initializeOptions
        );
    }

    initializeStruct(
        newName: string,
        originalName: string,
        metadata?: AstNodeCacheValueMetadata
    ): arkts.Statement | undefined {
        const rewriteFn = <T extends arkts.AstNode>(node: T): T => {
            const _node = coerceToAstNode<arkts.ArrowFunctionExpression>(node);
            return BuilderLambdaCacheFactory.updateBuilderArrowFunction(_node) as arkts.AstNode as T;
        };
        const isLastBuilderParam: boolean = this.propertyInfo?.structInfo?.lastBuilderParam === originalName;
        return initializeStructWithBuilderLambdaProperty.bind(this)(newName, originalName, isLastBuilderParam, rewriteFn);
    }

    field(newName: string, originalName?: string, metadata?: AstNodeCacheValueMetadata): arkts.ClassProperty {
        return fieldWithBuilderLambdaProperty.bind(this)(super.field.bind(this), newName);
    }

    getter(newName: string, originalName: string, metadata?: AstNodeCacheValueMetadata): arkts.MethodDefinition {
        return getterWithBuilderLambdaProperty.bind(this)(
            newName,
            originalName,
            this.initializeOptions?.isRequired || this.property.typeAnnotation?.tsType?.definitelyNotETSNullish
        );
    }

    setter(newName: string, originalName: string, metadata?: AstNodeCacheValueMetadata): arkts.MethodDefinition {
        return setterWithBuilderLambdaProperty.bind(this)(newName, originalName);
    }
}

/**
 * @deprecated
 */
export class BuilderParamInnerClassTranslator<T extends InnerClassPropertyTypes> extends InnerClassPropertyTranslator<T> {
    protected decorator: DecoratorNames = DecoratorNames.BUILDER_PARAM;

    /**
     * @deprecated
     */
    static canBeTranslated(node: arkts.AstNode): node is InnerClassPropertyTypes {
        if (arkts.isMethodDefinition(node)) {
            return hasDecorator(node, DecoratorNames.BUILDER_PARAM);
        } else if (arkts.isClassProperty(node)) {
            return hasDecorator(node, DecoratorNames.BUILDER_PARAM);
        }
        return false;
    }

    /**
     * Add `@memo` to getter's return type and setter's param type (expecting a function type or a function type within a union type).
     *
     * @param method expecting getter with `@BuilderParam` and a setter with `@BuilderParam` in the overloads.
     */
    updateStateMethodInInnerClass(method: arkts.MethodDefinition): arkts.MethodDefinition {
        return updateStateMethodInInnerClass.bind(this)(method);
    }

    /**
     * Add `@memo` to the type of the property (expecting a function type or a function type within a union type).
     *
     * @param property expecting property with `@BuilderParam`.
     */
    updateStatePropertyInInnerClass(property: arkts.ClassProperty): arkts.ClassProperty {
        return updateStatePropertyInInnerClass.bind(this)(property);
    }
}

export class BuilderParamCachedInnerClassTranslator<
    T extends InnerClassPropertyTypes,
> extends InnerClassPropertyCachedTranslator<T> {
    protected decorator: DecoratorNames = DecoratorNames.BUILDER_PARAM;

    static canBeTranslated(
        node: arkts.AstNode,
        metadata?: CustomComponentInnerClassPropertyInfo
    ): node is InnerClassPropertyTypes {
        return !!metadata?.annotationInfo?.hasBuilderParam;
    }

    /**
     * Add `@memo` to getter's return type and setter's param type (expecting a function type or a function type within a union type).
     *
     * @param method expecting getter with `@BuilderParam` and a setter with `@BuilderParam` in the overloads.
     */
    updateStateMethodInInnerClass(method: arkts.MethodDefinition): arkts.MethodDefinition {
        return updateStateMethodInInnerClass.bind(this)(method);
    }

    /**
     * Add `@memo` to the type of the property (expecting a function type or a function type within a union type).
     *
     * @param property expecting property with `@BuilderParam`.
     */
    updateStatePropertyInInnerClass(property: arkts.ClassProperty): arkts.ClassProperty {
        const newProperty = updateStatePropertyInInnerClass.bind(this)(property);
        return newProperty;
    }
}