// Copyright (c) 2023, 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 _ from 'underscore';

import type {ParsedAsmResult, ParsedAsmResultLine} from '../../types/asmresult/asmresult.interfaces.js';
import {CompilationResult, ExecutionOptionsWithEnv} from '../../types/compilation/compilation.interfaces.js';
import {CompilerOverrideOption, CompilerOverrideType} from '../../types/compilation/compiler-overrides.interfaces.js';
import type {
    OptPipelineBackendOptions,
    OptPipelineOutput,
} from '../../types/compilation/opt-pipeline-output.interfaces.js';
import type {PreliminaryCompilerInfo} from '../../types/compiler.interfaces.js';
import type {UnprocessedExecResult} from '../../types/execution/execution.interfaces.js';
import type {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces.js';
import type {SelectedLibraryVersion} from '../../types/libraries/libraries.interfaces.js';
import {BaseCompiler, c_value_placeholder} from '../base-compiler.js';
import {CompilationEnvironment} from '../compilation-env.js';
import {logger} from '../logger.js';
import {Dex2OatPassDumpParser} from '../parsers/dex2oat-pass-dump-parser.js';
import * as utils from '../utils.js';
import {D8Compiler} from './d8.js';

const BOOTCLASSPATH_JARS = [
    'bootjars/core-oj.jar',
    'bootjars/core-libart.jar',
    'bootjars/okhttp.jar',
    'bootjars/bouncycastle.jar',
    'bootjars/apache-xml.jar',
] as const;

const BOOTCLASSPATH_LOCATIONS = [
    '/apex/com.android.art/javalib/core-oj.jar',
    '/apex/com.android.art/javalib/core-libart.jar',
    '/apex/com.android.art/javalib/okhttp.jar',
    '/apex/com.android.art/javalib/bouncycastle.jar',
    '/apex/com.android.art/javalib/apache-xml.jar',
] as const;

export class Dex2OatCompiler extends BaseCompiler {
    readonly fullOutputFlag = '--full-output';

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

    passDumpParser: Dex2OatPassDumpParser;

    insnSetRegex: RegExp;
    insnSetFeaturesRegex: RegExp;

    compilerFilterRegex: RegExp;
    classRegex: RegExp;
    methodRegex: RegExp;
    methodSizeRegex: RegExp;
    insnRegex: RegExp;
    extraInfoRegex: RegExp;
    offsetRegex: RegExp;

    insnSetArgRegex: RegExp;
    compilerFilterArgRegex: RegExp;

    versionPrefixRegex: RegExp;
    latestVersionRegex: RegExp;

    fullOutput: boolean;

    d8Id: string;
    artArtifactDir: string;
    profmanPath: string;
    cwd: string;

    libs: SelectedLibraryVersion[];

    smaliLineNumberRegex: RegExp;
    smaliClassRegex: RegExp;
    smaliDexPcRegex: RegExp;
    smaliMethodStartRegex: RegExp;
    smaliMethodEndRegex: RegExp;

    constructor(compilerInfo: PreliminaryCompilerInfo, env: CompilationEnvironment) {
        super({...compilerInfo}, env);
        this.compiler.optPipeline = {
            arg: ['-print-after-all', '-print-before-all'],
            moduleScopeArg: ['-print-module-scope'],
            noDiscardValueNamesArg: [],
        };

        this.passDumpParser = new Dex2OatPassDumpParser();

        // These must be used before the output is split on newlines.
        this.insnSetRegex = /\s*INSTRUCTION\s+SET:\s*\n(.+?)\n/;
        this.insnSetFeaturesRegex = /\s*INSTRUCTION\s+SET\s+FEATURES:\s*\n(.+?)\n/;

        // These must be used after the output is split on newlines.
        this.compilerFilterRegex = /^compiler-filter\s=\s(.*)$/;
        this.classRegex = /^\s*\d+:\s+L(.*);\s+\(offset=0x\w+\)\s+\(type_idx=\d+\).*$/;
        this.methodRegex = /^\s+\d+:\s+(.*)\s+\(dex_method_idx=\d+\)$/;
        this.methodSizeRegex = /^\s+CODE:\s+\(code_offset=(0x\w+)\s+size=(\d+).*$/;
        this.insnRegex = /^\s+(0x\w+):\s+\w+\s+(.*)$/;

        this.extraInfoRegex = /^\s+((StackMap|InlineInfo)\[\d+\])\s+\((.*)\).*$/;

        // Similar to insnRegex above, but this applies after oatdump output has
        // been cleaned up.
        this.offsetRegex = /^\s*(0x\w+)\s+.*$/;

        // ART version codes in CE are in the format of AABB, where AA is the
        // API level and BB is the number of months since the initial release.
        this.versionPrefixRegex = /^(android-)?(java|kotlin)-dex2oat-(\d\d)\d+$/;
        this.latestVersionRegex = /^(android-)?(java|kotlin)-dex2oat-(latest|default|local)$/;

        // User-provided arguments (with a default behavior if not provided).
        this.insnSetArgRegex = /^--instruction-set=.*$/;
        this.compilerFilterArgRegex = /^--compiler-filter=.*$/;

        // Whether the full dex2oat output should be displayed instead of just
        // the parsed and formatted methods.
        this.fullOutput = false;

        // The underlying D8 version+exe.
        this.d8Id = this.compilerProps<string>(`compiler.${this.compiler.id}.d8Id`);

        // The directory containing ART artifacts necessary for dex2oat to run.
        this.artArtifactDir = this.compilerProps<string>(`compiler.${this.compiler.id}.artArtifactDir`);

        // The path to the `profman` binary.
        this.profmanPath = this.compilerProps<string>(`compiler.${this.compiler.id}.profmanPath`);

        // The path where D8/dex2oat are being run. This is used to find
        // classes.cfg from processAsm().
        this.cwd = '';

        // Libraries that will flow to D8Compiler and Java/KotlinCompiler.
        this.libs = [];

        // Regexes that apply to .smali files (R8/D8 dump output).
        this.smaliLineNumberRegex = /^\s+\.line\s+(\d+).*$/;
        this.smaliClassRegex = /^\.class.*\s(L.*;)$/;
        this.smaliDexPcRegex = /^\s+#@(\w+).*$/;
        this.smaliMethodStartRegex = /^\.method\s(.*)$/;
        this.smaliMethodEndRegex = /^\s*\.end\smethod.*$/;
    }

    override async runCompiler(
        compiler: string,
        options: string[],
        inputFilename: string,
        execOptions: ExecutionOptionsWithEnv,
        filters?: ParseFiltersAndOutputOptions,
    ): Promise<CompilationResult> {
        // Make sure --full-output from previous invocations doesn't persist.
        this.fullOutput = false;

        // Instantiate D8 compiler, which will in turn instantiate a Java or
        // Kotlin compiler based on the current language.
        const d8Compiler = this.env.findCompiler(this.lang.id, this.d8Id) as D8Compiler | undefined;
        if (!d8Compiler) {
            return {
                ...this.handleUserError(
                    {message: `Compiler ${this.lang.id} ${this.d8Id} not configured correctly`},
                    '',
                ),
                timedOut: false,
            };
        }
        const d8DirPath = path.dirname(inputFilename);
        const d8OutputFilename = d8Compiler.getOutputFilename(d8DirPath);

        if (!execOptions) {
            execOptions = this.getDefaultExecOptions();
        }

        let useDefaultInsnSet = true;
        let useDefaultCompilerFilter = true;
        let d8Flags: string | undefined;

        // The items in 'options' before the source file are user inputs.
        const sourceFileOptionIndex = options.findIndex(option => {
            return option.endsWith('.java') || option.endsWith('.kt');
        });
        const userOptions: string[] = [];
        for (let i = 0; i < sourceFileOptionIndex; ++i) {
            const option = options[i];
            if (this.insnSetArgRegex.test(option)) {
                useDefaultInsnSet = false;
                userOptions.push(option);
            } else if (this.compilerFilterArgRegex.test(option)) {
                useDefaultCompilerFilter = false;
                userOptions.push(option);
            } else if (option === this.fullOutputFlag) {
                this.fullOutput = true;
            } else if (option === '--d8-flags') {
                d8Flags = options[++i];
            } else {
                userOptions.push(option);
            }
        }

        const d8Options = _.compact(
            d8Compiler.prepareArguments(
                d8Flags?.split(' ') ?? [],
                d8Compiler.getDefaultFilters(),
                {}, // backendOptions
                inputFilename,
                d8OutputFilename,
                this.libs,
                [], // overrides
            ),
        );

        const compileResult = await d8Compiler.runCompiler(
            d8Compiler.getInfo().exe,
            d8Options,
            this.filename(inputFilename),
            d8Compiler.getDefaultExecOptions(),
        );

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

        const files = await fs.readdir(d8DirPath);
        const dexFile = files.find(f => f.endsWith('.dex'));
        if (!dexFile) {
            throw new Error('Generated dex file not found');
        }

        await d8Compiler.generateSmali(path.join(d8DirPath, dexFile), 64 * 1024 * 1024);

        const profileAndResult = await this.generateProfile(d8DirPath, dexFile);
        if (profileAndResult && profileAndResult.result.code !== 0) {
            return {
                ...this.transformToCompilationResult(profileAndResult.result, inputFilename),
                languageId: this.getCompilerResultLanguageId(filters),
            };
        }

        let isLatest = false;
        let versionPrefix = 0;
        let match;
        if (this.versionPrefixRegex.test(this.compiler.id)) {
            match = this.compiler.id.match(this.versionPrefixRegex);
            versionPrefix = Number.parseInt(match![2], 10);
        } else if (this.latestVersionRegex.test(this.compiler.id)) {
            isLatest = true;
        }

        const dex2oatOptions = [
            '--android-root=include',
            '--generate-debug-info',
            '--dex-location=/system/framework/classes.dex',
            `--dex-file=${d8DirPath}/${dexFile}`,
            '--copy-dex-files=always',
            ...(versionPrefix >= 34 || isLatest ? ['--runtime-arg', '-Xgc:CMC'] : []),
            '--runtime-arg',
            '-Xbootclasspath:' + BOOTCLASSPATH_JARS.map(f => path.join(this.artArtifactDir, f)).join(':'),
            '--runtime-arg',
            '-Xbootclasspath-locations:' + BOOTCLASSPATH_LOCATIONS.join(':'),
            `--boot-image=${this.artArtifactDir}/app/system/framework/boot.art`,
            `--oat-file=${d8DirPath}/classes.odex`,
            `--app-image-file=${d8DirPath}/classes.art`,
            '--force-allow-oj-inlines',
            `--dump-cfg=${d8DirPath}/classes.cfg`,
            ...userOptions,
        ];
        if (useDefaultInsnSet) {
            dex2oatOptions.push('--instruction-set=arm64');
        }
        if (useDefaultCompilerFilter) {
            if (profileAndResult == null) {
                dex2oatOptions.push('--compiler-filter=speed');
            } else {
                dex2oatOptions.push('--compiler-filter=speed-profile');
            }
        }
        if (profileAndResult != null) {
            dex2oatOptions.push(`--profile-file=${profileAndResult.path}`);
        }

        execOptions.customCwd = d8DirPath;
        this.cwd = d8DirPath;

        const result = await this.exec(this.compiler.exe, dex2oatOptions, execOptions);
        if (profileAndResult != null) {
            result.stdout = profileAndResult.result.stdout + result.stdout;
            result.stderr = profileAndResult.result.stderr + result.stderr;
        }
        return {
            ...this.transformToCompilationResult(result, d8OutputFilename),
            languageId: this.getCompilerResultLanguageId(filters),
        };
    }

    override getIncludeArguments(libraries: SelectedLibraryVersion[], dirPath: string): string[] {
        this.libs = libraries;
        return super.getIncludeArguments(libraries, dirPath);
    }

    private async generateProfile(
        d8DirPath: string,
        dexFile: string,
    ): Promise<{path: string; result: UnprocessedExecResult} | null> {
        const humanReadableFormatProfile = `${d8DirPath}/profile.prof.txt`;
        try {
            await fs.access(humanReadableFormatProfile);
        } catch {
            // No profile. This is expected.
            return null;
        }

        const execOptions = this.getDefaultExecOptions();
        execOptions.customCwd = d8DirPath;
        const binaryFormatProfile = `${d8DirPath}/profile.prof`;
        const result = await this.exec(
            this.profmanPath,
            [
                `--create-profile-from=${humanReadableFormatProfile}`,
                `--apk=${d8DirPath}/${dexFile}`,
                '--dex-location=/system/framework/classes.dex',
                ...BOOTCLASSPATH_JARS.map(f => `--apk=${path.join(this.artArtifactDir, f)}`),
                ...BOOTCLASSPATH_LOCATIONS.map(f => `--dex-location=${f}`),
                `--reference-profile-file=${binaryFormatProfile}`,
                '--output-profile-type=app',
            ],
            execOptions,
        );

        return {path: binaryFormatProfile, result: result};
    }

    override async objdump(outputFilename: string, result: any, maxSize: number) {
        const dirPath = path.dirname(outputFilename);
        const files = await fs.readdir(dirPath);
        const odexFile = files.find(f => f.endsWith('.odex'));
        const args = [...this.compiler.objdumperArgs, `--oat-file=${odexFile}`];
        const objResult = await this.exec(this.compiler.objdumper, args, {
            maxOutput: maxSize,
            customCwd: dirPath,
        });

        const asmResult: ParsedAsmResult = {
            asm: [
                {
                    text: objResult.stdout,
                },
            ],
        };
        if (objResult.code === 0) {
            result.objdumpTime = objResult.execTime;
        } else {
            asmResult.asm = [
                {
                    text: `<No output: oatdump returned ${objResult.code}>`,
                },
            ];
        }
        result.asm = asmResult.asm;
        return result;
    }

    override optionsForFilter(filters: ParseFiltersAndOutputOptions) {
        filters.binary = true;
        return [];
    }

    // dex2oat doesn't have --version, but artArtifactDir contains a file with
    // the build number.
    override async getVersion() {
        logger.info(`Gathering ${this.compiler.id} version information on ${this.compiler.exe}...`);
        const versionFile = this.artArtifactDir + '/snapshot-creation-build-number.txt';
        const version = await fs.readFile(versionFile, {encoding: 'utf8'});
        return {
            stdout: 'Android Build ' + version,
            stderr: '',
            code: 0,
        };
    }

    // Gets a string of methods parameters in the form used in .smali and
    // splits it.
    // '[Ljava/lang/String;[IIZ' --> ['Ljava/lang/String;', '[I', 'I', 'Z']
    splitMethodParameters(parameters: string): string[] {
        const split: string[] = [];
        let l = 0;
        let r = 0;
        let inReferenceType = false;
        while (l < parameters.length) {
            if (parameters.charAt(r) === '[') {
                r++;
            } else if (parameters.charAt(r) === 'L') {
                inReferenceType = true;
                r++;
            } else if (parameters.charAt(r) === ';') {
                inReferenceType = false;
                split.push(parameters.substring(l, r + 1));
                l = r + 1;
                r = l;
            } else if (inReferenceType) {
                r++;
            } else if (!inReferenceType) {
                // Any character found while not incrementing through the
                // reference type represents  a primitive.
                split.push(parameters.substring(l, r + 1));
                l = r + 1;
                r = l;
            }
        }
        return split;
    }

    prettyDescriptor(descriptor: string): string {
        let prettyOutput = '';

        let dim = 0;
        let idx = 0;

        // Determine dimensionality.
        while (descriptor.charAt(idx) === '[') {
            dim++;
            idx++;
        }

        // Remove leading 'L' for reference types, otherwise translate to
        // primitive type name.
        let primitive = false;
        if (descriptor.charAt(idx) === 'L') {
            idx++;
            prettyOutput = descriptor.slice(idx);
        } else {
            primitive = true;
            switch (descriptor.charAt(idx)) {
                case 'V': {
                    prettyOutput = 'void';
                    break;
                }
                case 'Z': {
                    prettyOutput = 'boolean';
                    break;
                }
                case 'B': {
                    prettyOutput = 'byte';
                    break;
                }
                case 'S': {
                    prettyOutput = 'short';
                    break;
                }
                case 'C': {
                    prettyOutput = 'char';
                    break;
                }
                case 'I': {
                    prettyOutput = 'int';
                    break;
                }
                case 'J': {
                    prettyOutput = 'long';
                    break;
                }
                case 'F': {
                    prettyOutput = 'float';
                    break;
                }
                case 'D': {
                    prettyOutput = 'double';
                    break;
                }
                default: {
                    return descriptor;
                }
            }
        }

        prettyOutput = prettyOutput.replaceAll('/', '.');

        // Remove trailing ';'.
        if (!primitive) {
            prettyOutput = prettyOutput.substring(0, prettyOutput.length - 1);
        }

        for (let i = 0; i < dim; i++) {
            prettyOutput += '[]';
        }

        return prettyOutput;
    }

    // Converts method signatures in the form of
    // 'methodName([Lsome/ref/Type;[IIZ)Lsome/ref/Type;' to
    // 'some.ref.Type methodName(some.ref.Type[], int[], int, boolean)'.
    // The former is used in .smali files and the latter is used in ART outputs.
    prettyMethodSignature(methodSignature: string): string {
        let prettyOutput = '';

        // Get just the last word, which is the full method signature in .smali
        // files. This removes preceding strings like 'static', 'constructor',
        // etc.
        const trimmed = methodSignature.split(/\s/).slice(-1)[0];

        // Matches something like 'methodName(parameterparameter)returnType'.
        const match = trimmed.match(/^(.*)\((.*)\)(.*)$/)!;

        // 'returnType methodName('
        prettyOutput += this.prettyDescriptor(match[3]) + ' ' + match[1] + '(';
        const parameters = this.splitMethodParameters(match[2]);
        for (let i = 0; i < parameters.length; i++) {
            // 'returnType methodName(parameter, parameter'
            prettyOutput += this.prettyDescriptor(parameters[i]);
            if (i < parameters.length - 1) {
                prettyOutput += ', ';
            }
        }
        // 'returnType methodName(parameter, parameter)'
        prettyOutput += ')';
        return prettyOutput;
    }

    // Adds the class name that the method is contained in to the method name.
    // 'int square(int)' -> 'int Square.square(int)'.
    insertClassIntoMethodSignature(className: string, methodSignature: string): string {
        const components = methodSignature.split(' ');
        components[1] = className + '.' + components[1];
        return components.join(' ');
    }

    // Associate each method's dex PCs to line numbers. For example, in this
    // short .smali example below, we should get something like
    // {'int Square.square(int)': {0: 14, 1: 14}}
    //
    // .method static square(I)I
    //    .registers 1
    //
    //    #@0
    //    .line 14
    //    mul-int/2addr p0, p0
    //
    //    #@1
    //    return p0
    // .end method
    parseSmaliForLineNumbers(dexPcsToLines: Record<string, Record<number, number>>, smaliLines: string[]) {
        let className = '';
        let methodSignature = '';
        let lineNumber = -1;
        let dexPc = -1;
        for (const l of smaliLines) {
            if (this.smaliClassRegex.test(l)) {
                className = this.prettyDescriptor(l.match(this.smaliClassRegex)![1]);
            } else if (this.smaliMethodStartRegex.test(l)) {
                methodSignature = this.insertClassIntoMethodSignature(
                    className,
                    this.prettyMethodSignature(l.match(this.smaliMethodStartRegex)![1]),
                );
                dexPcsToLines[methodSignature] = {};
            } else if (this.smaliMethodEndRegex.test(l)) {
                methodSignature = '';
                lineNumber = -1;
                dexPc = -1;
            } else if (this.smaliLineNumberRegex.test(l)) {
                // Line numbers are given in decimal.
                lineNumber = Number.parseInt(l.match(this.smaliLineNumberRegex)![1], 10);
                dexPcsToLines[methodSignature][dexPc] = lineNumber;
            } else if (this.smaliDexPcRegex.test(l)) {
                // Dex PCs are given in hex.
                dexPc = Number.parseInt(l.match(this.smaliDexPcRegex)![1], 16);
                dexPcsToLines[methodSignature][dexPc] = lineNumber;
            }
        }
    }

    override async processAsm(result, filters: ParseFiltersAndOutputOptions) {
        let asm = '';

        if (typeof result.asm === 'string') {
            const asmLines = utils.splitLines(result.asm);
            if (asmLines.length === 1 && asmLines[0][0] === '<') {
                return {
                    asm: [{text: asmLines[0], source: null}],
                };
            }
            return {
                asm: [{text: JSON.stringify(asmLines), source: null}],
            };
        }
        // result.asm is an array, but we only expect it to have one value.
        asm = result.asm[0].text;

        const segments: ParsedAsmResultLine[] = [];
        if (this.fullOutput || !filters.directives) {
            // Returns entire dex2oat output.
            segments.push({text: asm, source: null});
        } else {
            const {
                compileData,
                classNames,
                classToMethods,
                methodsToInstructions,
                methodsToSizes,
                absoluteToRelativeOffsets,
            } = this.parseAsm(asm);

            const classesCfg = path.join(this.cwd, 'classes.cfg');
            let methodsAndOffsetsToDexPcs: Record<string, Record<number, number>> = {};
            try {
                const rawCfgText = await fs.readFile(classesCfg, {encoding: 'utf8'});
                methodsAndOffsetsToDexPcs = this.passDumpParser.parsePassDumpsForDexPcs(rawCfgText.split(/\n/));
            } catch (e) {
                // This is expected if this is running in a test. If this fails for another reason, we just won't see
                // line highlights.
                segments.push({
                    text: `classes.cfg is missing, source lines will not be highlighted: ${e}`,
                    source: null,
                });
            }

            const dexPcsToLines: Record<string, Record<number, number>> = {};
            try {
                const files = await fs.readdir(this.cwd);
                const smaliFiles = files.filter(f => f.endsWith('.smali'));
                for (const smaliFile of smaliFiles) {
                    const rawSmaliText = await fs.readFile(path.join(this.cwd, smaliFile), 'utf-8');
                    this.parseSmaliForLineNumbers(dexPcsToLines, rawSmaliText.split(/\n/));
                }
            } catch (e) {
                // Same case as above.
                segments.push({
                    text: `*.smali is missing, source lines will not be highlighted: ${e}`,
                    source: null,
                });
            }

            segments.push(
                {
                    text: 'Instruction set:          ' + compileData.insnSet,
                    source: null,
                },
                {
                    text: 'Instruction set features: ' + compileData.insnSetFeatures,
                    source: null,
                },
                {
                    text: 'Compiler filter:          ' + compileData.compilerFilter,
                    source: null,
                },
                {text: '', source: null},
                {text: '', source: null},
            );

            for (const className of classNames) {
                for (const method of classToMethods[className]) {
                    segments.push({
                        text: method + ' [' + methodsToSizes[method] + ' bytes]',
                        source: null,
                    });
                    for (const instruction of methodsToInstructions[method]) {
                        let absoluteOffset = -1;
                        let relativeOffset = -1;
                        if (this.offsetRegex.test(instruction)) {
                            absoluteOffset = Number.parseInt(instruction.match(this.offsetRegex)![1], 16);
                            relativeOffset = absoluteToRelativeOffsets[absoluteOffset];
                        }
                        const offsetToDexPc = methodsAndOffsetsToDexPcs[method];
                        const dexPc = offsetToDexPc ? offsetToDexPc[relativeOffset] : -1;
                        const source =
                            Number.isInteger(dexPc) && dexPc >= 0 && dexPcsToLines[method]
                                ? {file: null, line: dexPcsToLines[method][dexPc]}
                                : null;
                        segments.push({
                            text: '    ' + instruction,
                            source: source,
                        });
                    }
                    segments.push({text: '', source: null});
                }
            }
        }

        return {asm: segments};
    }

    parseAsm(oatdumpOut: string) {
        const compileData: {
            insnSet?: string;
            insnSetFeatures?: string;
            compilerFilter?: string;
        } = {};

        const classNames: string[] = [];
        const classToMethods: Record<string, string[]> = {};
        const methodsToInstructions: Record<string, string[]> = {};
        const methodsToSizes: Record<string, number> = {};
        const absoluteToRelativeOffsets: Record<number, number> = {};

        let match;
        if (this.insnSetRegex.test(oatdumpOut)) {
            match = oatdumpOut.match(this.insnSetRegex);
            compileData.insnSet = match![1];
        }
        if (this.insnSetFeaturesRegex.test(oatdumpOut)) {
            match = oatdumpOut.match(this.insnSetFeaturesRegex);
            compileData.insnSetFeatures = match![1];
        }

        let inCode = false;
        let currentClass = '';
        let currentMethod = '';
        let currentCodeOffset = 0;
        for (const l of oatdumpOut.split(/\n/)) {
            if (this.compilerFilterRegex.test(l)) {
                match = l.match(this.compilerFilterRegex);
                compileData.compilerFilter = match![1];
            } else if (this.classRegex.test(l)) {
                match = l.match(this.classRegex);
                currentClass = match![1];
                classNames.push(currentClass);
                classToMethods[currentClass] = [];
            } else if (this.methodRegex.test(l)) {
                match = l.match(this.methodRegex);
                currentMethod = match![1];
                classToMethods[currentClass].push(currentMethod);
                methodsToInstructions[currentMethod] = [];
                inCode = false;
            } else if (this.methodSizeRegex.test(l)) {
                match = l.match(this.methodSizeRegex);
                methodsToSizes[currentMethod] = Number.parseInt(match![2], 10);
                currentCodeOffset = Number.parseInt(match![1], 16);
                inCode = true;
            } else if (inCode && this.insnRegex.test(l)) {
                match = l.match(this.insnRegex);
                methodsToInstructions[currentMethod].push(match![1] + '    ' + match![2]);
                // We need to convert to relative offsets because that is how
                // instructions are stored in classes.cfg's disassembly step.
                absoluteToRelativeOffsets[Number.parseInt(match![1], 16)] =
                    Number.parseInt(match![1], 16) - currentCodeOffset;
            } else if (inCode && this.extraInfoRegex.test(l)) {
                match = l.match(this.extraInfoRegex);
                methodsToInstructions[currentMethod].push(' ' + match![2] + '   ' + match![3]);
            }
        }

        return {
            compileData,
            classNames,
            classToMethods,
            methodsToInstructions,
            methodsToSizes,
            absoluteToRelativeOffsets,
        };
    }

    override async generateOptPipeline(
        inputFilename: string,
        options: string[],
        filters: ParseFiltersAndOutputOptions,
        OptPipelineOptions: OptPipelineBackendOptions,
    ): Promise<OptPipelineOutput | undefined> {
        const dirPath = path.dirname(inputFilename);
        const execOptions = this.getDefaultExecOptions();
        execOptions.maxOutput = 1024 * 1024 * 1024;

        const compileStart = performance.now();
        await this.runCompiler(this.compiler.exe, options, inputFilename, execOptions);
        const compileEnd = performance.now();

        try {
            const classesCfg = dirPath + '/classes.cfg';
            const rawText = await fs.readFile(classesCfg, {encoding: 'utf8'});
            const parseStart = performance.now();
            const optPipeline = this.passDumpParser.process(rawText);
            const parseEnd = performance.now();
            return {
                results: optPipeline,
                parseTime: parseEnd - parseStart,
                compileTime: compileEnd - compileStart,
            };
        } catch (e: any) {
            return {
                error: e.toString(),
                results: {},
                compileTime: compileEnd - compileStart,
            };
        }
    }

    override async getTargetsAsOverrideValues(): Promise<CompilerOverrideOption[]> {
        return ['arm', 'arm64', 'x86', 'x86_64', 'riscv64'].map(target => ({
            name: target,
            value: target,
        }));
    }

    override getTargetFlags(): string[] {
        return [`--instruction-set=${c_value_placeholder}`];
    }

    override async populatePossibleOverrides(): Promise<void> {
        super.populatePossibleOverrides();

        this.compiler.possibleOverrides?.push({
            type: 'options',
            name: CompilerOverrideType.full_output,
            display_title: 'Show full output',
            description: 'Show the full output of the object dump tool instead of only the assembly',
            flags: ['<value>'],
            values: [
                {name: 'Yes', value: this.fullOutputFlag},
                {name: 'No', value: ''},
            ],
        });

        this.compiler.possibleOverrides?.push({
            type: 'string',
            name: CompilerOverrideType.d8_flags,
            display_title: 'd8 Flags',
            description: 'This compiler uses d8 as a frontend translator. Change the flags of d8.',
            flags: ['--d8-flags', c_value_placeholder],
            default: '--release',
        });
    }
}
