// Copyright (c) 2021, 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 {PreliminaryCompilerInfo} from '../../types/compiler.interfaces.js';
import {ExecutableExecutionOptions} from '../../types/execution/execution.interfaces.js';
import type {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces.js';
import {LanguageKey} from '../../types/languages.interfaces.js';
import {BaseCompiler} from '../base-compiler.js';
import {CompilationEnvironment} from '../compilation-env.js';
import {AssemblyName, DotnetExtraConfiguration} from '../execution/dotnet-execution-env.js';
import {IExecutionEnvironment} from '../execution/execution-env.interfaces.js';
import {DotNetAsmParser} from '../parsers/asm-parser-dotnet.js';
import {DotNetPdbParser, type DotNetSourceMapping} from '../parsers/pdb-parser-dotnet.js';
import * as utils from '../utils.js';

type DotNetCompilationResult = CompilationResult & {dotnetSourceMapping?: DotNetSourceMapping};

class DotNetCompiler extends BaseCompiler {
    private readonly sdkBaseDir: string;
    private readonly buildConfig: string;
    private readonly clrBuildDir: string;
    private readonly langVersion: string;
    private readonly corerunPath: string;
    private readonly disassemblyLoaderPath: string;
    private readonly crossgen2Path: string;
    private readonly ilcPath: string;
    private readonly ildasmPath: string;
    private readonly toolsPath: string;

    constructor(compilerInfo: PreliminaryCompilerInfo, env: CompilationEnvironment) {
        super(compilerInfo, env);

        this.sdkBaseDir = path.join(path.dirname(compilerInfo.exe), 'sdk');

        this.buildConfig = this.compilerProps<string>(`compiler.${this.compiler.id}.buildConfig`);
        this.clrBuildDir = this.compilerProps<string>(`compiler.${this.compiler.id}.clrDir`);
        this.langVersion = this.compilerProps<string>(`compiler.${this.compiler.id}.langVersion`);

        this.corerunPath = path.join(this.clrBuildDir, 'corerun');
        this.crossgen2Path = path.join(this.clrBuildDir, 'crossgen2', 'crossgen2');
        this.ilcPath = path.join(this.clrBuildDir, 'ilc-published', 'ilc');
        this.ildasmPath = path.join(this.clrBuildDir, 'ildasm');
        this.toolsPath = path.join(this.clrBuildDir, 'dotnet-tools', '.store');

        this.asm = new DotNetAsmParser();
        this.disassemblyLoaderPath = path.join(this.clrBuildDir, 'DisassemblyLoader', 'DisassemblyLoader.dll');
    }

    async getCompilerInfo(lang: LanguageKey): Promise<DotNetCompilerInfo> {
        const sdkDirs = await fs.readdir(this.sdkBaseDir);
        const sdkVersion = sdkDirs[0];

        const parts = sdkVersion.split('.');
        const targetFramework = `net${parts[0]}.${parts[1]}`;
        const sdkMajorVersion = Number(parts[0]);

        switch (lang) {
            case 'csharp': {
                return {
                    targetFramework: targetFramework,
                    sdkVersion: sdkVersion,
                    sdkMajorVersion: sdkMajorVersion,
                    compilerPath: path.join(this.sdkBaseDir, sdkVersion, 'Roslyn', 'bincore', 'csc.dll'),
                };
            }
            case 'vb': {
                return {
                    targetFramework: targetFramework,
                    sdkVersion: sdkVersion,
                    sdkMajorVersion: sdkMajorVersion,
                    compilerPath: path.join(this.sdkBaseDir, sdkVersion, 'Roslyn', 'bincore', 'vbc.dll'),
                };
            }
            case 'fsharp': {
                return {
                    targetFramework: targetFramework,
                    sdkVersion: sdkVersion,
                    sdkMajorVersion: sdkMajorVersion,
                    compilerPath: path.join(this.sdkBaseDir, sdkVersion, 'FSharp', 'fsc.dll'),
                };
            }
            case 'il': {
                return {
                    targetFramework: targetFramework,
                    sdkVersion: sdkVersion,
                    sdkMajorVersion: sdkMajorVersion,
                    compilerPath: path.join(this.clrBuildDir, 'ilasm'),
                };
            }
            default: {
                throw new Error(`Unsupported language: ${lang}`);
            }
        }
    }

    setCompilerExecOptions(execOptions: ExecutionOptionsWithEnv, programDir: string) {
        if (!execOptions) {
            execOptions = this.getDefaultExecOptions();
        }

        // See https://github.com/dotnet/runtime/issues/50391 - the .NET runtime tries to make a 2TB memfile if we have
        // this feature enabled (which is on by default on .NET 7) This blows out our nsjail sandbox limit, so for now
        // we disable it.
        execOptions.env.DOTNET_EnableWriteXorExecute = '0';
        // Disable any phone-home.
        execOptions.env.DOTNET_CLI_TELEMETRY_OPTOUT = 'true';
        // Some versions of .NET complain if they can't work out what the user's directory is. We force it to the output
        // directory here.
        execOptions.env.DOTNET_CLI_HOME = programDir;
        execOptions.env.DOTNET_ROOT = path.join(this.clrBuildDir, '.dotnet');
        // Try to be less chatty
        execOptions.env.DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 'true';
        execOptions.env.DOTNET_NOLOGO = 'true';

        execOptions.maxOutput = 1024 * 1024 * 1024;
        execOptions.timeoutMs = 30000;

        execOptions.customCwd = programDir;
    }

    override async buildExecutable(
        compiler: string,
        options: string[],
        inputFilename: string,
        execOptions: ExecutionOptionsWithEnv,
    ) {
        const compilerInfo = await this.getCompilerInfo(this.lang.id);
        const compilerOptions = this.extractCompilerOptions(options);
        return await this.buildToDll(compiler, compilerInfo, inputFilename, execOptions, compilerOptions, true);
    }

    extractCompilerOptions(options: string[]): string[] {
        const compilerOptions: string[] = [];
        while (options.length > 0) {
            const currentOption = options.shift();
            if (!currentOption) {
                continue;
            }
            if (currentOption === '-compopt' || currentOption === '--compiler-options') {
                const value = options.shift();
                if (value) {
                    compilerOptions.push(value);
                }
            }
        }
        return compilerOptions;
    }

    async buildToDll(
        dotnetPath: string,
        compilerInfo: DotNetCompilerInfo,
        inputFilename: string,
        execOptions: ExecutionOptionsWithEnv,
        compilerOptions: string[],
        buildToBinary?: boolean,
    ): Promise<CompilationResult> {
        const programDir = path.dirname(inputFilename);
        const programOutputPath = path.join(programDir, 'bin', this.buildConfig, compilerInfo.targetFramework);
        await fs.mkdir(programOutputPath, {recursive: true});
        const outputFilename = path.join(programOutputPath, 'CompilerExplorer.dll');
        this.setCompilerExecOptions(execOptions, programDir);
        let compilerResult: CompilationResult;

        switch (this.lang.id) {
            case 'csharp': {
                compilerResult = await this.runCsc(
                    dotnetPath,
                    compilerInfo,
                    inputFilename,
                    outputFilename,
                    execOptions,
                    compilerOptions,
                    buildToBinary,
                );
                break;
            }
            case 'vb': {
                compilerResult = await this.runVbc(
                    dotnetPath,
                    compilerInfo,
                    inputFilename,
                    outputFilename,
                    execOptions,
                    compilerOptions,
                    buildToBinary,
                );
                break;
            }
            case 'fsharp': {
                compilerResult = await this.runFsc(
                    dotnetPath,
                    compilerInfo,
                    inputFilename,
                    outputFilename,
                    execOptions,
                    compilerOptions,
                    buildToBinary,
                );
                break;
            }
            case 'il': {
                compilerResult = await this.runIlAsm(
                    compilerInfo.compilerPath,
                    inputFilename,
                    outputFilename,
                    execOptions,
                    compilerOptions,
                    buildToBinary,
                );
                break;
            }
            default: {
                throw new Error(`Unsupported language: ${this.lang.id}`);
            }
        }

        if (compilerResult.code === 0) {
            await utils.ensureFileExists(this.getOutputFilename(programDir, this.outputFilebase));
        }
        return compilerResult;
    }

    async getRefAssembliesAndAnalyzers(dotnetPath: string, compilerInfo: DotNetCompilerInfo, lang: LanguageKey) {
        const packDir = path.join(path.dirname(dotnetPath), 'packs', 'Microsoft.NETCore.App.Ref');
        const packVersion = (await fs.readdir(packDir))[0];
        let refDir = path.join(packDir, packVersion, 'ref', compilerInfo.targetFramework);
        if (!(await utils.dirExists(refDir))) {
            // During the transition of major versions, we may have a short window where the pack version hasn't been updated
            const parts = compilerInfo.sdkVersion.split('.');
            const previousTargetFramework = `net${Number(parts[0]) - 1}.${parts[1]}`;
            refDir = path.join(packDir, packVersion, 'ref', previousTargetFramework);
        }
        const refAssemblies = (await fs.readdir(refDir)).filter(f => f.endsWith('.dll')).map(f => path.join(refDir, f));
        const analyzers: string[] = [];
        const analyzersDir = path.join(
            this.sdkBaseDir,
            compilerInfo.sdkVersion,
            'Sdks',
            'Microsoft.NET.Sdk',
            'analyzers',
        );
        switch (lang) {
            case 'csharp': {
                const generatorsDir = path.join(packDir, packVersion, 'analyzers', 'dotnet', 'cs');
                if (await utils.dirExists(generatorsDir)) {
                    analyzers.push(
                        ...(await fs.readdir(generatorsDir))
                            .filter(f => f.endsWith('.dll'))
                            .map(f => path.join(generatorsDir, f)),
                    );
                }
                analyzers.push(
                    path.join(analyzersDir, 'Microsoft.CodeAnalysis.NetAnalyzers.dll'),
                    path.join(analyzersDir, 'Microsoft.CodeAnalysis.CSharp.NetAnalyzers.dll'),
                );
                break;
            }
            case 'vb': {
                analyzers.push(
                    path.join(analyzersDir, 'Microsoft.CodeAnalysis.NetAnalyzers.dll'),
                    path.join(analyzersDir, 'Microsoft.CodeAnalysis.VisualBasic.NetAnalyzers.dll'),
                );
                break;
            }
        }
        refAssemblies.push(this.disassemblyLoaderPath);
        return {refAssemblies, analyzers};
    }

    getPreprocessorDefines(sdkMajorVersion: number, lang: LanguageKey) {
        const defines = [
            'TRACE',
            'NET',
            `NET${sdkMajorVersion}_0`,
            'RELEASE',
            'NETCOREAPP',
            'NETCOREAPP1_0_OR_GREATER',
            'NETCOREAPP1_1_OR_GREATER',
            'NETCOREAPP2_0_OR_GREATER',
            'NETCOREAPP2_1_OR_GREATER',
            'NETCOREAPP2_2_OR_GREATER',
            'NETCOREAPP3_0_OR_GREATER',
            'NETCOREAPP3_1_OR_GREATER',
        ];

        for (let version = sdkMajorVersion; version >= 5; version--) {
            defines.push(`NET${version}_0_OR_GREATER`);
        }

        if (lang === 'vb') {
            const vbDefines = defines.map(d => `${d}=-1`);
            vbDefines.push('CONFIG="Release"', 'PLATFORM="AnyCPU"', '_MyType="Empty"');
            return vbDefines;
        }

        return defines;
    }

    async runCsc(
        dotnetPath: string,
        compilerInfo: DotNetCompilerInfo,
        inputFilename: string,
        outputFilename: string,
        execOptions: ExecutionOptionsWithEnv,
        compilerOptions: string[],
        buildToBinary?: boolean,
    ) {
        const {refAssemblies, analyzers} = await this.getRefAssembliesAndAnalyzers(dotnetPath, compilerInfo, 'csharp');
        const defines = this.getPreprocessorDefines(compilerInfo.sdkMajorVersion, 'csharp');
        const options = [
            '-nologo',
            `-target:${buildToBinary ? 'exe' : 'library'}`,
            '-filealign:512',
            '-unsafe+',
            '-checked-',
            '-nowarn:1701,1702',
            '-fullpaths',
            '-nostdlib+',
            '-errorreport:prompt',
            '-warn:9',
            '-highentropyva+',
            '-nullable:enable',
            '-debug:portable',
            '-optimize+',
            '-warnaserror-',
            '-utf8output',
            '-deterministic+',
            `-langversion:${this.langVersion}`,
            '-warnaserror+:NU1605,SYSLIB0011',
            ...compilerOptions,
        ];
        for (const analyzer of analyzers) {
            options.push(`-analyzer:${analyzer}`);
        }
        for (const refAssembly of refAssemblies) {
            options.push(`-reference:${refAssembly}`);
        }

        const assemblyInfo = `using System;
using System.Reflection;
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute\
(".NETCoreApp,Version=v${compilerInfo.sdkMajorVersion}.0",\
 FrameworkDisplayName = ".NET ${compilerInfo.sdkMajorVersion}.0")]
[assembly: System.Reflection.AssemblyCompanyAttribute("CompilerExplorer")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
[assembly: System.Reflection.AssemblyProductAttribute("CompilerExplorer")]
[assembly: System.Reflection.AssemblyTitleAttribute("CompilerExplorer")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
`;
        const programDir = path.dirname(inputFilename);
        const assemblyInfoPath = path.join(programDir, 'AssemblyInfo.cs');
        await fs.writeFile(assemblyInfoPath, assemblyInfo);

        options.push(`-define:${defines.join(';')}`, `-out:${outputFilename}`, inputFilename, assemblyInfoPath);
        return await super.runCompiler(dotnetPath, [compilerInfo.compilerPath, ...options], inputFilename, execOptions);
    }

    async runVbc(
        dotnetPath: string,
        compilerInfo: DotNetCompilerInfo,
        inputFilename: string,
        outputFilename: string,
        execOptions: ExecutionOptionsWithEnv,
        compilerOptions: string[],
        buildToBinary?: boolean,
    ) {
        const {refAssemblies, analyzers} = await this.getRefAssembliesAndAnalyzers(dotnetPath, compilerInfo, 'vb');
        const defines = this.getPreprocessorDefines(compilerInfo.sdkMajorVersion, 'vb');
        const options = [
            '-nologo',
            `-target:${buildToBinary ? 'exe' : 'library'}`,
            '-filealign:512',
            `-imports:Microsoft.VisualBasic,System,System.Collections,System.Collections.Generic,\
System.Diagnostics,System.Linq,System.Xml.Linq,System.Threading.Tasks`,
            '-optioncompare:Binary',
            '-optionexplicit+',
            '-optionstrict:custom',
            '-nowarn:41999,42016,42017,42018,42019,42020,42021,42022,42032,42036',
            '-nosdkpath',
            '-optioninfer+',
            '-nostdlib',
            '-errorreport:prompt',
            '-rootnamespace:CompilerExplorer',
            '-highentropyva+',
            '-debug:portable',
            '-optimize+',
            '-warnaserror-',
            '-utf8output',
            '-deterministic+',
            `-langversion:${this.langVersion}`,
            '-warnaserror+:NU1605,SYSLIB0011',
            ...compilerOptions,
        ];
        for (const analyzer of analyzers) {
            options.push(`-analyzer:${analyzer}`);
        }
        for (const refAssembly of refAssemblies) {
            options.push(`-reference:${refAssembly}`);
        }

        const vbRuntime = refAssemblies.find(f => f.includes('Microsoft.VisualBasic.dll'));
        if (vbRuntime) {
            options.push(`-vbruntime:${vbRuntime}`);
        }

        const assemblyInfo = `Option Strict Off
Option Explicit On

Imports System
Imports System.Reflection
<Assembly: Global.System.Runtime.Versioning.TargetFrameworkAttribute\
(".NETCoreApp,Version=v${compilerInfo.sdkMajorVersion}.0",\
 FrameworkDisplayName:=".NET ${compilerInfo.sdkMajorVersion}.0")>
<Assembly: System.Reflection.AssemblyCompanyAttribute("CompilerExplorer"),  _
 Assembly: System.Reflection.AssemblyConfigurationAttribute("Release"),  _
 Assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0"),  _
 Assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0"),  _
 Assembly: System.Reflection.AssemblyProductAttribute("CompilerExplorer"),  _
 Assembly: System.Reflection.AssemblyTitleAttribute("CompilerExplorer"),  _
 Assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")>
`;
        const programDir = path.dirname(inputFilename);
        const assemblyInfoPath = path.join(programDir, 'AssemblyInfo.vb');
        await fs.writeFile(assemblyInfoPath, assemblyInfo);

        options.push(`-define:${defines.join(',')}`, `-out:${outputFilename}`, inputFilename, assemblyInfoPath);
        return await super.runCompiler(dotnetPath, [compilerInfo.compilerPath, ...options], inputFilename, execOptions);
    }

    async runFsc(
        dotnetPath: string,
        compilerInfo: DotNetCompilerInfo,
        inputFilename: string,
        outputFilename: string,
        execOptions: ExecutionOptionsWithEnv,
        compilerOptions: string[],
        buildToBinary?: boolean,
    ) {
        const {refAssemblies} = await this.getRefAssembliesAndAnalyzers(dotnetPath, compilerInfo, 'fsharp');
        const defines = this.getPreprocessorDefines(compilerInfo.sdkMajorVersion, 'fsharp');
        const options = [
            '--nologo',
            `--target:${buildToBinary ? 'exe' : 'library'}`,
            `--langversion:${this.langVersion}`,
            '--noframework',
            '--optimize+',
            '--utf8output',
            '--warn:3',
            '--warnaserror:3239',
            '--fullpaths',
            '--flaterrors',
            '--debug:portable',
            '--highentropyva+',
            '--targetprofile:netcore',
            '--nocopyfsharpcore',
            '--deterministic+',
            '--simpleresolution',
            ...compilerOptions,
        ];
        for (const refAssembly of refAssemblies) {
            options.push(`-r:${refAssembly}`);
        }

        const versionInfo = `namespace Microsoft.BuildSettings
[<System.Runtime.Versioning.TargetFrameworkAttribute\
(".NETCoreApp,Version=v${compilerInfo.sdkMajorVersion}.0",\
 FrameworkDisplayName=".NET ${compilerInfo.sdkMajorVersion}.0")>]
do ()
`;
        const assemblyInfo = `namespace FSharp

open System
open System.Reflection

[<assembly: System.Reflection.AssemblyCompanyAttribute("CompilerExplorer")>]
[<assembly: System.Reflection.AssemblyConfigurationAttribute("Release")>]
[<assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")>]
[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")>]
[<assembly: System.Reflection.AssemblyProductAttribute("CompilerExplorer")>]
[<assembly: System.Reflection.AssemblyTitleAttribute("CompilerExplorer")>]
[<assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")>]
do()
`;
        const programDir = path.dirname(inputFilename);
        const versionInfoPath = path.join(programDir, 'VersionInfo.fs');
        const assemblyInfoPath = path.join(programDir, 'AssemblyInfo.fs');
        await fs.writeFile(versionInfoPath, versionInfo);
        await fs.writeFile(assemblyInfoPath, assemblyInfo);

        options.push(
            ...defines.map(d => `--define:${d}`),
            `-o:${outputFilename}`,
            `-r:${path.join(path.dirname(compilerInfo.compilerPath), 'FSharp.Core.dll')}`,
            versionInfoPath,
            assemblyInfoPath,
            inputFilename,
        );
        return await super.runCompiler(dotnetPath, [compilerInfo.compilerPath, ...options], inputFilename, execOptions);
    }

    async runIlAsm(
        ilasmPath: string,
        inputFilename: string,
        outputFilename: string,
        execOptions: ExecutionOptionsWithEnv,
        compilerOptions: string[],
        buildToBinary?: boolean,
    ) {
        const assemblyInfo = `.assembly extern DisassemblyLoader { }
.assembly CompilerExplorer
{
    .ver 1:0:0:0
}
.module CompilerExplorer.dll
#include "${path.basename(inputFilename)}"
`;

        const programDir = path.dirname(inputFilename);
        const assemblyInfoPath = path.join(programDir, 'AssemblyInfo.il');
        await fs.writeFile(assemblyInfoPath, assemblyInfo);
        const options = [
            '-nologo',
            '-quiet',
            '-optimize',
            '-debug:OPT',
            buildToBinary ? '-exe' : '-dll',
            assemblyInfoPath,
            `-include:${programDir}`,
            `-output:${outputFilename}`,
            ...compilerOptions,
        ];
        return await super.runCompiler(ilasmPath, options, inputFilename, execOptions);
    }

    override getSharedLibraryPathsAsArguments() {
        return [];
    }

    // eslint-disable-next-line max-statements
    override async runCompiler(
        compiler: string,
        options: string[],
        inputFilename: string,
        execOptions: ExecutionOptionsWithEnv,
        filters: ParseFiltersAndOutputOptions,
    ): Promise<CompilationResult> {
        const compilerInfo = await this.getCompilerInfo(this.lang.id);
        const programDir = path.dirname(inputFilename);
        const programOutputPath = path.join(programDir, 'bin', this.buildConfig, compilerInfo.targetFramework);
        const programDllPath = path.join(programOutputPath, 'CompilerExplorer.dll');
        const envVarFileContents = ['DOTNET_EnableWriteXorExecute=0'];
        const isIlDasm = this.compiler.group === 'dotnetildasm';
        const isIlSpy = this.compiler.group === 'dotnetilspy';
        const isCoreRun = this.compiler.group === 'dotnetcoreclr';
        const toolOptions: string[] = [];
        const compilerOptions = this.extractCompilerOptions([...options]);

        let overrideDiffable = false;
        let overrideDisasm = false;
        let overrideAssembly = false;
        let overrideTiered = false;
        let isAot = this.compiler.group === 'dotnetnativeaot';
        let isMono = this.compiler.group === 'dotnetmono';
        let isCrossgen2 =
            this.compiler.group === 'dotnetcrossgen2' ||
            (this.compiler.group === 'dotnetlegacy' && compilerInfo.sdkMajorVersion === 6);
        let codegenArch = 'x64';

        while (options.length > 0) {
            const currentOption = options.shift();
            if (!currentOption) {
                continue;
            }

            // options before the input filename are user options
            if (currentOption.includes(path.basename(inputFilename))) {
                break;
            }

            if (currentOption === '-compopt' || currentOption === '--compiler-options') {
                options.shift(); // skip the value for this option, we don't need it
                continue;
            }

            if ((isCoreRun || isMono) && (currentOption === '-e' || currentOption === '--env')) {
                const envVar = options.shift();
                if (envVar) {
                    const [name] = envVar.split('=');
                    const normalizedName = name.trim().toUpperCase();
                    if (normalizedName === 'DOTNET_TIEREDCOMPILATION') {
                        overrideTiered = true;
                    }
                    if (normalizedName === 'DOTNET_JITDISASM') {
                        overrideDisasm = true;
                    }
                    if (normalizedName === 'DOTNET_JITDISASMASSEMBLIES') {
                        overrideAssembly = true;
                    }
                    if (normalizedName === 'DOTNET_JITDIFFABLEDASM' || normalizedName === 'DOTNET_JITDISASMDIFFABLE') {
                        overrideDiffable = true;
                    }
                    envVarFileContents.push(envVar);
                }
            } else {
                if (this.compiler.group === 'dotnetlegacy') {
                    if (currentOption === '--aot') {
                        isAot = true;
                    } else if (currentOption === '--crossgen2') {
                        isCrossgen2 = true;
                    } else if (currentOption === '--mono') {
                        isMono = true;
                    } else {
                        toolOptions.push(currentOption);
                    }
                } else {
                    toolOptions.push(currentOption);
                    if (currentOption === '--codegenopt' || currentOption === '--codegen-options') {
                        const value = options.shift();
                        if (value) {
                            toolOptions.push(value);
                            const normalizedValue = value.trim().toUpperCase();
                            if (normalizedValue.startsWith('TIEREDCOMPILATION=')) {
                                overrideTiered = true;
                            }
                            if (normalizedValue.startsWith('JITDISASM=')) {
                                overrideDisasm = true;
                            }
                            if (normalizedValue.startsWith('JITDISASMASSEMBLIES=')) {
                                overrideAssembly = true;
                            }
                            if (
                                normalizedValue.startsWith('JITDIFFABLEDASM=') ||
                                normalizedValue.startsWith('JITDISASMDIFFABLE=')
                            ) {
                                overrideDiffable = true;
                            }
                        }
                    }
                    if (currentOption === '--targetarch') {
                        const value = options.shift();
                        if (value) {
                            toolOptions.push(value);
                            codegenArch = value.trim().toLowerCase();
                        }
                    }
                }
            }
        }

        const needCodegenOptions = isCrossgen2 || isAot;

        if (needCodegenOptions) {
            toolOptions.push('--parallelism', '1');
        }

        if (!isIlDasm && !isIlSpy) {
            if (!overrideDiffable && compilerInfo.sdkMajorVersion < 8) {
                if (needCodegenOptions) {
                    toolOptions.push('--codegenopt', 'JitDiffableDasm=1');
                }
                envVarFileContents.push('DOTNET_JitDiffableDasm=1');
            }

            if (!overrideDisasm) {
                if (needCodegenOptions) {
                    toolOptions.push(
                        '--codegenopt',
                        compilerInfo.sdkMajorVersion === 6 ? 'NgenDisasm=*' : 'JitDisasm=*',
                    );
                }
                envVarFileContents.push('DOTNET_JitDisasm=*');
            }

            if (!overrideAssembly) {
                if (needCodegenOptions && compilerInfo.sdkMajorVersion >= 9) {
                    toolOptions.push('--codegenopt', 'JitDisasmAssemblies=CompilerExplorer');
                }
                envVarFileContents.push('DOTNET_JitDisasmAssemblies=CompilerExplorer');
            }

            if (!overrideTiered) {
                envVarFileContents.push('DOTNET_TieredCompilation=0');
            }
        }

        const mapDebugInfo = (isCoreRun && !isMono) || isCrossgen2 || isAot;

        if (mapDebugInfo) {
            if (needCodegenOptions) {
                toolOptions.push('--codegenopt', 'JitDisasmWithDebugInfo=1');
            }
            envVarFileContents.push('DOTNET_JitDisasmWithDebugInfo=1');
        }

        this.setCompilerExecOptions(execOptions, programDir);

        const compilerResult: DotNetCompilationResult = await this.buildToDll(
            compiler,
            compilerInfo,
            inputFilename,
            execOptions,
            compilerOptions,
            filters.binary,
        );
        if (compilerResult.code !== 0) {
            return compilerResult;
        }

        if (mapDebugInfo) {
            const pdbPath = utils.changeExtension(programDllPath, '.pdb');
            if (await utils.fileExists(pdbPath)) {
                compilerResult.dotnetSourceMapping = new DotNetPdbParser(
                    await fs.readFile(programDllPath),
                    await fs.readFile(pdbPath),
                ).parse();
            }
        }

        if (isIlDasm) {
            const ilDasmResult = await this.runIlDasm(
                execOptions,
                programDllPath,
                toolOptions,
                this.getOutputFilename(programDir, this.outputFilebase),
            );

            if (ilDasmResult.code !== 0) {
                return ilDasmResult;
            }
        } else if (isIlSpy) {
            const ilSpyResult = await this.runIlSpy(
                execOptions,
                programDllPath,
                toolOptions,
                this.getOutputFilename(programDir, this.outputFilebase),
                compilerInfo.sdkMajorVersion <= 6,
            );

            if (ilSpyResult.code !== 0) {
                return ilSpyResult;
            }
        } else if (isCrossgen2) {
            const crossgen2Result = await this.runCrossgen2(
                compiler,
                compilerInfo.sdkMajorVersion,
                codegenArch,
                execOptions,
                this.clrBuildDir,
                programDllPath,
                toolOptions,
                this.getOutputFilename(programDir, this.outputFilebase),
            );

            if (crossgen2Result.code !== 0) {
                return crossgen2Result;
            }
        } else if (isAot) {
            const ilcResult = await this.runIlc(
                this.ilcPath,
                codegenArch,
                execOptions,
                programDllPath,
                toolOptions,
                this.getOutputFilename(programDir, this.outputFilebase),
                filters.binary ?? false,
            );

            if (ilcResult.code !== 0) {
                return ilcResult;
            }
        } else {
            const corerunResult = await this.runCorerunForDisasm(
                execOptions,
                this.clrBuildDir,
                envVarFileContents,
                programDllPath,
                toolOptions,
                this.getOutputFilename(programDir, this.outputFilebase),
                isMono,
                compilerInfo.sdkMajorVersion >= 7,
            );

            if (corerunResult.code !== 0) {
                return corerunResult;
            }
        }

        return compilerResult;
    }

    override async processAsm(result: DotNetCompilationResult, filters: ParseFiltersAndOutputOptions) {
        return new DotNetAsmParser(result.dotnetSourceMapping).process(result.asm as string, filters);
    }

    override optionsForFilter(filters: ParseFiltersAndOutputOptions) {
        return [];
    }

    override postCompilationPreCacheHook(result: CompilationResult): CompilationResult {
        const isIlSpy = this.compiler.group === 'dotnetilspy';
        if (isIlSpy && result.code === 0) {
            result.languageId = 'csharp';
        }
        return result;
    }

    async getRuntimeVersion() {
        const versionString = await utils.tryReadTextFile(`${this.clrBuildDir}/version.txt`);
        return versionString ?? '<unknown version>';
    }

    async runCorerunForDisasm(
        execOptions: ExecutionOptionsWithEnv,
        coreRoot: string,
        envVars: string[],
        dllPath: string,
        options: string[],
        outputPath: string,
        isMono: boolean,
        useEnvFile: boolean,
    ) {
        if (isMono) {
            coreRoot = path.join(coreRoot, 'mono');
        }

        const corerunOptions = ['--clr-path', coreRoot].concat([...options, this.disassemblyLoaderPath, dllPath]);

        if (useEnvFile) {
            const envVarFilePath = path.join(path.dirname(outputPath), '.env');
            await fs.writeFile(envVarFilePath, envVars.join('\n'));
            corerunOptions.unshift('--env', envVarFilePath);
        } else {
            for (const env of envVars) {
                const delimiterIndex = env.indexOf('=');
                if (delimiterIndex !== -1) {
                    execOptions.env[env.substring(0, delimiterIndex)] = env.substring(delimiterIndex + 1);
                }
            }
        }

        const compilerExecResult = await this.exec(this.corerunPath, corerunOptions, execOptions);
        const result = this.transformToCompilationResult(compilerExecResult, dllPath);

        await fs.writeFile(
            outputPath,
            `// ${isMono ? 'mono' : 'coreclr'} ${await this.getRuntimeVersion()}${result.stdout
                .map(o => o.text)
                .reduce((a, n) => `${a}\n${n}`, '')}`,
        );

        return result;
    }

    async runIlSpy(
        execOptions: ExecutionOptions,
        dllPath: string,
        options: string[],
        outputPath: string,
        useDotNetHost: boolean,
    ) {
        const ilspyRoot = path.join(this.toolsPath, 'ilspycmd');
        const ilspyVersionDirs = await fs.readdir(ilspyRoot);
        const ilspyVersion = ilspyVersionDirs[0];
        const ilspyToolsDir = path.join(ilspyRoot, ilspyVersion, 'ilspycmd', ilspyVersion, 'tools');
        const targetFrameworkDirs = await fs.readdir(ilspyToolsDir);
        const targetFramework = targetFrameworkDirs[0];
        const ilspyPath = path.join(ilspyToolsDir, targetFramework, 'any', 'ilspycmd.dll');

        const ilspyOptions = [ilspyPath, dllPath, '--disable-updatecheck', '-r', this.clrBuildDir].concat(options);
        const compilerPath = useDotNetHost ? this.compiler.exe : this.corerunPath;
        const compilerExecResult = await this.exec(compilerPath, ilspyOptions, execOptions);
        const result = this.transformToCompilationResult(compilerExecResult, dllPath);

        await fs.writeFile(
            outputPath,
            `// ilspy ${await this.getRuntimeVersion()}${result.stdout
                .map(o => o.text)
                .reduce((a, n) => `${a}\n${n}`, '')}`,
        );

        return result;
    }

    async runIlDasm(execOptions: ExecutionOptions, dllPath: string, options: string[], outputPath: string) {
        const ildasmOptions = [dllPath, '-utf8'].concat(options);

        const compilerExecResult = await this.exec(this.ildasmPath, ildasmOptions, execOptions);
        const result = this.transformToCompilationResult(compilerExecResult, dllPath);

        await fs.writeFile(
            outputPath,
            `// ildasm ${await this.getRuntimeVersion()}${result.stdout
                .map(o => o.text)
                .reduce((a, n) => `${a}\n${n}`, '')}`,
        );

        return result;
    }

    async runCrossgen2(
        compiler: string,
        sdkMajorVersion: number,
        arch: string,
        execOptions: ExecutionOptions,
        bclPath: string,
        dllPath: string,
        options: string[],
        outputPath: string,
    ) {
        const crossgen2Options = [
            '-r',
            path.join(bclPath, '/'),
            '-r',
            this.disassemblyLoaderPath,
            dllPath,
            '-o',
            `${AssemblyName}.r2r.dll`,
        ].concat(options);

        const corelibPath = path.join(this.clrBuildDir, 'corelib', arch, 'System.Private.CoreLib.dll');
        if (await utils.fileExists(corelibPath)) {
            crossgen2Options.unshift('-r', corelibPath);
        }

        if (sdkMajorVersion >= 9) {
            crossgen2Options.push('--inputbubble', '--compilebubblegenerics');
        }

        if (await utils.fileExists(this.crossgen2Path)) {
            compiler = this.crossgen2Path;
        } else {
            crossgen2Options.unshift(this.crossgen2Path + '.dll');
        }

        const compilerExecResult = await this.exec(compiler, crossgen2Options, execOptions);
        const result = this.transformToCompilationResult(compilerExecResult, dllPath);

        await fs.writeFile(
            outputPath,
            `// crossgen2 ${await this.getRuntimeVersion()}${result.stdout
                .map(o => o.text)
                .reduce((a, n) => `${a}\n${n}`, '')}`,
        );

        return result;
    }

    async runIlc(
        compiler: string,
        arch: string,
        execOptions: ExecutionOptions,
        dllPath: string,
        options: string[],
        outputPath: string,
        buildToBinary: boolean,
    ) {
        const ilcOptions = [
            dllPath,
            '-o',
            `${AssemblyName}.obj`,
            '-r',
            this.disassemblyLoaderPath,
            '-r',
            path.join(this.clrBuildDir, 'aotsdk', arch, '*.dll'),
            '-r',
            path.join(this.clrBuildDir, '*.dll'),
            '--initassembly:System.Private.CoreLib',
            '--initassembly:System.Private.StackTraceMetadata',
            '--initassembly:System.Private.TypeLoader',
            '--initassembly:System.Private.Reflection.Execution',
            '--directpinvoke:libSystem.Native',
            '--directpinvoke:libSystem.Globalization.Native',
            '--directpinvoke:libSystem.IO.Compression.Native',
            '--directpinvoke:libSystem.Net.Security.Native',
            '--directpinvoke:libSystem.Security.Cryptography.Native.OpenSsl',
            '--feature:System.Reflection.Metadata.MetadataUpdater.IsSupported=false',
            '--feature:System.Resources.ResourceManager.AllowCustomResourceTypes=false',
            '--feature:System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported=false',
            '--feature:System.Runtime.InteropServices.EnableConsumingManagedCodeFromNativeHosting=false',
            '--feature:System.Runtime.InteropServices.EnableCppCLIHostActivation=false',
            '--feature:System.StartupHookProvider.IsSupported=false',
            '--feature:System.Linq.Expressions.CanEmitObjectArrayDelegate=false',
            '--feature:System.Diagnostics.Debugger.IsSupported=false',
            '--runtimeknob:System.Reflection.Metadata.MetadataUpdater.IsSupported=false',
            '--runtimeknob:System.Resources.ResourceManager.AllowCustomResourceTypes=false',
            '--runtimeknob:System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported=false',
            '--runtimeknob:System.Runtime.InteropServices.EnableConsumingManagedCodeFromNativeHosting=false',
            '--runtimeknob:System.Runtime.InteropServices.EnableCppCLIHostActivation=false',
            '--runtimeknob:System.StartupHookProvider.IsSupported=false',
            '--runtimeknob:System.Linq.Expressions.CanEmitObjectArrayDelegate=false',
            '--resilient',
            '--singlewarn',
            '--scanreflection',
            '--nosinglewarnassembly:CompilerExplorer',
            '--generateunmanagedentrypoints:System.Private.CoreLib',
        ].concat(options);

        if (!buildToBinary) {
            ilcOptions.push('--nativelib', '--root:CompilerExplorer');
        }

        const compilerExecResult = await this.exec(compiler, ilcOptions, execOptions);
        const result = this.transformToCompilationResult(compilerExecResult, dllPath);

        await fs.writeFile(
            outputPath,
            `// ilc ${await this.getRuntimeVersion()}${result.stdout
                .map(o => o.text)
                .reduce((a, n) => `${a}\n${n}`, '')}`,
        );

        return result;
    }

    override async runExecutable(executable: string, executeParameters: ExecutableExecutionOptions, homeDir: string) {
        const execOptionsCopy: ExecutableExecutionOptions = JSON.parse(
            JSON.stringify(executeParameters),
        ) as ExecutableExecutionOptions;

        if (this.compiler.executionWrapper) {
            execOptionsCopy.args = [...this.compiler.executionWrapperArgs, executable, ...execOptionsCopy.args];
            executable = this.compiler.executionWrapper;
        }

        const isCoreClr = this.compiler.group === 'dotnetcoreclr';
        const isMono = this.compiler.group === 'dotnetmono';

        if (!isCoreClr && !isMono) {
            return {
                ...utils.getEmptyExecutionResult(),
                stdout: [],
                stderr: utils.parseOutput('Execution is only supported for CoreCLR and Mono compilers'),
                code: -1,
            };
        }

        const compilerInfo = await this.getCompilerInfo(this.lang.id);
        const extraConfiguration: DotnetExtraConfiguration = {
            buildConfig: this.buildConfig,
            clrBuildDir: isMono ? path.join(this.clrBuildDir, 'mono') : this.clrBuildDir,
            langVersion: this.langVersion,
            targetFramework: compilerInfo.targetFramework,
            corerunPath: this.corerunPath,
        };

        const execEnv: IExecutionEnvironment = new this.executionEnvironmentClass(this.env);
        return await execEnv.execBinary(executable, execOptionsCopy, homeDir, extraConfiguration);
    }
}

interface DotNetCompilerInfo {
    targetFramework: string;
    sdkVersion: string;
    sdkMajorVersion: number;
    compilerPath: string;
}

export class DotNetCoreClrCompiler extends DotNetCompiler {
    static get key() {
        return 'dotnetcoreclr';
    }
}

export class DotNetCrossgen2Compiler extends DotNetCompiler {
    static get key() {
        return 'dotnetcrossgen2';
    }
}

export class DotNetMonoCompiler extends DotNetCompiler {
    static get key() {
        return 'dotnetmono';
    }
}

export class DotNetNativeAotCompiler extends DotNetCompiler {
    static get key() {
        return 'dotnetnativeaot';
    }
}

export class DotNetIlDasmCompiler extends DotNetCompiler {
    static get key() {
        return 'dotnetildasm';
    }
}

export class DotNetIlSpyCompiler extends DotNetCompiler {
    static get key() {
        return 'dotnetilspy';
    }
}

export class DotNetLegacyCompiler extends DotNetCompiler {
    static get key() {
        return 'dotnetlegacy';
    }
}
