// 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 path from 'path';

import fs from 'fs-extra';

import type {ExecutionOptionsWithEnv} from '../../types/compilation/compilation.interfaces.js';
import type {PreliminaryCompilerInfo} from '../../types/compiler.interfaces.js';
import {UnprocessedExecResult} from '../../types/execution/execution.interfaces.js';
import {BaseCompiler} from '../base-compiler.js';
import {CompilationEnvironment} from '../compilation-env.js';
import * as exec from '../exec.js';
import {logger} from '../logger.js';
import {TurboCAsmParser} from '../parsers/asm-parser-turboc.js';

export class DosboxCompiler extends BaseCompiler {
    private readonly dosbox: string;
    private readonly root: string;

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

        this.dosbox = this.compilerProps<string>(`compiler.${this.compiler.id}.dosbox`);
        this.root = this.compilerProps<string>(`compiler.${this.compiler.id}.root`);
        this.asm = new TurboCAsmParser(this.compilerProps);
    }

    protected override async writeMultipleFiles(files: any[], dirPath: string): Promise<any[]> {
        const filesToWrite: any[] = [];

        for (const file of files) {
            if (!file.filename) throw new Error('One of more files do not have a filename');

            const fullpath = this.getExtraFilepath(dirPath, file.filename);
            const contents = file.contents.replaceAll('\n', '\r\n');
            filesToWrite.push(fs.outputFile(fullpath, contents));
        }

        return Promise.all(filesToWrite);
    }

    protected override async writeAllFiles(dirPath: string, source: string, files: any[], filters: object) {
        if (!source) throw new Error(`File ${this.compileFilename} has no content or file is missing`);

        const inputFilename = path.join(dirPath, this.compileFilename);
        await fs.writeFile(inputFilename, source.replaceAll('\n', '\r\n'));

        if (files && files.length > 0) {
            await this.writeMultipleFiles(files, dirPath);
        }

        return {
            inputFilename,
        };
    }

    private getDosboxArgs(tempDir: string, compileArgs: string[]) {
        const binPath = path.relative(this.root, path.dirname(this.compiler.exe));
        const exeName = path.basename(this.compiler.exe).replace(/\.exe$/i, '');
        return [
            '-c',
            `mount c ${this.root}`,
            '-c',
            `mount d ${tempDir}`,
            '-c',
            `PATH=%PATH%;C:\\${binPath}`,
            '-c',
            'd:',
            '-c',
            `${exeName} ${compileArgs.join(' ')} > STDOUT.TXT`,
            '-c',
            'exit',
        ];
    }

    private getDosboxEnv() {
        return {
            SDL_VIDEODRIVER: 'dummy',
        };
    }

    public override async execCompilerCached(
        compiler: string,
        args: string[],
        options?: ExecutionOptionsWithEnv,
    ): Promise<UnprocessedExecResult> {
        if (this.mtime === null) {
            throw new Error('Attempt to access cached compiler before initialise() called');
        }
        if (!options) {
            options = this.getDefaultExecOptions();
            options.timeoutMs = 0;
            options.ldPath = this.getSharedLibraryPathsAsLdLibraryPaths([]);
        }

        const key = this.getCompilerCacheKey(compiler, args, options);
        let result: UnprocessedExecResult = await this.env.compilerCacheGet(key as any);
        if (!result) {
            result = await (this.env.enqueue(async () =>
                this.exec(compiler, args, options),
            ) as Promise<UnprocessedExecResult>);
            if (result && result.okToCache) {
                this.env
                    .compilerCachePut(key as any, result, undefined)
                    .then(() => {
                        // Do nothing, but we don't await here.
                    })
                    .catch(e => {
                        logger.info('Uncaught exception caching compilation results', e);
                    });
            }
        }

        return result;
    }

    public override async exec(filepath: string, args: string[], execOptions: any) {
        if (!execOptions) {
            execOptions = this.getDefaultExecOptions();
        }

        execOptions.env = this.getDosboxEnv();

        if (!execOptions.customCwd) {
            execOptions.customCwd = await this.newTempDir();
        }

        const tempDir = execOptions.customCwd;
        const fullArgs = this.getDosboxArgs(tempDir, args);

        const result = await exec.executeDirect(this.dosbox, fullArgs, execOptions);

        const stdoutFilename = path.join(tempDir, 'STDOUT.TXT');
        const stdout = await fs.readFile(stdoutFilename);
        result.stdout = stdout.toString('utf8');

        return result;
    }

    public override async runCompiler(
        compiler: string,
        options: string[],
        inputFilename: string,
        execOptions: ExecutionOptionsWithEnv,
    ) {
        return super.runCompiler(
            compiler,
            options.map(option => {
                if (option === inputFilename) {
                    return path.basename(option);
                } else {
                    return option;
                }
            }),
            inputFilename,
            execOptions,
        );
    }
}
