// 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.

const BaseCompiler = require('../base-compiler'),
    argumentParsers = require("./argument-parsers"),
    fs = require('fs-extra'),
    utils = require('../utils.js'),
    path = require('path'),
    logger = require('../logger').logger;

class JavaCompiler extends BaseCompiler {
    constructor(compilerInfo, env) {
        // Default is to disable all "cosmetic" filters
        if (!compilerInfo.disabledFilters) {
            compilerInfo.disabledFilters = ['labels', 'directives', 'commentOnly', 'trim'];
        }

        super(compilerInfo, env);
    }


    objdump(outputFilename, result, maxSize) {
        const dirPath = path.dirname(outputFilename);
        return fs.readdir(dirPath)
            .then(files => {
                logger.verbose("Class files: ", files);
                return files.filter(f => f.endsWith('.class'));
            })
            .then(classFiles => {
                return Promise.all(classFiles.map(classFile => {
                    const args = [
                        // Prints out disassembled code, i.e., the instructions that comprise the Java bytecodes,
                        // for each of the methods in the class.
                        '-c',
                        // Prints out line and local variable tables.
                        '-l',
                        classFile
                    ];
                    return this.exec(this.compiler.objdumper, args, {maxOutput: maxSize, customCwd: dirPath})
                        .then(objResult => {
                            const oneResult = {};
                            oneResult.asm = objResult.stdout;
                            if (objResult.code !== 0) {
                                oneResult.asm = "<No output: javap returned " + objResult.code + ">";
                            }
                            return oneResult;
                        });
                }));
            })
            .then(results => {
                const merged = {asm: []};
                for (const result of results) {
                    const asmBackup = merged.asm;
                    Object.assign(merged, result);
                    merged.asm = asmBackup;
                    merged.asm.push(result.asm);
                }

                result.asm = merged.asm;
                return result;
            });
    }


    optionsForFilter(filters) {
        // Forcibly enable javap
        filters.binary = true;

        return [
            '-Xlint:all'
        ];
    }


    getArgumentParser() {
        return argumentParsers.Java;
    }

    getOutputFilename(dirPath) {
        return path.join(dirPath, `${path.basename(this.compileFilename, this.lang.extensions[0])}.class`);
    }


    filterUserOptions(userOptions) {
        const filteredOptions = [];
        let toSkip = 0;

        const oneArgBlacklist = [
            // -d directory
            // Sets the destination directory for class files.
            "-d",
            // -s directory
            // Specifies the directory used to place the generated source files.
            "-s",
            // --source-path path or -sourcepath path
            // Specifies where to find input source files.
            "--source-path", "-sourcepath"];
        for (const userOption of userOptions) {
            if (toSkip > 0) {
                toSkip--;
                continue;
            }
            if (oneArgBlacklist.indexOf(userOption) !== -1) {
                toSkip = 1;
                continue;
            }

            filteredOptions.push(userOption);
        }

        return filteredOptions;
    }

    processAsm(result) {
        // Handle "error" documents.
        if (result.asm.indexOf("\n") === -1 && result.asm[0] === '<') {
            return [{text: result.asm, source: null}];
        }

        // result.asm is an array of javap stdouts
        const parseds = result.asm.map(asm => this.parseAsmForClass(asm));
        // Sort class file outputs according to first source line they reference
        parseds.sort((o1, o2) => o1.firstSourceLine - o2.firstSourceLine);

        const segments = [];
        parseds.forEach((parsed, classNumber) => {
            if (classNumber > 0) {
                // Separate classes with two line breaks
                segments.push({text: "", source: null});
                segments.push({text: "", source: null});
            }
            for (let i = 0; i < parsed.textsBeforeMethod.length; i++) {
                // Line-based highlighting doesn't work if some segments span multiple lines,
                // even if they don't have a source line number
                // -> split the lines and create segment for each separately
                for (const line of utils.splitLines(parsed.textsBeforeMethod[i])) {
                    // javap output always starts with "Compiled from" on first line, discard these lines.
                    if (line.startsWith("Compiled from")) {
                        continue;
                    }
                    segments.push({
                        text: line,
                        source: null
                    });
                }

                // textsBeforeMethod[last] is actually *after* the last method.
                // Check whether there is a method following the text block
                if (i < parsed.methods.length) {
                    parsed.methods[i].instructions.forEach(({text, sourceLine}) => {
                        segments.push({text: text, source: {file: null, line: sourceLine}});
                    });
                }
            }
        });
        return segments;
    }

    parseAsmForClass(javapOut) {
        const textsBeforeMethod = [];
        const methods = [];
        // javap output puts `    Code:` after every signature. (Line will not be shown to user)
        // We use this to find the individual methods.
        // Before the first `Code:` occurrence, there is the method signature as well as the name of the class.
        // Subsequent matches are always followed by lists of assembly instructions as well as line info mappings

        // Regex idea: make sure `Code:` is the only thing on the line. Also consume trailing line ending!
        const [classNameAndFirstMethodSignature, ...codeAndLineNumberTables] = javapOut.split(/^\s+Code:\s*$\r?\n/m);
        textsBeforeMethod.push(classNameAndFirstMethodSignature.trimEnd()); // possible trailing \r on windows

        for (const codeAndLineNumberTable of codeAndLineNumberTables) {
            const method = {
                instructions: []
            };
            methods.push(method);


            for (const codeLineCandidate of utils.splitLines(codeAndLineNumberTable)) {
                // Match
                //       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
                const match = codeLineCandidate.match(/\s+(\d+): (.*)/);
                if (match) {
                    const instrOffset = Number.parseInt(match[1]);
                    method.instructions.push({
                        instrOffset: instrOffset,
                        // Should an instruction ever not be followed by a line number table,
                        // it might contain a trailing \r on Windows -> trim it, otherwise this would not be necessary
                        text: codeLineCandidate.trimRight()
                    });
                } else {
                    break;
                }
            }


            let lineRegex = /line\s*(\d+):\s*(\d+)/g;
            let m;
            let currentInstr = 0;
            let currentSourceLine = -1;
            let lastIndex = -1;
            do {
                m = lineRegex.exec(codeAndLineNumberTable);
                if (m) {
                    // If exec doesn't find a match anymore, lineRegex.lastIndex will be reset to 0
                    // therefore, cache value here on match
                    lastIndex = lineRegex.lastIndex;
                    const [, sourceLineS, instructionS] = m;
                    logger.verbose("Found source mapping: ", sourceLineS, "to instruction", instructionS);
                    const instrOffset = Number.parseInt(instructionS);

                    // Some instructions don't receive an explicit line number.
                    // They are all assigned to the previous explicit line number,
                    // because the line consists of multiple instructions.
                    while (currentInstr < method.instructions.length &&
                    method.instructions[currentInstr].instrOffset !== instrOffset) {

                        if (currentSourceLine !== -1) {
                            // instructions without explicit line number get assigned the last explicit/same line number
                            method.instructions[currentInstr].sourceLine = currentSourceLine;
                        } else {
                            logger.error("Skipping over instruction even though currentSourceLine == -1");
                        }
                        currentInstr++;
                    }

                    const sourceLine = Number.parseInt(sourceLineS);
                    currentSourceLine = sourceLine;
                    method.instructions[currentInstr].sourceLine = currentSourceLine;

                    if (typeof method.startLine === "undefined") {
                        method.startLine = sourceLine;
                    }
                    // method.instructions.push({sourceLine: instrOffset});
                }
            } while (m);
            if (lastIndex !== -1) {
                // Get "interesting" text after the LineNumbers table (header of next method/tail of file)
                // trimRight() because of trailing \r on Windows
                textsBeforeMethod.push(codeAndLineNumberTable.substr(lastIndex).trimRight());
            }

            if (currentSourceLine !== -1) {
                // Assign remaining instructions to the same source line
                while (currentInstr + 1 < method.instructions.length) {
                    currentInstr++;
                    method.instructions[currentInstr].sourceLine = currentSourceLine;
                }
            }
        }
        return {
            // Used for sorting
            firstSourceLine: methods.reduce((p, m) => p === -1 ? m.startLine : Math.min(p, m.startLine), -1),
            methods: methods,
            textsBeforeMethod
        };
    }
}

module.exports = JavaCompiler;
