// Copyright (c) 2019, 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 {Container} from 'golden-layout';
import $ from 'jquery';
import _ from 'underscore';

import {escapeHTML} from '../../shared/common-utils.js';
import {
    BypassCache,
    CompilationRequest,
    CompilationRequestOptions,
    CompilationResult,
    FiledataPair,
} from '../../types/compilation/compilation.interfaces.js';
import {CompilerInfo} from '../../types/compiler.interfaces.js';
import {ResultLine} from '../../types/resultline/resultline.interfaces.js';
import {Filter as AnsiToHtml} from '../ansi-to-html.js';
import {ArtifactHandler} from '../artifact-handler.js';
import * as BootstrapUtils from '../bootstrap-utils.js';
import {CompilationStatus as CompilerServiceCompilationStatus} from '../compiler-service.interfaces.js';
import {CompilerService} from '../compiler-service.js';
import {ICompilerShared} from '../compiler-shared.interfaces.js';
import {CompilerShared} from '../compiler-shared.js';
import {SourceAndFiles} from '../download-service.js';
import {Hub} from '../hub.js';
import * as LibUtils from '../lib-utils.js';
import {LanguageLibs} from '../options.interfaces.js';
import {languagesService} from '../services/languages.service.js';
import {Settings, SiteSettings} from '../settings.js';
import * as utils from '../utils.js';
import {Alert} from '../widgets/alert.js';
import {CompilerPicker} from '../widgets/compiler-picker.js';
import {CompilerVersionInfo, setCompilerVersionPopoverForPane} from '../widgets/compiler-version-info.js';
import {FontScale} from '../widgets/fontscale.js';
import {LibsWidget} from '../widgets/libs-widget.js';
import {PaneRenaming} from '../widgets/pane-renaming.js';
import * as TimingWidget from '../widgets/timing-info-widget.js';
import {Toggles} from '../widgets/toggles.js';
import {LangInfo} from './compiler-request.interfaces.js';
import {ExecutorState} from './executor.interfaces.js';
import {PaneState} from './pane.interfaces.js';
import {Pane} from './pane.js';

type CompilationStatus = Omit<CompilerServiceCompilationStatus, 'compilerOut'> & {
    didExecute?: boolean;
};

function makeAnsiToHtml(color?: string): AnsiToHtml {
    return new AnsiToHtml({
        fg: color ? color : '#333',
        bg: '#f5f5f5',
        stream: true,
        escapeXML: true,
    });
}

export class Executor extends Pane<ExecutorState> {
    private contentRoot: JQuery<HTMLElement>;
    private readonly sourceEditorId: number | null;
    private sourceTreeId: number | null;
    private readonly id: number;
    private deferCompiles: boolean;
    private needsCompile: boolean;
    private executionArguments: string;
    private executionStdin: string;
    private source: string;
    private lastTimeTaken: number;
    private pendingRequestSentAt: number;
    private pendingCMakeRequestSentAt: number;
    private nextRequest: CompilationRequest | null;
    private nextCMakeRequest: CompilationRequest | null;
    private options: string;
    private lastResult: CompilationResult | null;
    private alertSystem: Alert;
    private readonly normalAnsiToHtml: AnsiToHtml;
    private readonly errorAnsiToHtml: AnsiToHtml;
    private fontScale: FontScale;
    private compilerPicker: CompilerPicker;
    private currentLangId: string;
    private toggleWrapButton: Toggles;
    private outputContentRoot: JQuery<HTMLElement>;
    private executionStatusSection: JQuery<HTMLElement>;
    private compilerOutputSection: JQuery<HTMLElement>;
    private executionOutputSection: JQuery<HTMLElement>;
    private optionsField: JQuery<HTMLElement>;
    private execArgsField: JQuery<HTMLElement>;
    private execStdinField: JQuery<HTMLElement>;
    private prependOptions: JQuery<HTMLElement>;
    private fullCompilerName: JQuery<HTMLElement>;
    private fullTimingInfo: JQuery<HTMLElement>;
    private libsButton: JQuery<HTMLElement>;
    private compileTimeLabel: JQuery<HTMLElement>;
    private shortCompilerName: JQuery<HTMLElement>;
    private bottomBar: JQuery<HTMLElement>;
    private statusLabel: JQuery<HTMLElement>;
    private statusIcon: JQuery<HTMLElement> | null;
    private panelCompilation: JQuery<HTMLElement>;
    private panelArgs: JQuery<HTMLElement>;
    private panelStdin: JQuery<HTMLElement>;
    private wrapTitle: JQuery<HTMLElement>;
    private rerunButton: JQuery<HTMLElement>;
    private compileClearCache: JQuery<HTMLElement>;
    private wrapButton: JQuery<HTMLElement>;
    private toggleCompilation: JQuery<HTMLElement>;
    private toggleArgs: JQuery<HTMLElement>;
    private toggleStdin: JQuery<HTMLElement>;
    private toggleCompilerOut: JQuery<HTMLElement>;
    private libsWidget?: LibsWidget;
    private readonly infoByLang: Record<string, LangInfo | undefined>;
    private compiler: CompilerInfo | null;
    private compilerShared: ICompilerShared;
    private artifactHandler: ArtifactHandler;

    constructor(hub: Hub, container: Container, state: PaneState & ExecutorState) {
        super(hub, container, state);
        if (this.sourceTreeId) {
            this.sourceEditorId = null;
        } else {
            this.sourceEditorId = state.source || 1;
        }
        this.id = state.id || this.hub.nextExecutorId();

        this.contentRoot = this.domRoot.find('.content');
        this.infoByLang = {};
        this.deferCompiles = true;
        this.needsCompile = false;
        this.source = '';
        this.lastResult = {code: -1, timedOut: false, stdout: [], stderr: []};
        this.lastTimeTaken = 0;
        this.pendingRequestSentAt = 0;
        this.pendingCMakeRequestSentAt = 0;
        this.nextRequest = null;
        this.nextCMakeRequest = null;

        this.alertSystem = new Alert();
        this.alertSystem.prefixMessage = 'Executor #' + this.id;
        this.artifactHandler = new ArtifactHandler(this.alertSystem);

        this.normalAnsiToHtml = makeAnsiToHtml();
        this.errorAnsiToHtml = makeAnsiToHtml('var(--terminal-red)');

        this.initButtons(state);

        this.fontScale = new FontScale(this.domRoot, state, 'pre.content');

        this.compilerShared = new CompilerShared(this.domRoot, this.onCompilerOverridesChange.bind(this));
        this.compilerShared.updateState(state);
        this.initCallbacks();
        // Handle initial settings
        this.onSettingsChange(this.settings);

        this.postInit(state);
    }

    private async postInit(state: PaneState & ExecutorState) {
        await this.initLangAndCompiler(state);

        this.compilerPicker = new CompilerPicker(
            this.domRoot,
            this.hub,
            this.currentLangId,
            this.compiler ? this.compiler.id : '',
            this.onCompilerChange.bind(this),
            this.compilerIsVisible,
        );

        await this.initLibraries(state);
        this.updateCompilerInfo();
        this.updateState();

        if (this.sourceTreeId) {
            this.compile();
        }

        if (!this.hub.deferred) {
            this.eventHub.emit('executorOpen', this.id, this.sourceEditorId ?? false);
            this.undefer();
        }
    }

    override initializeCompilerInfo(state: PaneState) {
        this.compilerInfo = {
            compilerId: 0,
            compilerName: '',
            editorId: state.editorid,
            treeId: state.treeid,
        };
    }

    override initializeStateDependentProperties(state: PaneState & ExecutorState) {
        this.sourceTreeId = state.tree ?? null;
        this.settings = Settings.getStoredSettings();
        this.options = state.options || '';
        this.executionArguments = state.execArgs || '';
        this.executionStdin = state.execStdin || '';
        this.paneRenaming = new PaneRenaming(this, state, this.hub);
    }

    override getInitialHTML(): string {
        return $('#executor').html();
    }

    compilerIsVisible(compiler: CompilerInfo): boolean {
        return !!(compiler.supportsExecute || compiler.supportsBinary);
    }

    getEditorIdByFilename(filename: string): number | null {
        if (this.sourceTreeId) {
            const tree = this.hub.getTreeById(this.sourceTreeId);
            if (tree) {
                return tree.multifileService.getEditorIdByFilename(filename);
            }
        } else if (this.sourceEditorId) {
            return this.sourceEditorId;
        }
        return null;
    }

    async initLangAndCompiler(state: PaneState & ExecutorState): Promise<void> {
        const langId = state.lang ?? null;
        const compilerId = state.compiler;
        const result = await this.hub.compilerService.processFromLangAndCompiler(langId, compilerId);
        this.compiler = result?.compiler ?? null;
        this.currentLangId = result?.langId ?? '';
        this.updateLibraries();
    }

    close(): void {
        this.eventHub.unsubscribe();
        if (this.compilerPicker instanceof CompilerPicker) {
            this.compilerPicker.destroy();
        }

        this.eventHub.emit('executorClose', this.id);
    }

    undefer(): void {
        this.deferCompiles = false;
        if (this.needsCompile) this.compile();
    }

    override resize(): void {
        _.defer(self => {
            let topBarHeight = utils.updateAndCalcTopBarHeight(self.domRoot, $(self.topBar[0]), self.hideable);

            // We have some more elements that modify the topBarHeight
            if (!self.panelCompilation.hasClass('d-none')) {
                topBarHeight += self.panelCompilation.outerHeight(true);
            }
            if (!self.panelArgs.hasClass('d-none')) {
                topBarHeight += self.panelArgs.outerHeight(true);
            }
            if (!self.panelStdin.hasClass('d-none')) {
                topBarHeight += self.panelStdin.outerHeight(true);
            }

            const bottomBarHeight = self.bottomBar.outerHeight(true);
            self.outputContentRoot.outerHeight(self.domRoot.height() - topBarHeight - bottomBarHeight);
        }, this);
    }

    private errorResult(message: string): CompilationResult {
        return {stdout: [], timedOut: false, code: -1, stderr: [{text: message}]};
    }

    compile(bypassCache?: BypassCache): void {
        if (this.deferCompiles) {
            this.needsCompile = true;
            return;
        }
        this.needsCompile = false;
        this.compileTimeLabel.text(' - Compiling...');
        const options: CompilationRequestOptions = {
            userArguments: this.options,
            executeParameters: {
                args: this.executionArguments,
                stdin: this.executionStdin,
                runtimeTools: this.compilerShared.getRuntimeTools(),
            },
            compilerOptions: {
                executorRequest: true,
                skipAsm: true,
                overrides: this.compilerShared.getOverrides(),
            },
            filters: {execute: true},
            tools: [],
            libraries: [],
        };

        this.libsWidget?.getLibsInUse().forEach(item => {
            options.libraries.push({
                id: item.libId,
                version: item.versionId,
            });
        });

        if (this.sourceTreeId) {
            this.compileFromTree(options, bypassCache);
        } else {
            this.compileFromEditorSource(options, bypassCache);
        }
    }

    compileFromEditorSource(options: CompilationRequestOptions, bypassCache?: BypassCache): void {
        if (!this.compiler || !this.compilerIsVisible(this.compiler)) {
            this.alertSystem.notify('This compiler (' + this.compiler?.name + ') does not support execution', {
                group: 'execution',
            });
            return;
        }
        this.hub.compilerService
            .expandToFiles(this.source)
            .then((sourceAndFiles: SourceAndFiles) => {
                const request: CompilationRequest = {
                    source: sourceAndFiles.source || '',
                    compiler: this.compiler ? this.compiler.id : '',
                    options: options,
                    lang: this.currentLangId,
                    files: sourceAndFiles.files,
                };
                if (bypassCache) request.bypassCache = bypassCache;
                if (!this.compiler) {
                    this.onCompileResponse(request, this.errorResult('<Please select a compiler>'), false);
                } else {
                    this.sendCompile(request);
                }
            })
            .catch(error => {
                this.onCompileResponse(
                    {
                        source: this.source,
                        compiler: this.compiler?.id || '',
                        options: options,
                        lang: this.currentLangId,
                        files: [],
                    },
                    this.errorResult(
                        'Failed to expand includes: ' + (error instanceof Error ? error.message : String(error)),
                    ),
                    false,
                );
            });
    }

    compileFromTree(options: CompilationRequestOptions, bypassCache?: BypassCache): void {
        const tree = this.hub.getTreeById(this.sourceTreeId ?? -1);
        if (!tree) {
            this.sourceTreeId = null;
            this.compileFromEditorSource(options, bypassCache);
            return;
        }

        const request: CompilationRequest = {
            source: tree.multifileService.getMainSource(),
            compiler: this.compiler ? this.compiler.id : '',
            options: options,
            lang: this.currentLangId,
            files: tree.multifileService.getFiles(),
        };

        const fetches: Promise<void>[] = [];
        fetches.push(
            this.hub.compilerService.expandToFiles(request.source).then((sourceAndFiles: SourceAndFiles) => {
                request.source = sourceAndFiles.source;
                request.files.push(...sourceAndFiles.files);
            }),
        );

        const moreFiles: FiledataPair[] = [];
        for (let i = 0; i < request.files.length; i++) {
            const file = request.files[i];
            fetches.push(
                this.hub.compilerService.expandToFiles(file.contents).then((sourceAndFiles: SourceAndFiles) => {
                    file.contents = sourceAndFiles.source;
                    moreFiles.push(...sourceAndFiles.files);
                }),
            );
        }

        Promise.all(fetches)
            .then(() => {
                const treeState = tree.currentState();
                const cmakeProject = tree.multifileService.isACMakeProject();
                request.files.push(...moreFiles);

                if (bypassCache) request.bypassCache = bypassCache;
                if (!this.compiler) {
                    this.onCompileResponse(request, this.errorResult('<Please select a compiler>'), false);
                } else if (cmakeProject && request.source === '') {
                    this.onCompileResponse(request, this.errorResult('<Please supply a CMakeLists.txt>'), false);
                } else {
                    if (cmakeProject) {
                        request.options.compilerOptions.cmakeArgs = treeState.cmakeArgs;
                        request.options.compilerOptions.customOutputFilename = treeState.customOutputFilename;
                        this.sendCMakeCompile(request);
                    } else {
                        this.sendCompile(request);
                    }
                }
            })
            .catch(error => {
                this.onCompileResponse(
                    request,
                    this.errorResult(
                        'Failed to expand includes in files: ' +
                            (error instanceof Error ? error.message : String(error)),
                    ),
                    false,
                );
            });
    }

    sendCMakeCompile(request: CompilationRequest): void {
        const onCompilerResponse = this.onCMakeResponse.bind(this);

        if (this.pendingCMakeRequestSentAt) {
            // If we have a request pending, then just store this request to do once the
            // previous request completes.
            this.nextCMakeRequest = request;
            return;
        }
        // this.eventHub.emit('compiling', this.id, this.compiler);
        // Display the spinner
        this.handleCompilationStatus({code: 4});
        this.pendingCMakeRequestSentAt = Date.now();
        // After a short delay, give the user some indication that we're working on their
        // compilation.
        this.hub.compilerService
            .submitCMake(request)
            .then((x: any) => {
                onCompilerResponse(request, x.result, x.localCacheHit);
            })
            .catch(x => {
                let message = 'Unknown error';
                const isNetworkError = (x as any)?.isNetworkError === true;
                if (_.isString(x)) {
                    message = x;
                } else if (x) {
                    message = x.error || x.code || x.message || x;
                }
                const result = isNetworkError
                    ? {...this.errorResult(message), networkError: true}
                    : this.errorResult(message);
                onCompilerResponse(request, result, false);
            });
    }

    sendCompile(request: CompilationRequest): void {
        const onCompilerResponse = this.onCompileResponse.bind(this);

        if (this.pendingRequestSentAt) {
            // If we have a request pending, then just store this request to do once the
            // previous request completes.
            this.nextRequest = request;
            return;
        }
        // this.eventHub.emit('compiling', this.id, this.compiler);
        // Display the spinner
        this.handleCompilationStatus({code: 4});
        this.pendingRequestSentAt = Date.now();
        // After a short delay, give the user some indication that we're working on their
        // compilation.
        this.hub.compilerService
            .submit(request)
            .then((x: any) => {
                onCompilerResponse(request, x.result, x.localCacheHit);
            })
            .catch(x => {
                let message = 'Unknown error';
                const isNetworkError = (x as any)?.isNetworkError === true;
                if (typeof x === 'string') {
                    message = x;
                } else if (x) {
                    message = x.error || x.code || x.message || x;
                }
                const result = isNetworkError
                    ? {...this.errorResult(message), networkError: true}
                    : this.errorResult(message);
                onCompilerResponse(request, result, false);
            });
    }

    addCompilerOutputLine(
        msg: string,
        container: JQuery,
        lineNum: number | undefined,
        column: number | undefined,
        addLineLinks: boolean,
        filename: string | null,
    ): void {
        const elem = $('<div/>').appendTo(container);
        if (addLineLinks && lineNum) {
            elem.empty();
            $('<span class="linked-compiler-output-line"></span>')
                .html(msg)
                .on('click', e => {
                    if (this.hasActiveSelection()) {
                        return;
                    }
                    const editorId = this.getEditorIdByFilename(filename ?? '');
                    if (editorId) {
                        this.eventHub.emit('editorLinkLine', editorId, lineNum, column ?? 0, (column ?? 0) + 1, true);
                    }
                    // do not bring user to the top of index.html
                    // http://stackoverflow.com/questions/3252730
                    e.preventDefault();
                    return false;
                })
                .on('mouseover', () => {
                    const editorId = this.getEditorIdByFilename(filename ?? '');
                    if (editorId) {
                        this.eventHub.emit('editorLinkLine', editorId, lineNum, column ?? 0, (column ?? 0) + 1, false);
                    }
                })
                .appendTo(elem);
        } else {
            elem.html(msg);
        }
    }

    clearPreviousOutput(): void {
        this.executionStatusSection.empty();
        this.compilerOutputSection.empty();
        this.executionOutputSection.empty();
    }

    handleOutput(
        output: ResultLine[],
        element: JQuery<HTMLElement>,
        ansiParser: AnsiToHtml,
        addLineLinks: boolean,
    ): JQuery<HTMLElement> {
        const outElem = $('<pre class="card execution-stdoutstderr"></pre>').appendTo(element);
        output.forEach(obj => {
            // Bug #8152: output lines with only null characters should be rendered as empty lines
            if (obj.text === '' || (obj.text.includes('\x00') && obj.text.replace(/\x00/g, '') === '')) {
                this.addCompilerOutputLine('<br/>', outElem, undefined, undefined, false, null);
            } else {
                const lineNumber = obj.tag ? obj.tag.line : obj.line;
                const columnNumber = obj.tag ? obj.tag.column : -1;
                const filename = obj.tag ? obj.tag.file : false;
                this.addCompilerOutputLine(
                    ansiParser.toHtml(obj.text),
                    outElem,
                    lineNumber,
                    columnNumber,
                    addLineLinks,
                    filename || null,
                );
            }
        });
        return outElem;
    }

    getBuildStdoutFromResult(result: CompilationResult): ResultLine[] {
        let arr: ResultLine[] = [];

        if (result.buildResult) {
            arr = arr.concat(result.buildResult.stdout);
        }

        if (result.buildsteps) {
            result.buildsteps.forEach(step => {
                arr = arr.concat(step.stdout);
            });
        }

        return arr;
    }

    getBuildStderrFromResult(result: CompilationResult): ResultLine[] {
        let arr: ResultLine[] = [];

        if (result.buildResult) {
            arr = arr.concat(result.buildResult.stderr);
        }

        if (result.buildsteps) {
            result.buildsteps.forEach(step => {
                arr = arr.concat(step.stderr);
            });
        }

        return arr;
    }

    getExecutionStdoutfromResult(result: CompilationResult): ResultLine[] {
        if (result.execResult) {
            return result.execResult.stdout;
        }

        return result.stdout || [];
    }

    getExecutionStderrfromResult(result: CompilationResult): ResultLine[] {
        if (result.execResult) {
            return result.execResult.stderr;
        }

        return result.stderr || [];
    }

    onCMakeResponse(request: CompilationRequest, result: CompilationResult, cached: boolean): void {
        result.source = this.source;
        this.lastResult = result;
        const timeTaken = Math.max(0, Date.now() - this.pendingCMakeRequestSentAt);
        this.lastTimeTaken = timeTaken;
        const wasRealReply = this.pendingCMakeRequestSentAt > 0;
        this.pendingCMakeRequestSentAt = 0;

        this.handleCompileRequestAndResponse(request, result, cached, wasRealReply, timeTaken);

        this.doNextCMakeRequest();
    }

    doNextCompileRequest(): void {
        if (this.nextRequest) {
            const next = this.nextRequest;
            this.nextRequest = null;
            this.sendCompile(next);
        }
    }

    doNextCMakeRequest(): void {
        if (this.nextCMakeRequest) {
            const next = this.nextCMakeRequest;
            this.nextCMakeRequest = null;
            this.sendCMakeCompile(next);
        }
    }

    handleCompileRequestAndResponse(
        request: CompilationRequest,
        result: CompilationResult,
        cached: boolean,
        wasRealReply: boolean,
        timeTaken: number,
    ): void {
        this.clearPreviousOutput();
        const compileStdout = this.getBuildStdoutFromResult(result);
        const compileStderr = this.getBuildStderrFromResult(result);
        const execStdout = this.getExecutionStdoutfromResult(result);
        const execStderr = this.getExecutionStderrfromResult(result);

        let buildResultCode = 0;

        if (result.buildResult) {
            buildResultCode = result.buildResult.code;
        } else if (result.buildsteps) {
            result.buildsteps.forEach(step => {
                buildResultCode = step.code;
            });
        }

        if (!result.didExecute) {
            if (!result.networkError) {
                this.executionStatusSection.append($('<div/>').text('Could not execute the program'));
            }
            if (execStderr.length > 0) {
                this.handleOutput(execStderr, this.executionStatusSection, this.normalAnsiToHtml, false);
            }
            if (!result.networkError) {
                this.executionStatusSection.append($('<div/>').text('Compiler returned: ' + buildResultCode));
            }
        }
        // reset stream styles
        this.normalAnsiToHtml.reset();
        this.errorAnsiToHtml.reset();
        if (compileStdout.length > 0) {
            this.compilerOutputSection.append($('<div/>').text('Compiler stdout'));
            this.handleOutput(compileStdout, this.compilerOutputSection, this.normalAnsiToHtml, true);
        }
        if (compileStderr.length > 0) {
            this.compilerOutputSection.append($('<div/>').text('Compiler stderr'));
            this.handleOutput(compileStderr, this.compilerOutputSection, this.errorAnsiToHtml, true);
        }
        if (result.didExecute) {
            const exitCode = result.execResult ? result.execResult.code : result.code;
            this.executionOutputSection.append($('<div/>').text('Program returned: ' + exitCode));
            if (execStdout.length > 0) {
                this.executionOutputSection.append($('<div/>').text('Program stdout'));
                const outElem = this.handleOutput(
                    execStdout,
                    this.executionOutputSection,
                    this.normalAnsiToHtml,
                    false,
                );
                outElem.addClass('execution-stdout');
            }
            if (execStderr.length > 0) {
                this.executionOutputSection.append($('<div/>').text('Program stderr'));
                this.handleOutput(execStderr, this.executionOutputSection, this.normalAnsiToHtml, false);
            }
        }

        this.handleCompilationStatus({code: 1, didExecute: result.didExecute});
        let timeLabelText = '';
        if (cached) {
            timeLabelText = ' - cached';
        } else if (wasRealReply) {
            timeLabelText = ' - ' + timeTaken + 'ms';
        }
        this.compileTimeLabel.text(timeLabelText);

        this.setCompilationOptionsPopover(result.buildResult ? result.buildResult.compilationOptions.join(' ') : '');

        if (this.currentLangId) {
            const languages = languagesService.getLanguagesOrFail();
            this.eventHub.emit('executeResult', this.id, this.compiler, result, languages[this.currentLangId]);
        }

        this.offerFilesIfPossible(result);
    }

    offerFilesIfPossible(result: CompilationResult) {
        this.artifactHandler.handle(result);
    }

    onCompileResponse(request: CompilationRequest, result: CompilationResult, cached: boolean): void {
        // Save which source produced this change. It should probably be saved earlier though
        result.source = this.source;
        this.lastResult = result;
        const timeTaken = Math.max(0, Date.now() - this.pendingRequestSentAt);
        this.lastTimeTaken = timeTaken;
        const wasRealReply = this.pendingRequestSentAt > 0;
        this.pendingRequestSentAt = 0;

        this.handleCompileRequestAndResponse(request, result, cached, wasRealReply, timeTaken);

        this.doNextCompileRequest();
    }

    resendResult(): boolean {
        if (!$.isEmptyObject(this.lastResult)) {
            const languages = languagesService.getLanguagesOrFail();
            this.eventHub.emit('executeResult', this.id, this.compiler, this.lastResult, languages[this.currentLangId]);
            return true;
        }
        return false;
    }

    onResendExecutionResult(id: number): void {
        if (id === this.id) {
            this.resendResult();
        }
    }

    onEditorChange(editor: number, source: string, langId: string, compilerId?: number): void {
        if (this.sourceTreeId) {
            const tree = this.hub.getTreeById(this.sourceTreeId);
            if (tree) {
                if (tree.multifileService.isEditorPartOfProject(editor)) {
                    if (this.settings.compileOnChange) {
                        this.compile();

                        return;
                    }
                }
            }
        }

        if (editor === this.sourceEditorId && langId === this.currentLangId && compilerId === undefined) {
            this.source = source;
            if (this.settings.compileOnChange) {
                this.compile();
            }
        }
    }

    initButtons(state: PaneState & ExecutorState): void {
        this.outputContentRoot = this.domRoot.find('pre.content');
        this.executionStatusSection = this.outputContentRoot.find('.execution-status');
        this.compilerOutputSection = this.outputContentRoot.find('.compiler-output');
        this.executionOutputSection = this.outputContentRoot.find('.execution-output');
        this.toggleWrapButton = new Toggles(this.domRoot.find('.options'), state as unknown as Record<string, boolean>);

        this.optionsField = this.domRoot.find('.compilation-options');
        this.execArgsField = this.domRoot.find('.execution-arguments');
        this.execStdinField = this.domRoot.find('.execution-stdin');
        this.prependOptions = this.domRoot.find('.prepend-options');
        this.fullCompilerName = this.domRoot.find('.full-compiler-name');
        this.fullTimingInfo = this.domRoot.find('.full-timing-info');
        this.setCompilationOptionsPopover(this.compiler?.options ?? null);

        this.compileTimeLabel = this.domRoot.find('.compile-time');
        this.libsButton = this.domRoot.find('.btn.show-libs');

        // Dismiss on any click that isn't either in the opening element, inside
        // the popover or on any alert
        $(document).on('mouseup', e => {
            const target = $(e.target);
            if (
                !target.is(this.prependOptions) &&
                this.prependOptions.has(target as any).length === 0 &&
                target.closest('.popover').length === 0
            ) {
                const popover = BootstrapUtils.getPopoverInstance(this.prependOptions);
                if (popover) popover.hide();
            }

            if (
                !target.is(this.fullCompilerName) &&
                this.fullCompilerName.has(target as any).length === 0 &&
                target.closest('.popover').length === 0
            ) {
                const popover = BootstrapUtils.getPopoverInstance(this.fullCompilerName);
                if (popover) popover.hide();
            }
        });

        this.optionsField.val(this.options);
        this.execArgsField.val(this.executionArguments);
        this.execStdinField.val(this.executionStdin);

        this.shortCompilerName = this.domRoot.find('.short-compiler-name');
        this.setCompilerVersionPopover();

        this.topBar = this.domRoot.find('.top-bar');
        this.bottomBar = this.domRoot.find('.bottom-bar');
        this.statusLabel = this.domRoot.find('.status-text');

        this.hideable = this.domRoot.find('.hideable');
        this.statusIcon = this.domRoot.find('.status-icon');

        this.panelCompilation = this.domRoot.find('.panel-compilation');
        this.panelArgs = this.domRoot.find('.panel-args');
        this.panelStdin = this.domRoot.find('.panel-stdin');

        this.wrapButton = this.domRoot.find('.wrap-lines');
        this.wrapTitle = this.wrapButton.prop('title');

        this.rerunButton = this.bottomBar.find('.rerun');
        this.compileClearCache = this.bottomBar.find('.clear-cache');

        this.initToggleButtons(state);
    }

    initToggleButtons(state: PaneState & ExecutorState): void {
        this.toggleCompilation = this.domRoot.find('.toggle-compilation');
        this.toggleArgs = this.domRoot.find('.toggle-args');
        this.toggleStdin = this.domRoot.find('.toggle-stdin');
        this.toggleCompilerOut = this.domRoot.find('.toggle-compilerout');

        if (!state.compilationPanelShown) {
            this.hidePanel(this.toggleCompilation, this.panelCompilation);
        }

        if (state.argsPanelShown) {
            this.showPanel(this.toggleArgs, this.panelArgs);
        }

        if (state.stdinPanelShown) {
            this.showPanel(this.toggleStdin, this.panelStdin);
        }

        if (!state.compilerOutShown) {
            this.hidePanel(this.toggleCompilerOut, this.compilerOutputSection);
        }

        if (state.wrap === true) {
            this.contentRoot.addClass('wrap');
            this.wrapButton.prop('title', '[ON] ' + this.wrapTitle);
        } else {
            this.contentRoot.removeClass('wrap');
            this.wrapButton.prop('title', '[OFF] ' + this.wrapTitle);
        }
    }

    onLibsChanged(): void {
        this.updateState();
        this.compile();
    }

    async initLibraries(state: PaneState & ExecutorState): Promise<void> {
        this.libsWidget = new LibsWidget(
            this.currentLangId,
            this.compiler,
            this.libsButton,
            state,
            this.onLibsChanged.bind(this),
            await LibUtils.getSupportedLibraries(
                this.compiler ? this.compiler.libsArr : [],
                this.currentLangId,
                this.compiler?.remote ?? undefined,
            ),
        );
        // Wait for the lazily-loaded libraries to be selected before compiling or persisting state.
        await this.libsWidget.stateLoaded;
    }

    onFontScale(): void {
        this.updateState();
    }

    initListeners(): void {
        // this.filters.on('change', this.onFilterChange.bind(this));
        this.fontScale.on('change', this.onFontScale.bind(this));
        this.eventHub.on('renamePane', this.updateState.bind(this));
        this.toggleWrapButton.on('change', this.onToggleWrapChange.bind(this));

        this.container.on('destroy', this.close, this);
        this.container.on('resize', this.resize, this);
        this.container.on('shown', this.resize, this);
        this.container.on('open', () => {
            this.eventHub.emit('executorOpen', this.id, this.sourceEditorId ?? false);
        });
        this.eventHub.on('editorChange', this.onEditorChange, this);
        this.eventHub.on('editorClose', this.onEditorClose, this);
        this.eventHub.on('settingsChange', this.onSettingsChange, this);
        this.eventHub.on('requestCompilation', this.onRequestCompilation, this);
        this.eventHub.on('resendExecution', this.onResendExecutionResult, this);
        this.eventHub.on('resize', this.resize, this);
        this.eventHub.on('findExecutors', this.sendExecutor, this);
        this.eventHub.on('languageChange', this.onLanguageChange, this);

        this.fullTimingInfo.off('click').on('click', () => {
            TimingWidget.displayCompilationTiming(this.lastResult, this.lastTimeTaken);
        });
    }

    showPanel(button: JQuery<HTMLElement>, panel: JQuery<HTMLElement>): void {
        panel.removeClass('d-none');
        button.addClass('active');
        this.resize();
    }

    hidePanel(button: JQuery<HTMLElement>, panel: JQuery<HTMLElement>): void {
        panel.addClass('d-none');
        button.removeClass('active');
        this.resize();
    }

    togglePanel(button: JQuery<HTMLElement>, panel: JQuery<HTMLElement>): void {
        if (panel.hasClass('d-none')) {
            this.showPanel(button, panel);
        } else {
            this.hidePanel(button, panel);
        }
        this.updateState();
    }

    initCallbacks(): void {
        this.initListeners();

        const optionsChange = _.debounce(e => {
            this.onOptionsChange($(e.target).val() as string);
        }, 800);

        const execArgsChange = _.debounce(e => {
            this.onExecArgsChange($(e.target).val() as string);
        }, 800);

        const execStdinChange = _.debounce(e => {
            this.onExecStdinChange($(e.target).val() as string);
        }, 800);

        this.optionsField.on('change', optionsChange).on('keyup', optionsChange);

        this.execArgsField.on('change', execArgsChange).on('keyup', execArgsChange);

        this.execStdinField.on('change', execStdinChange).on('keyup', execStdinChange);

        // Dismiss the popover on escape.
        $(document).on('keyup.editable', e => {
            if (e.which === 27) {
                const popover = BootstrapUtils.getPopoverInstance(this.libsButton);
                if (popover) popover.hide();
            }
        });

        this.toggleCompilation.on('click', () => {
            this.togglePanel(this.toggleCompilation, this.panelCompilation);
        });

        this.toggleArgs.on('click', () => {
            this.togglePanel(this.toggleArgs, this.panelArgs);
        });

        this.toggleStdin.on('click', () => {
            this.togglePanel(this.toggleStdin, this.panelStdin);
        });

        this.toggleCompilerOut.on('click', () => {
            this.togglePanel(this.toggleCompilerOut, this.compilerOutputSection);
        });

        this.rerunButton.on('click', () => {
            this.compile(BypassCache.Execution);
        });

        this.compileClearCache.on('click', () => {
            this.compile(BypassCache.Compilation);
        });

        // Dismiss on any click that isn't either in the opening element, inside
        // the popover or on any alert
        $(document).on('click', e => {
            const elem = this.libsButton;
            const target = $(e.target);
            if (!target.is(elem) && elem.has(target as any).length === 0 && target.closest('.popover').length === 0) {
                const popover = BootstrapUtils.getPopoverInstance(elem);
                if (popover) popover.hide();
            }
        });

        this.eventHub.on('initialised', this.undefer, this);

        if (MutationObserver !== undefined) {
            new MutationObserver(this.resize.bind(this)).observe(this.execStdinField[0], {
                attributes: true,
                attributeFilter: ['style'],
            });
        }
    }

    shouldEmitExecutionOnFieldChange(): boolean {
        return this.settings.executorCompileOnChange;
    }

    doTypicalOnChange() {
        this.updateState();
        if (this.shouldEmitExecutionOnFieldChange()) {
            this.compile();
        }
    }

    onOptionsChange(options: string): void {
        this.options = options;
        this.doTypicalOnChange();
    }

    onExecArgsChange(args: string): void {
        this.executionArguments = args;
        this.doTypicalOnChange();
    }

    onCompilerOverridesChange(): void {
        this.doTypicalOnChange();
    }

    onRuntimeToolsChange(): void {
        this.doTypicalOnChange();
    }

    onExecStdinChange(newStdin: string): void {
        this.executionStdin = newStdin;
        this.doTypicalOnChange();
    }

    onRequestCompilation(editorId: number | boolean, treeId: number | boolean): void {
        if (editorId === this.sourceEditorId || (treeId && treeId === this.sourceTreeId)) {
            this.compile();
        }
    }

    updateCompilerInfo(): void {
        this.updateCompilerName();
        if (this.compiler) {
            if (this.compiler.notification) {
                this.alertSystem.notify(this.compiler.notification, {
                    group: 'compilerwarning',
                    alertClass: 'notification-info',
                    dismissTime: 5000,
                });
            }
            this.prependOptions.data('content', this.compiler.options);
        }
        this.sendExecutor();
    }

    updateCompilerUI(): void {
        this.updateCompilerInfo();
        // Resize in case the new compiler name is too big
        this.resize();
    }

    async onCompilerChange(value: string): Promise<void> {
        this.compiler = await this.hub.compilerService.findCompiler(this.currentLangId, value);
        this.updateLibraries();
        this.updateState();
        this.compile();
        this.updateCompilerUI();
    }

    onToggleWrapChange(): void {
        const state = this.getCurrentState();
        this.contentRoot.toggleClass('wrap', state.wrap);
        this.wrapButton.prop('title', '[' + (state.wrap ? 'ON' : 'OFF') + '] ' + this.wrapTitle);
        this.updateState();
    }

    sendExecutor(): void {
        this.eventHub.emit(
            'executor',
            this.id,
            this.compiler,
            this.options,
            this.sourceEditorId ?? -1,
            this.sourceTreeId ?? -1,
        );
    }

    onEditorClose(editor: number): void {
        if (editor === this.sourceEditorId) {
            // We can't immediately close as an outer loop somewhere in GoldenLayout is iterating over
            // the hierarchy. We can't modify while it's being iterated over.
            this.close();
            _.defer(self => {
                self.container.close();
            }, this);
        }
    }

    override getCurrentState(): ExecutorState & PaneState {
        const state: ExecutorState & PaneState = {
            id: this.id,
            compilerName: '',
            compiler: this.compiler ? this.compiler.id : '',
            source: this.sourceEditorId ?? undefined,
            tree: this.sourceTreeId ?? undefined,
            options: this.options,
            execArgs: this.executionArguments,
            execStdin: this.executionStdin,
            libs: this.libsWidget?.get(),
            lang: this.currentLangId,
            compilationPanelShown: !this.panelCompilation.hasClass('d-none'),
            compilerOutShown: !this.compilerOutputSection.hasClass('d-none'),
            argsPanelShown: !this.panelArgs.hasClass('d-none'),
            stdinPanelShown: !this.panelStdin.hasClass('d-none'),
            wrap: this.toggleWrapButton.get().wrap,
            overrides: this.compilerShared.getOverrides(),
            runtimeTools: this.compilerShared.getRuntimeTools(),
        };

        this.paneRenaming.addState(state);
        this.fontScale.addState(state);
        return state;
    }

    override updateState(): void {
        const state = this.getCurrentState();
        this.container.setState(state);
        this.compilerShared.updateState(state);
    }

    getCompilerName(): string {
        return this.compiler ? this.compiler.name : 'No compiler set';
    }

    getLanguageName(): string {
        const languages = languagesService.getLanguagesOrFail();
        const lang = this.currentLangId ? languages[this.currentLangId] : undefined;
        return lang ? lang.name : '?';
    }

    getLinkHint(): string {
        if (this.sourceTreeId) {
            return 'Tree #' + this.sourceTreeId;
        }
        return 'Editor #' + this.sourceEditorId;
    }

    override getPaneName(): string {
        const langName = this.getLanguageName();
        const compName = this.getCompilerName();
        return 'Executor ' + compName + ' (' + langName + ', ' + this.getLinkHint() + ')';
    }

    override updateTitle(): void {
        const name = this.paneName ? this.paneName : this.getPaneName();
        this.container.setTitle(escapeHTML(name));
    }

    updateCompilerName() {
        this.updateTitle();
        const compilerName = this.getCompilerName();
        const compilerVersion = this.compiler?.version ?? '';
        const compilerFullVersion = this.compiler?.fullVersion ?? compilerVersion;
        const compilerNotification = this.compiler?.notification ?? '';
        this.shortCompilerName.text(compilerName);
        this.setCompilerVersionPopover(
            {
                version: compilerVersion,
                fullVersion: compilerFullVersion,
            },
            compilerNotification,
            this.compiler?.id,
        );
    }

    setCompilationOptionsPopover(content: string | null) {
        // Dispose of existing popover
        const existingPopover = BootstrapUtils.getPopoverInstance(this.prependOptions);
        if (existingPopover) existingPopover.dispose();

        // Initialize new popover
        BootstrapUtils.initPopover(this.prependOptions, {
            content: content || 'No options in use',
            template:
                '<div class="popover' +
                (content ? ' compiler-options-popover' : '') +
                '" role="tooltip"><div class="arrow"></div>' +
                '<h3 class="popover-header"></h3><div class="popover-body"></div></div>',
        });
    }

    setCompilerVersionPopover(version?: CompilerVersionInfo, notification?: string, compilerId?: string) {
        setCompilerVersionPopoverForPane(this, version, notification, compilerId);
    }

    override onSettingsChange(newSettings: SiteSettings): void {
        this.settings = _.clone(newSettings);
    }

    private ariaLabel(status: CompilationStatus): string {
        // Compiling...
        if (status.code === 4) return 'Compiling';
        if (status.didExecute) {
            return 'Program compiled & executed';
        }
        return 'Program could not be executed';
    }

    private color(status: CompilationStatus) {
        // Compiling...
        if (status.code === 4) return '#888888';
        if (status.didExecute) return '#12BB12';
        return '#FF1212';
    }

    // TODO: Duplicate with compiler-service.ts?
    handleCompilationStatus(status: CompilationStatus): void {
        // We want to do some custom styles for the icon, so we don't pass it here and instead do it later
        CompilerService.handleCompilationStatus(this.statusLabel, null, {compilerOut: 0, ...status});

        if (this.statusIcon != null) {
            this.statusIcon
                .removeClass()
                .addClass('status-icon fas')
                .css('color', this.color(status))
                .toggle(status.code !== 0)
                .attr('aria-label', this.ariaLabel(status))
                .toggleClass('fa-spinner fa-spin', status.code === 4)
                .toggleClass('fa-times-circle', status.code !== 4 && !status.didExecute)
                .toggleClass('fa-check-circle', status.code !== 4 && status.didExecute);
        }
    }

    async updateLibraries(): Promise<void> {
        if (this.libsWidget) {
            let filteredLibraries: LanguageLibs = {};
            if (this.compiler) {
                filteredLibraries = await LibUtils.getSupportedLibraries(
                    this.compiler.libsArr,
                    this.currentLangId || '',
                    this.compiler.remote ?? undefined,
                );
            }

            this.libsWidget.setNewLangId(this.currentLangId, this.compiler?.id ?? '', filteredLibraries);
        }
    }

    onLanguageChange(editorId: number | boolean, newLangId: string): void {
        if (this.sourceEditorId === editorId && this.currentLangId) {
            const languages = languagesService.getLanguagesOrFail();
            const oldLangId = this.currentLangId;
            this.currentLangId = newLangId;
            // Store the current selected stuff to come back to it later in the same session (Not state stored!)
            this.infoByLang[oldLangId] = {
                compiler: this.compiler?.id ? this.compiler.id : (languages[oldLangId]?.defaultCompiler ?? ''),
                options: this.options,
                execArgs: this.executionArguments,
                execStdin: this.executionStdin,
            };
            const info = this.infoByLang[this.currentLangId];
            this.initLangAndCompiler({compilerName: '', id: 0, lang: newLangId, compiler: info?.compiler ?? ''}).then(
                () => {
                    this.updateCompilersSelector(info);
                    this.updateCompilerUI();
                    this.updateState();
                },
            );
        }
    }

    async getCurrentLangCompilers(): Promise<CompilerInfo[]> {
        const allCompilers = await this.hub.compilerService.getCompilersForLang(this.currentLangId);
        if (!allCompilers || Object.keys(allCompilers).length === 0) return [];

        const hasAtLeastOneExecuteSupported = Object.values(allCompilers).some(compiler =>
            this.compilerIsVisible(compiler),
        );

        if (!hasAtLeastOneExecuteSupported) {
            this.compiler = null;
            return [];
        }

        return Object.values(allCompilers).filter(compiler => {
            return (
                (compiler.hidden !== true && this.compilerIsVisible(compiler)) ||
                (this.compiler && compiler.id === this.compiler.id)
            );
        });
    }

    updateCompilersSelector(info: LangInfo | undefined): void {
        this.compilerPicker.update(this.currentLangId, this.compiler?.id ?? '');
        this.options = info?.options || '';
        this.optionsField.val(this.options);
        this.executionArguments = info?.execArgs || '';
        this.execArgsField.val(this.executionArguments);
        this.executionStdin = info?.execStdin || '';
        this.execStdinField.val(this.executionStdin);
    }

    getDefaultPaneName(): string {
        return '';
    }

    onCompileResult(compilerId: number, compiler: CompilerInfo, result: CompilationResult): void {}

    onCompiler(compilerId: number, compiler: CompilerInfo, options: string, editorId: number, treeId: number): void {}
}
