/*
 * 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 path from 'path';
import * as arkts from '@koalaui/libarkts';
import { matchPrefix, removeRelativePathSuffix } from '../common/arkts-utils';
import {
    ARKUI_IMPORT_PREFIX_NAMES,
    CUSTOM_DIALOG_CONTROLLER_SOURCE_NAME,
    CustomComponentNames,
    CustomDialogNames,
    DecoratorNames,
    StructDecoratorNames,
    GlobalReusePoolNames,
} from '../common/predefines';
import { DeclarationCollector } from '../common/declaration-collector';
import { MetaDataCollector } from '../common/metadata-collector';
import { hasDecorator } from './property-translators/utils';
import { ImportCollector } from '../common/import-collector';
import { getDeclOriPath } from './interop/utils';

export type EntryAnnoInfo = {
    startPosition: arkts.SourcePosition;
    endPosition: arkts.SourcePosition;
    name: string;
    annotation?: arkts.AnnotationUsage;
};

export type LocalImportInfo = {
    symbolName: string;
    localSource: string;
    localTypeName: string;
}

export function getSystemResourcePath(): string {
    return path.resolve(__dirname, './sysResource.js');
}

// IMPORT
export function findImportSourceByName(importName: string): string {
    const source = DeclarationCollector.getInstance().findExternalSourceFromName(importName);
    if (!source) {
        throw new Error(`cannot find import source by name: "${importName}".`);
    }
    return source;
}

export function findImportSourceByNode(declNode: arkts.AstNode): string {
    const source = DeclarationCollector.getInstance().findExternalSourceFromNode(declNode);
    if (!source) {
        throw new Error(`cannot find import source by peer.`);
    }
    return source;
}

export function findLocalImport(
    node: arkts.ETSImportDeclaration,
    sourceName: string,
    importedName: string
): arkts.Identifier | undefined {
    const isFromSource = !!node.source && node.source.str === sourceName;
    if (!isFromSource) return undefined;

    const importSpecifier = node.specifiers.find(
        (spec) => arkts.isImportSpecifier(spec) && !!spec.imported && spec.imported.name === importedName
    ) as arkts.ImportSpecifier | undefined;
    return importSpecifier?.local ?? importSpecifier?.imported;
}

// AST NODE
export function isStatic(node: arkts.AstNode): boolean {
    return node.isStatic;
}

/**
 * Determine whether the type node includes null or undefined type.
 *
 * @param type type node
 */
export function hasNullOrUndefinedType(type: arkts.TypeNode): boolean {
    let res: boolean = false;
    if (arkts.isETSUnionType(type)) {
        type.types.forEach((item: arkts.TypeNode) => {
            res = res || hasNullOrUndefinedType(item);
        });
    }
    if (arkts.isETSUndefinedType(type) || arkts.isETSNullType(type)) {
        res = true;
    }
    return res;
}

// TYPE PARAMETER
export function getTypeParamsFromClassDecl(node: arkts.ClassDeclaration | undefined): readonly arkts.TSTypeParameter[] {
    return node?.definition?.typeParams?.params ?? [];
}

export function getTypeNameFromTypeParameter(node: arkts.TSTypeParameter | undefined): string | undefined {
    return node?.name?.name;
}

// GETTER
export function getGettersFromClassDecl(definition: arkts.ClassDefinition): arkts.MethodDefinition[] {
    return definition.body.filter(
        (member) =>
            arkts.isMethodDefinition(member) &&
            arkts.hasModifierFlag(member, arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_GETTER) &&
            !hasDecorator(member, DecoratorNames.COMPUTED)
    ) as arkts.MethodDefinition[];
}

// ANNOTATION
export function hasPropertyInAnnotation(annotation: arkts.AnnotationUsage, propertyName: string, allowDefault?: boolean): boolean {
    return !!getPropertyInAnnotation(annotation, propertyName, allowDefault);
}

export function getPropertyInAnnotation(
    annotation: arkts.AnnotationUsage, 
    propertyName: string, 
    allowDefault?: boolean
): arkts.ClassProperty | undefined {
    return (allowDefault ? arkts.getAnnotationDeclarationProperties(annotation) : annotation.properties).find(
        (annoProp: arkts.AstNode) =>
            arkts.isClassProperty(annoProp) &&
            annoProp.key &&
            arkts.isIdentifier(annoProp.key) &&
            annoProp.key.name === propertyName
    ) as arkts.ClassProperty | undefined;
}

// CUSTOM COMPONENT
export type CustomComponentInfo = {
    name: string;
    isDecl: boolean;
    isCustomComponentClass: boolean;
    annotations: CustomComponentAnontations;
    lastBuilderParam?: string;
};

export type CustomComponentAnontations = {
    component?: arkts.AnnotationUsage;
    componentV2?: arkts.AnnotationUsage;
    entry?: arkts.AnnotationUsage;
    reusable?: arkts.AnnotationUsage;
    reusableV2?: arkts.AnnotationUsage;
    customLayout?: arkts.AnnotationUsage;
    customDialog?: arkts.AnnotationUsage;
};

export type StructAnnoationInfo = {
    isComponent: boolean;
    isComponentV2: boolean;
    isEntry: boolean;
    isReusable: boolean;
    isReusableV2: boolean;
    isCustomLayout: boolean;
    isCustomDialog: boolean;
};

export type ComponentType = {
    hasEntry: boolean;
    hasComponent: boolean;
    hasComponentV2: boolean;
    hasCustomDialog: boolean;
    hasCustomLayout: boolean;
    hasReusable: boolean;
    hasReusableV2: boolean;
};

export type ClassInfo = {
    className: string;
    isFromStruct: boolean;
};

export function isCustomComponentAnnotation(
    anno: arkts.AnnotationUsage,
    decoratorName: StructDecoratorNames,
    ignoreDecl?: boolean
): boolean {
    if (!(!!anno.expr && arkts.isIdentifier(anno.expr) && anno.expr.name === decoratorName)) {
        return false;
    }
    if (!ignoreDecl) {
        const decl = arkts.getPeerIdentifierDecl(anno.expr.peer);
        if (!decl) {
            return false;
        }
        const moduleName = arkts.getProgramFromAstNode(decl)?.moduleName;
        if (!moduleName || !matchPrefix(ARKUI_IMPORT_PREFIX_NAMES, moduleName)) {
            return false;
        }
        DeclarationCollector.getInstance().collect(decl);
    }
    return true;
}

export function isBaseComponentClass(name: string): boolean {
    return (
        name === CustomComponentNames.COMPONENT_CLASS_NAME ||
        name === CustomComponentNames.COMPONENT_V2_CLASS_NAME ||
        name === CustomComponentNames.BASE_CUSTOM_DIALOG_NAME
    );
}

export function collectCustomComponentScopeInfo(
    node: arkts.ClassDeclaration | arkts.ETSStructDeclaration
): CustomComponentInfo | undefined {
    const definition: arkts.ClassDefinition | undefined = node.definition;
    if (!definition || !definition?.ident?.name) {
        return undefined;
    }
    const isStruct = definition.isFromStruct || arkts.isETSStructDeclaration(node);
    const isDecl: boolean = MetaDataCollector.getInstance().isDeclaration ||
        arkts.hasModifierFlag(node, arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_DECLARE);
    const isCustomComponentClass = !isStruct && isDecl && isBaseComponentClass(definition.ident.name);
    const shouldIgnoreDecl = isStruct || isDecl;
    if (!isStruct && !isCustomComponentClass) {
        return undefined;
    }
    let annotations: CustomComponentAnontations = {};
    if (!isCustomComponentClass) {
        let isCustomComponent: boolean = false;
        for (const anno of definition.annotations) {
            const { isComponent, isComponentV2, isEntry, isReusable, isReusableV2, isCustomLayout, isCustomDialog } =
                getAnnotationInfoForStruct(anno, shouldIgnoreDecl);
            isCustomComponent ||= isComponent || isComponentV2 || isCustomDialog;
            annotations = {
                ...annotations,
                ...(isComponent && !annotations?.component && { component: anno }),
                ...(isComponentV2 && !annotations?.componentV2 && { componentV2: anno }),
                ...(isEntry && !annotations?.entry && { entry: anno }),
                ...(isReusable && !annotations?.reusable && { reusable: anno }),
                ...(isReusableV2 && !annotations?.reusableV2 && { reusableV2: anno }),
                ...(isCustomLayout && !annotations?.customLayout && { customLayout: anno }),
                ...(isCustomDialog && !annotations?.customDialog && { customDialog: anno }),
            };
        }
        if (!isCustomComponent) {
            return undefined;
        }
        checkGlobalReuseAnnotation(annotations);
    }
    return {
        name: definition.ident.name,
        isDecl,
        isCustomComponentClass,
        annotations: annotations as CustomComponentAnontations,
    };
}

function checkGlobalReuseAnnotation(annotations: CustomComponentAnontations): void {
    const componentAnno = annotations.component ?? annotations.componentV2;
    if (!componentAnno || componentAnno.properties.length === 0) {
        return;
    }
    const hasReusePool = hasPropertyInAnnotation(componentAnno, GlobalReusePoolNames.REUSE_POOL);
    const hasPoolAccepts = hasPropertyInAnnotation(componentAnno, GlobalReusePoolNames.POOL_ACCEPTS);
    checkReusePoolAndPoolAcceptsBothSet(componentAnno, hasReusePool, hasPoolAccepts);
    checkPoolAcceptsNotEmpty(componentAnno, hasReusePool, hasPoolAccepts);
}

function checkReusePoolAndPoolAcceptsBothSet(
    componentAnno: arkts.AnnotationUsage,
    hasReusePool: boolean,
    hasPoolAccepts: boolean
): void {
    if (hasReusePool === hasPoolAccepts) {
        return;
    }
    const message = `'reusePool' and 'poolAccepts' must be both set. Neither can be omitted when using the global reuse pool.`;
    const diagnosticKind = arkts.createDiagnosticKind(message, arkts.Es2pandaPluginDiagnosticType.ES2PANDA_PLUGIN_ERROR);
    const diagnosticInfo = arkts.createDiagnosticInfo(diagnosticKind, componentAnno.startPosition);
    if (!hasReusePool && hasPoolAccepts) {
        const poolAcceptsProp = getPropertyInAnnotation(componentAnno, GlobalReusePoolNames.POOL_ACCEPTS);
        if (!poolAcceptsProp || !poolAcceptsProp.value) {
            return;
        }
        const annoName = (componentAnno.expr as arkts.Identifier).name;
        const fixCode = `@${annoName}({ reusePool: ReusePoolOwnership.SHARED, poolAccepts: ${poolAcceptsProp.value.dumpSrc()} })`;
        const startPos = arkts.createSourcePosition(componentAnno.startPosition.getIndex() - 1, componentAnno.startPosition.getLine());
        const annoRange = arkts.createSourceRange(startPos, componentAnno.endPosition);
        const suggestionKind = arkts.createDiagnosticKind(message, arkts.Es2pandaPluginDiagnosticType.ES2PANDA_PLUGIN_SUGGESTION);
        const suggestionInfo = arkts.createSuggestionInfo(suggestionKind, fixCode, `Add 'reusePool' property`, annoRange);
        arkts.logDiagnosticWithSuggestion(diagnosticInfo, suggestionInfo);
    } else {
        arkts.logDiagnostic(diagnosticKind, componentAnno.startPosition);
    }
}

function checkPoolAcceptsNotEmpty(
    componentAnno: arkts.AnnotationUsage,
    hasReusePool: boolean,
    hasPoolAccepts: boolean
): void {
    if (!hasPoolAccepts || !hasReusePool) {
        return;
    }
    const poolAcceptsProp = getPropertyInAnnotation(componentAnno, GlobalReusePoolNames.POOL_ACCEPTS);
    if (
        !poolAcceptsProp ||
        !arkts.isClassProperty(poolAcceptsProp) ||
        !poolAcceptsProp.value ||
        !arkts.isArrayExpression(poolAcceptsProp.value)
    ) {
        return;
    }
    const poolAcceptsValue = poolAcceptsProp.value;
    if (poolAcceptsValue.elements.length !== 0) {
        return;
    }
    const reusePoolProp = getPropertyInAnnotation(componentAnno, GlobalReusePoolNames.REUSE_POOL);
    if (
        !reusePoolProp ||
        !reusePoolProp.value ||
        !arkts.isMemberExpression(reusePoolProp.value) ||
        !arkts.isIdentifier(reusePoolProp.value.property) ||
        reusePoolProp.value.property.name === GlobalReusePoolNames.REUSE_POOL_OWNERSHIP_OFF
    ) {
        return;
    }
    const message = `'poolAccepts' cannot be an empty array. Provide at least one '@Reusable' or '@ReusableV2' component.`;
    const diagnosticKind = arkts.createDiagnosticKind(message, arkts.Es2pandaPluginDiagnosticType.ES2PANDA_PLUGIN_ERROR);
    arkts.logDiagnostic(diagnosticKind, componentAnno.startPosition);
}

export function getAnnotationInfoForStruct(
    anno: arkts.AnnotationUsage,
    shouldIgnoreDecl: boolean
): StructAnnoationInfo {
    const isComponent = isCustomComponentAnnotation(anno, StructDecoratorNames.COMPONENT, shouldIgnoreDecl);
    const isComponentV2 = isCustomComponentAnnotation(anno, StructDecoratorNames.COMPONENT_V2, shouldIgnoreDecl);
    const isEntry = isCustomComponentAnnotation(anno, StructDecoratorNames.ENTRY, shouldIgnoreDecl);
    const isReusable = isCustomComponentAnnotation(anno, StructDecoratorNames.RESUABLE, shouldIgnoreDecl);
    const isReusableV2 = isCustomComponentAnnotation(anno, StructDecoratorNames.RESUABLE_V2, shouldIgnoreDecl);
    const isCustomLayout = isCustomComponentAnnotation(anno, StructDecoratorNames.CUSTOM_LAYOUT, shouldIgnoreDecl);
    const isCustomDialog = isCustomComponentAnnotation(anno, StructDecoratorNames.CUSTOMDIALOG, shouldIgnoreDecl);
    return { isComponent, isComponentV2, isEntry, isReusable, isReusableV2, isCustomLayout, isCustomDialog };
}

export function isComponentStruct(node: arkts.ETSStructDeclaration, scopeInfo: CustomComponentInfo): boolean {
    return scopeInfo.name === node.definition!.ident?.name;
}

/**
 * Determine whether it is a custom component.
 *
 * @param node class declaration node
 */
export function isCustomComponentClass(node: arkts.ClassDeclaration, scopeInfo: CustomComponentInfo): boolean {
    const classDef = node.definition;
    if (!classDef?.ident?.name) {
        return false;
    }
    const name: string = classDef.ident.name;
    return name === scopeInfo.name;
}

export function isCustomComponentInnerClass(node: arkts.ClassDeclaration): boolean {
    const classDef = node.definition;
    const checkPrefix = !!classDef?.ident?.name.startsWith(CustomComponentNames.COMPONENT_INTERFACE_PREFIX);
    const checkComponent = !!classDef?.annotations.some(
        (anno) =>
            isCustomComponentAnnotation(anno, StructDecoratorNames.COMPONENT) ||
            isCustomComponentAnnotation(anno, StructDecoratorNames.COMPONENT_V2) ||
            isCustomComponentAnnotation(anno, StructDecoratorNames.CUSTOMDIALOG)
    );
    return checkPrefix && checkComponent;
}

export function getCustomComponentOptionsName(className: string): string {
    return `${CustomComponentNames.COMPONENT_INTERFACE_PREFIX}${className}`;
}

export function getStructNameFromOptionsName(optionsName: string): string {
    return optionsName.slice(CustomComponentNames.COMPONENT_INTERFACE_PREFIX.length);
}

/**
 * Determine whether it is method with specified name.
 *
 * @param method method definition node
 * @param name specified method name
 */
export function isKnownMethodDefinition(method: arkts.MethodDefinition, name: string): boolean {
    if (!method || !arkts.isMethodDefinition(method)) {
        return false;
    }

    // For now, we only considered matched method name.
    const isNameMatched: boolean = method.id?.name === name;
    return isNameMatched;
}

export function isSpecificNewClass(node: arkts.ETSNewClassInstanceExpression, className: string): boolean {
    if (
        node.typeRef &&
        arkts.isETSTypeReference(node.typeRef) &&
        node.typeRef.part &&
        arkts.isETSTypeReferencePart(node.typeRef.part) &&
        node.typeRef.part.name &&
        arkts.isIdentifier(node.typeRef.part.name) &&
        node.typeRef.part.name.name === className
    ) {
        return true;
    }
    return false;
}

export function isCustomDialogControllerOptions(
    newNode: arkts.TSInterfaceDeclaration,
    externalSourceName: string | undefined
): boolean {
    return (
        externalSourceName === CUSTOM_DIALOG_CONTROLLER_SOURCE_NAME &&
        !!newNode.id &&
        newNode.id.name === CustomDialogNames.CUSTOM_DIALOG_CONTROLLER_OPTIONS
    );
}

export function getComponentExtendsName(annotations: CustomComponentAnontations, componentType: ComponentType): string {
    if (!!annotations.reusable) {
        componentType.hasReusable ||= true;
    } else if (!!annotations.reusableV2) {
        componentType.hasReusableV2 ||= true;
    }
    if (!!annotations.entry) {
        componentType.hasEntry ||= true;
    }
    if (!!annotations.customLayout) {
        componentType.hasCustomLayout ||= true;
    }
    if (!!annotations.customDialog) {
        componentType.hasCustomDialog ||= true;
        return CustomComponentNames.BASE_CUSTOM_DIALOG_NAME;
    }
    if (!!annotations.componentV2) {
        componentType.hasComponentV2 ||= true;
        return CustomComponentNames.COMPONENT_V2_CLASS_NAME;
    }
    componentType.hasComponent ||= true;
    return CustomComponentNames.COMPONENT_CLASS_NAME;
}

export function computedField(name: string): string {
    return `__computed_${name}`;
}

export function monitorField(name: string): string {
    return `__monitor_${name}`;
}

export function syncMonitorField(name: string): string {
    return `__SyncMonitor_${name}`;
}

export function optionsHasField(name: string): string {
    return `__options_has_${name}`;
}

/**
 * @deprecated
 */
export function getClassPropertyType(property: arkts.ClassProperty): arkts.TypeNode | undefined {
    const type = property.typeAnnotation;
    if (!!type) {
        return type.clone();
    }
    return inferTypeFromValue(property.value);
}

/**
 * @deprecated
 */
export function inferTypeFromValue(value: arkts.AstNode | undefined): arkts.TypeNode | undefined {
    let inferredType: arkts.AstNode | undefined = value ? arkts.createTypeNodeFromTsType(value) : undefined;
    if (inferredType && arkts.isTypeNode(inferredType)) {
        return inferredType;
    }
    return undefined;
}

export function expectNameInTypeReference(node: arkts.TypeNode | undefined): arkts.Identifier | undefined {
    if (!node || !arkts.isETSTypeReference(node)) {
        return undefined;
    }
    const part = node.part;
    if (!part || !arkts.isETSTypeReferencePart(part)) {
        return undefined;
    }
    const nameNode = part.name;
    if (!nameNode || !arkts.isIdentifier(nameNode)) {
        return undefined;
    }
    return nameNode;
}

/**
 * Get the value of the corresponding property from the annotation.
 * @param anno annotation.
 * @param decoratorName annotation name.
 * @param key property key name.
 */
export function getValueInObjectAnnotation(
    anno: arkts.AnnotationUsage | undefined,
    decoratorName: string,
    key: string,
    allowDefault?: boolean
): arkts.Expression | undefined {
    if (!anno) {
        return undefined;
    }
    const isSuitableAnnotation: boolean =
        !!anno.expr && arkts.isIdentifier(anno.expr) && anno.expr.name === decoratorName;
    if (!isSuitableAnnotation) {
        return undefined;
    }
    const keyItem: arkts.AstNode | undefined = (allowDefault 
        ? arkts.getAnnotationDeclarationProperties(anno) 
        : anno.properties
    ).find(
        (annoProp: arkts.AstNode) =>
            arkts.isClassProperty(annoProp) &&
            annoProp.key &&
            arkts.isIdentifier(annoProp.key) &&
            annoProp.key.name === key
    );
    if (keyItem && arkts.isClassProperty(keyItem)) {
        return keyItem.value;
    }
    return undefined;
}

export function findLocalSourceNameToImport(
    symbolName: string,
    declModuleName: string | undefined,
    declRelativePath: string | undefined,
    isDeclForDynamicStaticInterop?: boolean
): string | undefined {
    const localSourceName: string | undefined = ImportCollector.getInstance().getLocalSource(symbolName);
    if (!!localSourceName) {
        return localSourceName;
    }
    if (isDeclForDynamicStaticInterop) {
        const declOriPath = getDeclOriPath(declRelativePath);
        return !!declOriPath ? removeRelativePathSuffix(declOriPath) : undefined;
    }
    if (!!declModuleName && matchPrefix(ARKUI_IMPORT_PREFIX_NAMES, declModuleName)) {
        return declModuleName;
    }
    if (!!declRelativePath) {
        return removeRelativePathSuffix(declRelativePath);
    }
    return undefined;
}