// Copyright (c) 2018, Microsoft Corporation
// 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 {ParsedAsmResult, ParsedAsmResultLine} from '../../types/asmresult/asmresult.interfaces.js';
import {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces.js';
import {assert} from '../assert.js';
import {logger} from '../logger.js';
import {PropertyGetter} from '../properties.interfaces.js';
import * as utils from '../utils.js';
import {AsmParser} from './asm-parser.js';
import {AsmRegex} from './asmregex.js';

type Source = {file: string | null; line: number | null};
type Line = {text: string; source: Source | null};
type Label = {
    lines: Line[];
    name: string | undefined;
    initialLine: number | undefined;
    file: string | null | undefined;
};

type ResultObject = {
    prefix: Line[];
    functions: Label[];
    postfix: Line | null;
};

export class VcAsmParser extends AsmParser {
    private readonly filenameComment = /^; File (.+)/;
    protected miscDirective = /^\s*(include|INCLUDELIB|TITLE|\.|THUMB|ARM64|TTL|DD|voltbl|_volmd|END$)/;
    private readonly postfixComment = /; (.*)/;
    private readonly stdData = /^`?[?_0-9$A-Z_a-z']+@std@@.* DC?[BWDQT] /;
    private readonly localLabelDef = /^([$A-Z_a-z]+) (?:=|EQU)/;
    private readonly lineNumberComment = /^; Line (\d+)/;
    protected beginSegment =
        /^(CONST|_BSS|\.?[prx]?data(\$[A-Za-z]+)?|CRT(\$[A-Za-z]+)?|_TEXT|\.?text(\$[A-Za-z]+)?)\s+SEGMENT|\s*AREA/;
    protected endSegment = /^(CONST|_BSS|[prx]?data(\$[A-Za-z]+)?|CRT(\$[A-Za-z]+)?|_TEXT|text(\$[A-Za-z]+)?)\s+ENDS/;
    private readonly beginFunction = /^; Function compile flags: /;
    private readonly endProc = /^([$?@A-Z_a-z][\w$<>?@]*)?\s+ENDP/;
    private readonly labelFind = /[$?@A-Z_a-z][\w$<>?@]*/g;

    constructor(compilerProps?: PropertyGetter) {
        super(compilerProps);
        this.commentOnly = /^;/;

        this.labelDef = /^\|?([$?@A-Z_a-z][\w$<>?@]*)\|?\s+(PROC|=|D[BWDQT])/;
        this.definesGlobal = /^\s*(PUBLIC|EXTRN|EXPORT)\s+/;
        this.definesFunction = /^\|?([$?@A-Z_a-z][\w$<>?@]*)\|?\s+PROC/;
        this.dataDefn = /^(\|?[$?@A-Z_a-z][\w$<>?@]*\|?)\sDC?[BDQW]\s|\s+DC?[BDQW]\s|\s+ORG/;

        // these are set to an impossible regex. VC-x86 does have inline assembly, but no special treatment is needed
        this.startAppBlock = this.startAsmNesting = /a^/;
        this.endAppBlock = this.endAsmNesting = /a^/;
        // same, but for CUDA
        this.cudaBeginDef = /a^/;
    }

    override hasOpcode(line: string): boolean {
        // note: cl doesn't output leading labels
        // strip comments
        line = line.split(';', 1)[0];
        // check for empty lines
        if (line.length === 0) return false;
        // check for a local label definition
        if (this.localLabelDef.test(line)) return false;
        // check for global label definitions
        if (this.definesGlobal.test(line)) return false;
        // check for data definitions
        if (this.dataDefn.test(line)) return false;
        // check for segment begin and end
        if (this.beginSegment.test(line) || this.endSegment.test(line)) return false;
        // check for function begin and end
        // note: functionBegin is used for the function compile flags comment
        if (this.definesFunction.test(line) || this.endProc.test(line)) return false;
        // check for miscellaneous directives
        if (this.miscDirective.test(line)) return false;

        return this.hasOpcodeRe.test(line);
    }

    override processBinaryAsm(asm: string, filters: ParseFiltersAndOutputOptions): ParsedAsmResult {
        // TODO: actually implement
        const result: ParsedAsmResultLine[] = [];
        const asmLines = asm.split('\n');

        for (const line of asmLines) {
            result.push({text: line, source: null});
        }

        return {
            asm: result,
        };
    }

    override processAsm(asm: string, filters: ParseFiltersAndOutputOptions): ParsedAsmResult {
        if (filters.binary || filters.binaryObject) {
            return this.processBinaryAsm(asm, filters);
        }

        const getFilenameFromComment = (line: string): string | null => {
            const matches = line.match(this.filenameComment);
            if (matches) {
                return matches[1];
            }
            return null;
        };
        const getLineNumberFromComment = (line: string) => {
            const matches = line.match(this.lineNumberComment);
            if (matches) {
                return Number.parseInt(matches[1], 10);
            }
            return null;
        };

        const asmLines = utils.splitLines(asm);
        // note: VC doesn't output unused labels, afaict

        const stdInLooking = /<stdin>|^-$|example\.[^/]+$|<source>/;

        const resultObject: ResultObject = {
            prefix: [],
            functions: [],
            postfix: null,
        };

        let currentFunction: Label | null = null;
        let currentFile: string | null = null;
        let currentLine: number | null = null;

        let seenEnd = false;

        const datadefLabels: string[] = [];
        const datadefLabelsUsed: string[] = [];

        const createSourceFor = (
            hasopc: boolean,
            currentFile: string | null,
            currentLine: number | null,
        ): Source | null => {
            if (hasopc && (currentFile || currentLine)) {
                return {
                    file: currentFile || null,
                    line: currentLine || null,
                };
            }

            return null;
        };

        const checkUsedDatadefLabels = (line: string) => {
            const labels = line.match(this.labelFind);
            if (!labels) return;
            labels.splice(0, 1);
            for (const item of labels) {
                if (datadefLabels.find(l => item === l)) {
                    datadefLabelsUsed.push(item);
                }
            }
        };

        const checkBeginFunction = (line: string) => {
            if (this.beginFunction.test(line)) {
                currentFunction = {
                    lines: [],
                    initialLine: undefined,
                    name: undefined,
                    file: undefined,
                };
                resultObject.functions.push(currentFunction);
            }
            return currentFunction;
        };

        const checkForDdefLabel = (line: string) => {
            const ddef = line.match(this.dataDefn);
            if (ddef?.[1]) {
                datadefLabels.push(ddef[1]);
            }
        };

        for (let line of asmLines) {
            if (line.trim() === 'END') {
                seenEnd = true;
                if (!filters.directives) {
                    resultObject.postfix = {text: line, source: null};
                }
                continue;
            }
            if (line.trim() === '') {
                if (seenEnd) continue;

                const emptyLine = {text: '', source: null};
                if (currentFunction === null) {
                    resultObject.prefix.push(emptyLine);
                } else {
                    currentFunction.lines.push(emptyLine);
                }
                continue;
            }
            if (seenEnd) {
                // this should never happen
                throw new Error('Visual C++: text after the end statement');
            }

            const fileName = getFilenameFromComment(line);
            if (fileName === null) {
                const lineNum = getLineNumberFromComment(line);
                if (lineNum !== null) {
                    if (currentFile === undefined) {
                        logger.error('Somehow, we have a line number comment without a file comment: %s', line);
                    }
                    assert(currentFunction);
                    if (currentFunction.initialLine === undefined) {
                        currentFunction.initialLine = lineNum;
                    }
                    currentLine = lineNum;
                }
            } else {
                if (currentFunction === null) {
                    logger.error('We have a file comment outside of a function: %s', line);
                }
                // if the file is the "main file", give it the file `null`
                if (stdInLooking.test(fileName)) {
                    currentFile = null;
                } else {
                    currentFile = fileName;
                }
                assert(currentFunction);
                if (currentFunction.file === undefined) {
                    currentFunction.file = currentFile;
                }
            }

            currentFunction = checkBeginFunction(line);

            const functionLine = line.match(this.definesFunction);
            if (functionLine) {
                if (asmLines.length === 0) {
                    continue;
                }
                assert(currentFunction);
                const functionName = line.match(this.postfixComment); // Try to extract demangled name from line comment
                if (functionName?.[1]) currentFunction.name = functionName[1];
                else currentFunction.name = functionLine[1];
            }

            if (filters.commentOnly && this.commentOnly.test(line)) continue;

            const shouldSkipDirective =
                filters.directives &&
                (this.endSegment.test(line) ||
                    this.definesGlobal.test(line) ||
                    this.miscDirective.test(line) ||
                    this.beginSegment.test(line));

            const shouldSkipLibraryCode = filters.libraryCode && currentFunction?.name?.trim().startsWith('std::');
            // Filter out lines like
            // const std::bad_alloc::`RTTI Complete Object Locator' DD 01H
            const shouldSkipLibOrDirective = (filters.directives || filters.libraryCode) && this.stdData.test(line);

            if (shouldSkipDirective || shouldSkipLibraryCode || shouldSkipLibOrDirective) {
                continue;
            }

            checkForDdefLabel(line);

            line = utils.expandTabs(line);
            const hasopc = this.hasOpcode(line);
            const textAndSource: Line = {
                text: AsmRegex.filterAsmLine(line, filters),
                source: createSourceFor(hasopc, currentFile, currentLine),
            };
            if (currentFunction === null) {
                resultObject.prefix.push(textAndSource);
            } else {
                currentFunction.lines.push(textAndSource);
            }

            checkUsedDatadefLabels(line);
        }

        return this.resultObjectIntoArray(resultObject, filters, datadefLabelsUsed);
    }

    resultObjectIntoArray(
        obj: ResultObject,
        filters: ParseFiltersAndOutputOptions,
        ddefLabelsUsed: string[],
    ): ParsedAsmResult {
        const collator = new Intl.Collator();

        obj.functions.sort((f1, f2) => {
            // order the main file above all others
            if (f1.file === null && f2.file !== null) {
                return -1;
            }
            if (f1.file !== null && f2.file === null) {
                return 1;
            }
            // order no-file below all others
            if (f1.file === undefined && f2.file !== undefined) {
                return 1;
            }
            if (f1.file !== undefined && f2.file === undefined) {
                return -1;
            }

            // if the files are the same, use line number ordering
            if (f1.file === f2.file) {
                // if the lines are the same as well, it's either:
                //   - two template instantiations, or
                //   - two compiler generated functions
                // order by name
                if (f1.initialLine === f2.initialLine) {
                    return collator.compare(f1.name || '', f2.name || '');
                }
                // NOTE: initialLine can be undefined here, that's ok
                return (f1.initialLine as number) - (f2.initialLine as number);
            }

            // else, order by file
            assert(typeof f1.file === 'string' && typeof f2.file === 'string');
            return collator.compare(f1.file, f2.file);
        });

        const result: ParsedAsmResultLine[] = [];
        let lastLineWasWhitespace = true;
        const pushLine = (line: ParsedAsmResultLine) => {
            if (line.text.trim() === '') {
                if (!lastLineWasWhitespace) {
                    result.push({text: '', source: null});
                    lastLineWasWhitespace = true;
                }
            } else {
                result.push(line);
                lastLineWasWhitespace = false;
            }
        };

        if (filters.labels) {
            let currentDdef: string | undefined;
            let isUsed = false;
            for (const line of obj.prefix) {
                const matches = line.text.match(this.dataDefn);
                if (matches) {
                    if (matches[1]) {
                        currentDdef = matches[1];
                        isUsed = !!ddefLabelsUsed.find(label => currentDdef === label);
                    }

                    if (isUsed) {
                        pushLine(line);
                    }
                } else {
                    currentDdef = undefined;
                    pushLine(line);
                }
            }
        } else {
            for (const line of obj.prefix) {
                pushLine(line);
            }
        }

        for (const func of obj.functions) {
            if (!filters.libraryCode || func.file === null) {
                pushLine({text: '', source: null});
                for (const line of func.lines) {
                    pushLine(line);
                }
            }
        }

        if (obj.postfix !== null) {
            pushLine(obj.postfix);
        }

        return {
            asm: result,
        };
    }
}
