/*
 * 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.
 */

import * as path from 'path';
import * as fs from 'fs';

import { initKoalaModules } from '../init/init_koala_modules';
import {
    BuildConfig,
    PluginsConfig,
    CompileJobInfo,
    FileInfo,
    DeclgenV1JobInfo,
    CompileJobType,
    AliasConfig,
    ArkTS,
    KoalaModule,
    JobContentType,
    BUILD_MODE
} from '../types';
import {
    Logger,
    LogDataFactory
} from '../logger';
import {
    buildDeclgenOutputPath,
    changeFileExtension,
    createFileIfNotExists,
    ensurePathExists,
    validatePathLength,
} from './utils';
import {
    DECL_ETS_SUFFIX,
    DECL_TS_SUFFIX,
    STATIC_RECORD_FILE,
    STATIC_RECORD_FILE_CONTENT,
    ENABLE_DECLARATION_BARRIER,
    MERGED_INTERMEDIATE_FILE
} from '../pre_define';
import {
    PluginDriver,
    PluginHook
} from '../plugins/plugins_driver';
import { KitImportTransformer } from '../plugins/KitImportTransformer';
import { ErrorCode, DriverError } from '../util/error';
import {
    BS_PERF_FILE_NAME,
    RecordEvent,
    StatisticsRecorder,
} from '../util/statsRecorder'

enum Ets2pandaEvent {
    CREATE_INSTANCE = 'Create instance',
    PARSE = 'Parse stage',
    PLUGIN_PARSE = 'Parse plugins stage',
    DECLGEN = 'Declgen stage',
    CHECK = 'Checker stage',
    PLUGIN_CHECK = 'Checker plugins stage',
    EMIT = 'Emit binary stage',
    DESTROY_INSTANCE = 'Destroy instance'
}

function formEvent(event: Ets2pandaEvent): string {
    return '[Ets2panda] ' + event;
}

export class Ets2panda {
    private static instance?: Ets2panda;
    private readonly logger: Logger = Logger.getInstance();
    private readonly plugins: PluginsConfig;
    private readonly buildSdkPath: string;
    private readonly aliasConfig: Record<string, Record<string, AliasConfig>>;
    private readonly declgenV2OutDir: string;
    private readonly cacheDir: string;
    private readonly pluginDriver: PluginDriver = PluginDriver.getInstance();
    private readonly recordType?: 'ON' | 'OFF';
    private readonly projectRootPath: string;
    private readonly debugBuild: boolean = false;
    private readonly dumpPerf: boolean = false;
    /** Directory where per-module decl obfuscation name-cache JSON files are written (see --generate-decl:nameCachePath). */
    private readonly declFileNameCachePath?: string;

    // NOTE: should be Ets2panda Wrapper Module
    // NOTE: to be refactored
    private readonly koalaModule: KoalaModule;

    private constructor(buildConfig: BuildConfig) {
        this.koalaModule = initKoalaModules(buildConfig);
        this.plugins = buildConfig.plugins;
        this.buildSdkPath = buildConfig.buildSdkPath;
        this.aliasConfig = buildConfig.aliasConfig;
        this.cacheDir = buildConfig.cachePath;
        this.recordType = buildConfig.recordType;
        this.declgenV2OutDir = buildConfig.declgenV2OutPath;
        this.pluginDriver.initPlugins(buildConfig);
        this.projectRootPath = buildConfig.projectRootPath;
        this.debugBuild = (buildConfig.buildMode === BUILD_MODE.DEBUG);
        this.dumpPerf = buildConfig.dumpPerf ?? false;
        this.declFileNameCachePath = buildConfig.declFileNameCacheConfig?.nameCachePath;
    }

    public static getInstance(buildConfig?: BuildConfig): Ets2panda {
        if (!Ets2panda.instance) {
            if (!buildConfig) {
                throw new Error('buildConfig is required for the first Ets2panda instantiation.');
            }
            Ets2panda.instance = new Ets2panda(buildConfig);
        }
        return Ets2panda.instance;
    }

    public static destroyInstance(): void {
        Ets2panda.instance = undefined;
    }

    public initalize(): void {
        if (this.koalaModule === undefined) {
            throw new Error('KoalaModule is not initialized');
        }
        const arkts: ArkTS = this.koalaModule.arkts;
        arkts.memInitialize();
    }

    public finalize(): void {
        if (this.koalaModule === undefined) {
            throw new Error('KoalaModule is not initialized');
        }
        const arkts: ArkTS = this.koalaModule.arkts;
        arkts.memFinalize();
    }

    private transformImportStatementsWithAliasConfig(): void {
        if (this.plugins === undefined) {
            return;
        }
        const { arkts, arktsGlobal } = this.koalaModule;
        let ast = arkts.EtsScript.fromContext();
        if (this.aliasConfig && Object.keys(this.aliasConfig).length > 0) {
            // if aliasConfig is set, transform aliasName@kit.xxx to default@ohos.xxx through the plugin
            this.logger.printDebug('Transforming import statements with alias config');
            let transformAst = new KitImportTransformer(
                arkts,
                arktsGlobal.compilerContext.program,
                this.buildSdkPath,
                this.aliasConfig
            ).transform(ast);
            this.pluginDriver.getPluginContext().setArkTSAst(transformAst);
        } else {
            this.pluginDriver.getPluginContext().setArkTSAst(ast);
        }
    }

    private formCompileCliCmd(
        job: CompileJobInfo,
        incremental: boolean = false
    ): string[] {

        const ets2pandaCmd: string[] = [
            '_',
            '--extension',
            'ets',
            '--arktsconfig',
            job.arktsConfig
        ]

        // Force simultaneous mode, since plugins may not support single file mode
        ets2pandaCmd.push('--simultaneous');
        if ((job.contentType === JobContentType.CLUSTER) && incremental) {
            ets2pandaCmd.push('--incremental');
        }

        if ((job.jobType & CompileJobType.DECL) !== 0) {
            ets2pandaCmd.push('--emit-declaration');
            if (this.declFileNameCachePath) {
                ets2pandaCmd.push(`--generate-decl:nameCachePath=${this.declFileNameCachePath}`);
            }
        }

        if (job.contentType === JobContentType.FILE) {
            ets2pandaCmd.push('--output')
            ets2pandaCmd.push((job.content as FileInfo).output)
        } else if (job.contentType === JobContentType.CLUSTER && !incremental) {
            // In case of simultaneous compilation and not incremental the output abc path is pre defined
            ets2pandaCmd.push('--output')
            ets2pandaCmd.push(path.resolve(this.cacheDir, MERGED_INTERMEDIATE_FILE))
        }

        if (this.debugBuild) {
            ets2pandaCmd.push('--debug-info');
            ets2pandaCmd.push('--opt-level=0');
        }

        ets2pandaCmd.push('--ets-warnings:diagnostic-format=build-system');

        if (job.contentType === JobContentType.FILE) {
            ets2pandaCmd.push((job.content as FileInfo).input)
        }

        if (this.dumpPerf) {
            ets2pandaCmd.push('--dump-perf-metrics');
        }

        return ets2pandaCmd
    }

    private formDeclgenCliCmd(
        job: DeclgenV1JobInfo,
    ): string[] {

        const ets2pandaCmd: string[] = [
            '_',
            '--extension',
            'ets',
            '--arktsconfig',
            job.arktsConfig
        ]

        ets2pandaCmd.push('--simultaneous')
        ets2pandaCmd.push('--ets-warnings:base-path=' + this.projectRootPath);

        if (job.contentType === JobContentType.FILE) {
            ets2pandaCmd.push((job.content as FileInfo).input)
        }

        if (this.declFileNameCachePath) {
            ets2pandaCmd.push(`--generate-decl:nameCachePath=${this.declFileNameCachePath}`);
        }

        return ets2pandaCmd
    }

    public compile(
        jobId: string,
        job: CompileJobInfo,
        incremental: boolean = false,
        declGenCb?: () => void,
        compAbcCb?: () => void
    ): void {
        let statsRecorder = new StatisticsRecorder(
            path.resolve(this.cacheDir, BS_PERF_FILE_NAME),
            this.recordType,
            `Compile. Job id: ${jobId.slice(0, 5)}`
        );

        this.logger.printDebug(`Ets2panda.compile Job: ${jobId}`)

        const ets2pandaCmd: string[] = this.formCompileCliCmd(job, incremental);
        this.logger.printDebug('ets2pandaCmd: ' + ets2pandaCmd.join(' '));

        let inputs: string[];
        if (job.contentType === JobContentType.CLUSTER) {
            inputs = (job.content as FileInfo[]).map((fi: FileInfo) => fi.input);
        } else {
            inputs = [(job.content as FileInfo).input];
        }

        let { arkts, arktsGlobal } = this.koalaModule;
        if (!ENABLE_DECLARATION_BARRIER) {
            // WORKAROUND for disabling synchronization
            // start next task not waiting emitting the declaration
            declGenCb?.();
        }
        try {
            statsRecorder.record(formEvent(Ets2pandaEvent.CREATE_INSTANCE));
            arktsGlobal.config = arkts.Config.create(ets2pandaCmd).peer;
            arktsGlobal.compilerContext = arkts.Context.createContextSimultaneousMode(inputs);
            this.pluginDriver.getPluginContext().setArkTSProgram(arktsGlobal.compilerContext.program);
            this.logger.printInfo('[Ets2panda] Created instance');

            statsRecorder.record(formEvent(Ets2pandaEvent.PARSE));
            arkts.proceedToState(arkts.Es2pandaContextState.ES2PANDA_STATE_PARSED, arktsGlobal.compilerContext.peer);
            this.logger.printInfo('[Ets2panda] Parsed');

            // Update output file info
            if (job.contentType === JobContentType.CLUSTER) {
                (job.content as FileInfo[]).forEach((fi: FileInfo) => {
                    fi.output = arkts.formOutputPathForFile(fi.input);
                    validatePathLength(fi.output, 'Output file path');
                })
            } else {
                const fi: FileInfo = job.content as FileInfo;
                fi.output = arkts.formOutputPathForFile(fi.input);
                validatePathLength(fi.output, 'Output file path');
            }

            statsRecorder.record(formEvent(Ets2pandaEvent.PLUGIN_PARSE));
            this.transformImportStatementsWithAliasConfig()
            this.pluginDriver.runPluginHook(PluginHook.PARSED);
            this.logger.printInfo('[Ets2panda] Parser plugins finished');

            statsRecorder.record(formEvent(Ets2pandaEvent.CHECK));
            arkts.proceedToState(arkts.Es2pandaContextState.ES2PANDA_STATE_CHECKED, arktsGlobal.compilerContext.peer);
            this.logger.printInfo('[Ets2panda] Checked');

            if (job.jobType & CompileJobType.DECL) {
                statsRecorder.record(formEvent(Ets2pandaEvent.DECLGEN));
                const contents = job.contentType === JobContentType.CLUSTER
                    ? job.content as FileInfo[]
                    : [job.content as FileInfo];
                for (const fi of contents) {
                    // emit declarations based on relative location of the file in a project,
                    // since es2panda doesn't know about ohos modules right now
                    const relative: string = changeFileExtension(
                        path.relative(job.moduleRoot, fi.input),
                        DECL_ETS_SUFFIX
                    )
                    const declEtsOutputPath: string = path.resolve(
                        this.declgenV2OutDir,
                        relative
                    )
                    ensurePathExists(declEtsOutputPath);
                    // .etscache files are generated separately from .abc file right now
                    validatePathLength(declEtsOutputPath, 'Declaration output path');
                    arkts.generateStaticDeclarationsFromContext(declEtsOutputPath);
                    this.logger.printInfo(`[Ets2panda] Generated 1.2 decl file for ${fi.input}`)
                }
                if (ENABLE_DECLARATION_BARRIER) {
                    declGenCb?.();
                }
            }

            if (job.jobType & CompileJobType.ABC) {
                statsRecorder.record(formEvent(Ets2pandaEvent.PLUGIN_CHECK));
                let ast = arkts.EtsScript.fromContext();
                this.pluginDriver.getPluginContext().setArkTSAst(ast);
                this.pluginDriver.runPluginHook(PluginHook.CHECKED);
                this.logger.printInfo('[Ets2panda] Checker plugins finished');

                statsRecorder.record(formEvent(Ets2pandaEvent.EMIT));
                arkts.proceedToState(
                    arkts.Es2pandaContextState.ES2PANDA_STATE_ASM_GENERATED,
                    arktsGlobal.compilerContext.peer
                );
                arktsGlobal.es2panda._FreeCompilerPartMemory(arktsGlobal.compilerContext.peer);
                arkts.proceedToState(
                    arkts.Es2pandaContextState.ES2PANDA_STATE_BIN_GENERATED,
                    arktsGlobal.compilerContext.peer
                );
                compAbcCb?.();
                this.logger.printInfo(`[Ets2panda] Compiled abc file for cycle ${jobId}`)
            }
        } catch (error) {
            if (error instanceof DriverError) {
                throw error;
            }
            if (error instanceof Error) {
                throw new DriverError(
                    LogDataFactory.newInstance(
                        ErrorCode.BUILDSYSTEM_COMPILE_ABC_FAIL,
                        'Compile abc files failed.',
                        error.message
                    )
                );
            }
        } finally {
            statsRecorder.record(formEvent(Ets2pandaEvent.DESTROY_INSTANCE));
            this.pluginDriver.runPluginHook(PluginHook.CLEAN);
            if (arktsGlobal.compilerContext) {
                arktsGlobal.es2panda._DestroyContext(arktsGlobal.compilerContext.peer);
            }
            arkts.destroyConfig(arktsGlobal.config);
            statsRecorder.record(RecordEvent.END);
            statsRecorder.writeSumSingle();
        }
    }

    public declgenV1(
        jobInfo: DeclgenV1JobInfo,
        skipDeclCheck: boolean,
        genDeclAnnotations: boolean
    ): void {
        const contentFiles: FileInfo[] = jobInfo.contentType === JobContentType.FILE
            ? [jobInfo.content as FileInfo]
            : jobInfo.content as FileInfo[];
        const inputFiles: string[] = contentFiles.map(fi => fi.input);
        const outputDeclEtsPaths: string[] = [];
        const outputEtsPaths: string[] = [];
        for (const file of inputFiles) {
            const { declEtsOutputPath, glueCodeOutputPath } = buildDeclgenOutputPath(
                file, jobInfo.fileToModuleMap[file], this.cacheDir
            );
            outputDeclEtsPaths.push(declEtsOutputPath);
            outputEtsPaths.push(glueCodeOutputPath);
            validatePathLength(declEtsOutputPath, 'Declaration file path');
            validatePathLength(glueCodeOutputPath, 'Bridge code file path');
        }

        const firstFileModule = jobInfo.fileToModuleMap[inputFiles[0]];
        const staticRecordPath = path.join(
            firstFileModule.declgenV1OutPath!,
            STATIC_RECORD_FILE
        )
        validatePathLength(staticRecordPath, 'Static record file path');
        const declEtsOutputDir = path.dirname(outputDeclEtsPaths[0]);
        const staticRecordRelativePath = changeFileExtension(
            path.relative(declEtsOutputDir, staticRecordPath).replace(/\\/g, '\/'),
            '',
            DECL_TS_SUFFIX
        );
        createFileIfNotExists(staticRecordPath, STATIC_RECORD_FILE_CONTENT);
        let ets2pandaCmd = this.formDeclgenCliCmd(jobInfo)
        this.logger.printDebug(`ets2panda cmd: ${ets2pandaCmd.join(' ')}`)

        let { arkts, arktsGlobal } = this.koalaModule;
        let declgen: any = null;
        try {
            arktsGlobal.config = arkts.Config.create(ets2pandaCmd).peer;
            arktsGlobal.compilerContext = arkts.Context.createContextSimultaneousMode(
                inputFiles
            );
            this.pluginDriver.getPluginContext().setArkTSProgram(arktsGlobal.compilerContext.program);
            arkts.proceedToState(arkts.Es2pandaContextState.ES2PANDA_STATE_PARSED, arktsGlobal.compilerContext.peer, skipDeclCheck);
            declgen = arkts.createTsDeclgen(
                inputFiles,
                outputDeclEtsPaths,
                outputEtsPaths,
                false,
                false,
                staticRecordRelativePath,
                genDeclAnnotations
            );
            let ast = arkts.EtsScript.fromContext();
            this.pluginDriver.getPluginContext().setArkTSAst(ast);
            this.pluginDriver.runPluginHook(PluginHook.PARSED);
            arkts.generateTsDeclarationsAfterParsed(declgen);

            arkts.proceedToState(arkts.Es2pandaContextState.ES2PANDA_STATE_CHECKED, arktsGlobal.compilerContext.peer, skipDeclCheck);
            ast = arkts.EtsScript.fromContext();
            this.pluginDriver.getPluginContext().setArkTSAst(ast);
            this.pluginDriver.runPluginHook(PluginHook.CHECKED);
            arkts.generateTsDeclarationsAfterCheck(declgen);

            arkts.writeTsDeclarations(declgen);

            this.logger.printInfo(`[Ets2panda] Generated 1.0 declaration file for ${inputFiles[0]}`)
        } catch (error) {
            if (error instanceof Error) {
                throw new DriverError(
                    LogDataFactory.newInstance(
                        ErrorCode.BUILDSYSTEM_DECLGEN_FAIL,
                        'Failed to generate 1.0 declaration file.',
                        error.message,
                        inputFiles[0]
                    )
                );
            }
        } finally {
            this.pluginDriver.runPluginHook(PluginHook.CLEAN);
            if (declgen) {
                arkts.destroyTsDeclgen(declgen);
            }
            if (arktsGlobal.compilerContext) {
                arktsGlobal.es2panda._DestroyContext(arktsGlobal.compilerContext.peer);
            }
            arkts.destroyConfig(arktsGlobal.config);
        }
    }

    public extractDeclarationsFromAbcFile(abcFile: string, cacheDir: string) {
        let { arkts } = this.koalaModule;
        arkts.ExtractDeclarationsFromAbcFile(abcFile, cacheDir);
    }
}