// Copyright (c) 2024, 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 path from 'node:path';

import {CompilationResult} from '../../types/compilation/compilation.interfaces.js';
import type {PreliminaryCompilerInfo} from '../../types/compiler.interfaces.js';
import type {CompilerOutputOptions, ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces.js';
import {BaseCompiler} from '../base-compiler.js';
import {CompilationEnvironment} from '../compilation-env.js';
import {OdinAsmParser} from '../parsers/asm-parser-odin.js';
import * as utils from '../utils.js';

class OdinVersion {
    year: number = 0;
    month: number = 0;

    constructor(version: string) {
        const match = version.match(/(\d{4})-(\d{1,2})[a-zA-Z]*/);
        if (!match) {
            throw new Error(`Invalid version format: "${version}"`);
        }

        const year = Number(match[1]);
        const month = Number(match[2]);

        if (month < 1 || month > 12) {
            throw new Error(`Invalid month in version: "${version}"`);
        }

        this.year = year;
        this.month = month;
    }

    gte(version: OdinVersion) {
        if (this.year > version.year) {
            return true;
        } else if (this.year === version.year) {
            return this.month >= version.month;
        } else {
            return false;
        }
    }
}

export class OdinCompiler extends BaseCompiler {
    private clangPath?: string;

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

    constructor(info: PreliminaryCompilerInfo, env: CompilationEnvironment) {
        super(info, env);
        this.asm = new OdinAsmParser(this.compilerProps);
        this.compiler.supportsIrView = true;
        this.compiler.irArg = [];
        this.compiler.supportsIntel = false;
        this.clangPath = this.compilerProps<string>('clangPath', undefined);
    }

    override optionsForFilter(filters: ParseFiltersAndOutputOptions, outputFilename: string) {
        const options: string[] = ['-debug', '-keep-temp-files', `-out:${this.filename(outputFilename)}`];

        const version = new OdinVersion(this.compiler.version);
        if (version.gte(new OdinVersion('dev-2025-02'))) {
            options.push('-use-single-module');
        }

        if (!(filters.execute || filters.binary)) {
            filters.preProcessLines = this.preProcessLines.bind(this);
            options.push('-build-mode:asm');
        }

        return options;
    }

    override orderArguments(
        options: string[],
        inputFilename: string,
        libIncludes: string[],
        libOptions: string[],
        libPaths: string[],
        libLinks: string[],
        userOptions: string[],
        staticLibLinks: string[],
    ) {
        return ['build', this.filename(inputFilename), '-file'].concat(options, userOptions);
    }

    override getDefaultExecOptions() {
        const execOptions = super.getDefaultExecOptions();
        if (this.clangPath) {
            execOptions.env.ODIN_CLANG_PATH = this.clangPath;
        }

        return execOptions;
    }

    override async checkOutputFileAndDoPostProcess(
        asmResult: CompilationResult,
        outputFilename: string,
        filters: ParseFiltersAndOutputOptions,
    ) {
        let newOutputFilename = outputFilename;
        if (!filters.binary && !filters.execute) newOutputFilename = outputFilename.replace(/.s$/, '.S');
        return super.checkOutputFileAndDoPostProcess(asmResult, newOutputFilename, filters);
    }

    override getIrOutputFilename(inputFilename: string): string {
        return this.filename(path.dirname(inputFilename) + '/output.ll');
    }

    override async postProcessAsm(result, filters?: ParseFiltersAndOutputOptions) {
        // we dont need demangling
        return result;
    }

    /**
     * Preprocess the source code to '@require' all the functions
     * so that their asm is emitted
     */
    override preProcess(source: string, filters: CompilerOutputOptions): string {
        if (filters.binary && !this.stubRe.test(source)) {
            source += `\n${this.stubText}\n`;
        }

        if (!filters.binary && !filters.execute) {
            const sourceLines = utils.splitLines(source);
            const outputLines: string[] = [];
            const procRE = /^\s*([A-Za-z]\w+)\s*:\s*:\s*proc\s*("\w+")?\s*\(/;
            let lastLine = '';
            for (const line of sourceLines) {
                // skip if the line doesn't contain proc keyword
                if (!line.includes('proc')) {
                    outputLines.push(line);
                    lastLine = line;
                    continue;
                }

                const match = line.match(procRE);
                if (!match) {
                    outputLines.push(line);
                    lastLine = line;
                    continue;
                }

                // last line already has require?
                if (lastLine.includes('@require') || lastLine.includes('@(require)')) {
                    outputLines.push(line);
                    continue;
                }

                // @require the function so they dont get inlined
                // put @require on the same line so that output source
                // has same number of lines so that asm<->source mapping
                // in ui works correctly.
                outputLines.push('@(require) ' + line);
                lastLine = line;
            }

            let text = outputLines.join('\n');
            // append a final trailing newline
            if (!text.endsWith('\n')) {
                text += '\n';
            }
            return text;
        }

        return source;
    }

    preProcessLines(asmLines: string[]) {
        let i = 0;
        let funcStart = -1;
        while (i < asmLines.length) {
            const line = asmLines[i];
            // filter out __$ and "__$" builtin functions
            const hasBuiltinPrefix = line.startsWith('__$') || line.startsWith('"__$');
            if (funcStart === -1 && hasBuiltinPrefix && line.endsWith(':')) {
                // ensure there is cfi_startproc
                for (let j = i; j < asmLines.length && j < i + 5; j++) {
                    if (asmLines[j].includes('.cfi_startproc')) {
                        funcStart = i;
                        break;
                    }
                }

                // make sure this is a globl
                if (funcStart !== -1) {
                    for (let j = i; j >= 0 && j >= i - 3; --j) {
                        if (asmLines[j].includes('.globl')) {
                            funcStart = j;
                            break;
                        }
                    }
                }
            }

            if (funcStart !== -1 && line.includes('.cfi_endproc')) {
                const len = i - funcStart;
                asmLines.splice(funcStart, len + 1);
                i = funcStart - 1;
                funcStart = -1;
            }
            i++;
        }

        return this.removeNonMainSourceFunctions(asmLines);
    }

    parseFiles(asmLines: string[]) {
        const files: Record<number, string> = {};
        const fileFind = /^\s*\.(?:cv_)?file\s+(\d+)\s+"([^"]+)"(\s+"([^"]+)")?.*/;
        for (const line of asmLines) {
            const match = line.match(fileFind);
            if (match) {
                const lineNum = Number.parseInt(match[1], 10);
                if (match[4] && !line.includes('.cv_file')) {
                    // Clang-style file directive '.file X "dir" "filename"'
                    if (match[4].startsWith('/')) {
                        files[lineNum] = match[4];
                    } else {
                        files[lineNum] = match[2] + '/' + match[4];
                    }
                } else {
                    files[lineNum] = match[2];
                }
            }
        }
        return files;
    }

    removeNonMainSourceFunctions(asmLines: string[]): string[] {
        const files = this.parseFiles(asmLines);
        const sourceTag = /^\s*\.loc\s+(\d+)\s+(\d+)\s+(.*)/;
        const stdInLooking = /<stdin>|^-$|example\.[^/]+$|<source>/;
        let i = 0;
        let funcStart = -1;
        while (i < asmLines.length) {
            const line = asmLines[i];
            // find a label
            if (funcStart === -1 && line.endsWith(':')) {
                // scan the next 5 lines for a source loc label
                for (let j = i; j < i + 5 && j < asmLines.length; j++) {
                    const fline = asmLines[j];
                    const match = fline.match(sourceTag);
                    if (!match) continue;
                    const file = files[match[1]];
                    if (!file) continue;
                    // if the file is not current file we remove this function
                    if (!stdInLooking.test(file)) {
                        funcStart = i;
                        break;
                    }
                }

                // ensure there is cfi_startproc
                if (funcStart !== -1) {
                    funcStart = -1;
                    for (let j = i; j < asmLines.length && j < i + 5; j++) {
                        if (asmLines[j].includes('.cfi_startproc')) {
                            funcStart = i;
                            break;
                        }
                    }
                }

                if (funcStart !== -1) {
                    for (let j = i; j >= 0 && j >= i - 3; --j) {
                        if (asmLines[j].includes('.type') && asmLines[j].endsWith('@function')) {
                            funcStart = j;
                            break;
                        }
                    }
                }
            }

            if (funcStart !== -1 && line.includes('.cfi_endproc')) {
                const len = i - funcStart;
                asmLines.splice(funcStart, len + 1);
                i = funcStart - 1;
                funcStart = -1;
            }
            i++;
        }

        return asmLines;
    }
}
