// Copyright (c) 2022, Compiler Explorer Authors
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//     * Redistributions of source code must retain the above copyright notice,
//       this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above copyright
//       notice, this list of conditions and the following disclaimer in the
//       documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

import fs from 'node:fs/promises';
import path from 'node:path';

import type {
    CompilationResult,
    ExecutionOptions,
    ExecutionOptionsWithEnv,
} from '../../types/compilation/compilation.interfaces.js';
import type {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces.js';
import {BaseCompiler} from '../base-compiler.js';
import * as exec from '../exec.js';
import {logger} from '../logger.js';
import * as utils from '../utils.js';

interface ASICSelection {
    asic?: string;
    error?: string;
    printASICs?: boolean;
}

// RGA := Radeon GPU Analyzer
export class RGACompiler extends BaseCompiler {
    private dxcPath: string;

    static get key() {
        return 'rga';
    }

    constructor(info: any, env: any) {
        super(info, env);

        this.compiler.supportsIntel = false;
        this.dxcPath = this.compilerProps<string>(`compiler.${this.compiler.id}.dxcPath`);
        logger.debug(`RGA compiler ${this.compiler.id} configured to use DXC at ${this.dxcPath}`);
    }

    override optionsForFilter(filters: ParseFiltersAndOutputOptions, outputFilename: any, userOptions?: any): any[] {
        return [outputFilename];
    }

    extractASIC(dxcArgs: string[]): ASICSelection {
        // Scan dxc args for an `--asic` argument that should be stripped and passed later to RGA
        // Default to RDNA2
        let asic = 'gfx1030';
        let printASICs = true;
        for (let i = 0; i !== dxcArgs.length; ++i) {
            const arg = dxcArgs[i];
            if (arg === '--asic') {
                // NOTE: the last arguments are the input source file and -spirv, so check
                // if --asic immediately precedes that
                if (i === dxcArgs.length - 3) {
                    return {
                        error: '--asic flag supplied without subsequent ASIC!',
                    };
                }
                asic = dxcArgs[i + 1];
                // Do a quick sanity check to determine if a valid ASIC was supplied
                if (!asic.startsWith('gfx')) {
                    return {
                        error: `The argument immediately following --asic doesn't appear to be a valid ASIC.
Please supply an ASIC from the following options:`,
                        printASICs: true,
                    };
                }
                // Remove these two arguments from the dxcArgs list
                dxcArgs.splice(i, 2);

                // If the user supplied a specific ASIC, don't bother printing available ASIC options
                printASICs = false;
                break;
            }
        }

        return {
            asic,
            printASICs,
        };
    }

    override async runCompiler(
        compiler: string,
        options: string[],
        inputFilename: string,
        execOptions: ExecutionOptionsWithEnv,
    ): Promise<CompilationResult> {
        if (!execOptions) {
            execOptions = this.getDefaultExecOptions();
        }

        if (!execOptions.customCwd) {
            execOptions.customCwd = path.dirname(inputFilename);
        }

        const result = await this.execDXCandRGA(compiler, options, execOptions);
        return this.transformToCompilationResult(result, inputFilename);
    }

    async execDXCandRGA(filepath: string, args: string[], execOptions: ExecutionOptions): Promise<any> {
        // RGA is invoked in two steps. First, DXC is invoked to compile the SPIR-V output of the HLSL file.
        // Next, RGA is invoked to consume the SPIR-V output and produce the requested ISA.

        // Track the total time spent instead of relying on executeDirect's internal timing facility
        const startTime = process.hrtime.bigint();

        // The first argument is the target output file
        const outputFile = args[0];
        const outputDir = path.dirname(outputFile);
        const spvTemp = 'output.spv.txt';
        logger.debug(`Intermediate SPIR-V output: ${spvTemp}`);

        const dxcArgs = args.slice(1);
        if (!dxcArgs.includes('-spirv')) {
            dxcArgs.push('-spirv');
        }
        logger.debug(`DXC args: ${dxcArgs}`);

        const asicSelection = this.extractASIC(dxcArgs);
        if (asicSelection.error) {
            // Invalid user ASIC selected, bail out immediately
            const endTime = process.hrtime.bigint();

            // Synthesize a faux-execution result (see promise resolution code in executeDirect)
            return {
                code: -1,
                okToCache: true,
                filenameTransform: (x: string) => x,
                stdout: asicSelection.error,
                execTime: utils.deltaTimeNanoToMili(startTime, endTime),
            };
        }

        const dxcResult = await exec.execute(this.dxcPath, dxcArgs, execOptions);
        if (dxcResult.code !== 0) {
            // Failed to compile SPIR-V intermediate product. Exit immediately with DXC invocation result.
            const endTime = process.hrtime.bigint();
            dxcResult.execTime = utils.deltaTimeNanoToMili(startTime, endTime);
            return dxcResult;
        }

        try {
            await fs.writeFile(path.join(outputDir, spvTemp), dxcResult.stdout);
        } catch {
            const endTime = process.hrtime.bigint();
            return {
                code: -1,
                okToCache: true,
                filenameTransform: (x: string) => x,
                stdout: 'Failed to emit intermediate SPIR-V result.',
                execTime: utils.deltaTimeNanoToMili(startTime, endTime),
            };
        }

        let registerAnalysisFile = 'livereg.txt';
        const rgaArgs = [
            '-s',
            'vk-spv-txt-offline',
            '-c',
            asicSelection.asic || '',
            '--isa',
            outputFile,
            '--livereg',
            registerAnalysisFile,
            spvTemp,
        ];
        logger.debug(`RGA args: ${rgaArgs}`);

        const rgaResult = await exec.execute(filepath, rgaArgs, execOptions);
        if (rgaResult.code !== 0) {
            // Failed to compile AMD ISA
            const endTime = process.hrtime.bigint();
            rgaResult.execTime = utils.deltaTimeNanoToMili(startTime, endTime);
            return rgaResult;
        }

        // RGA doesn't emit the exact file we requested. It prepends the requested GPU
        // architecture and appends the shader type (with underscore separators). Here,
        // we rename the generated file to the output file Compiler Explorer expects.

        const files = await fs.readdir(outputDir, {encoding: 'utf8'});
        for (const file of files) {
            if (file.startsWith((asicSelection.asic as string) + '_output')) {
                await fs.rename(path.join(outputDir, file), outputFile);

                registerAnalysisFile = path.join(outputDir, file.replace('output', 'livereg').replace('.s', '.txt'));
                // The register analysis file contains a legend, and register liveness data
                // for each line of disassembly. Interleave those lines into the final output
                // as assembly comments.
                const asm = await fs.readFile(outputFile, 'utf8');
                const asmLines = asm.split(/\r?\n/);
                const analysis = await fs.readFile(registerAnalysisFile, 'utf8');
                const analysisLines = analysis.split(/\r?\n/);

                // The first few lines of the register analysis are the legend. Emit those lines
                // as comments at the start of the output.
                let analysisOffset = analysisLines.indexOf('');
                analysisOffset += 3;
                const epilogueOffset = analysisLines.indexOf('', analysisOffset);
                const outputAsm = analysisLines.slice(epilogueOffset + 1).map(line => `; ${line}`);
                outputAsm.push(...analysisLines.slice(0, analysisOffset).map(line => `; ${line}`), '\n');

                let asmOffset = asmLines.indexOf('');
                outputAsm.push(...asmLines.slice(0, asmOffset));
                asmOffset += 1;

                // Perform the interleave
                for (let i = 0; i !== asmOffset + asmLines.length; ++i) {
                    if (i + analysisOffset >= epilogueOffset) {
                        outputAsm.push(...asmLines.slice(i));
                        break;
                    }

                    // Check if this line of assembly corresponds to a label. If so, emit the asm line
                    // and continue from the next line (the register analysis file operates on instructions,
                    // and labels are not considered instructions)
                    if (asmLines[i + asmOffset].startsWith('label')) {
                        outputAsm.push(asmLines[i + asmOffset]);
                        ++asmOffset;
                        --i;
                        continue;
                    }

                    outputAsm.push(`; ${analysisLines[i + analysisOffset]}`, asmLines[i + asmOffset]);
                }

                await fs.writeFile(outputFile, outputAsm.join('\n'));

                if (asicSelection.printASICs) {
                    rgaResult.stdout += `ISA compiled with the default AMD ASIC (Radeon RX 6800 series RDNA2).
To override this, pass --asic [ASIC] to the options above (nonstandard DXC option),
where [ASIC] corresponds to one of the following options:`;

                    const asics = await exec.execute(filepath, ['-s', 'vk-spv-txt-offline', '-l'], execOptions);
                    rgaResult.stdout += '\n';
                    rgaResult.stdout += asics.stdout;
                }

                const endTime = process.hrtime.bigint();
                rgaResult.execTime = utils.deltaTimeNanoToMili(startTime, endTime);
                return rgaResult;
            }
        }

        // Arriving here means the expected ISA result wasn't emitted. Synthesize an error.
        const endTime = process.hrtime.bigint();
        rgaResult.execTime = utils.deltaTimeNanoToMili(startTime, endTime);
        rgaResult.stdout += `\nRGA didn't emit expected ISA output.`;
        return rgaResult;
    }
}
