/****************************************************************************
**
** Copyright (C) 2019 Denis Shienkov <denis.shienkov@gmail.com>
** Contact: http://www.qt.io/licensing
**
** This file is part of Qbs.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms and
** conditions see http://www.qt.io/terms-conditions. For further information
** use the contact form at http://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 or version 3 as published by the Free
** Software Foundation and appearing in the file LICENSE.LGPLv21 and
** LICENSE.LGPLv3 included in the packaging of this file.  Please review the
** following information to ensure the GNU Lesser General Public License
** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, The Qt Company gives you certain additional
** rights.  These rights are described in The Qt Company LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
****************************************************************************/

var Cpp = require("cpp.js");
var Environment = require("qbs.Environment");
var File = require("qbs.File");
var FileInfo = require("qbs.FileInfo");
var ModUtils = require("qbs.ModUtils");
var PathTools = require("qbs.PathTools");
var Process = require("qbs.Process");
var TemporaryDir = require("qbs.TemporaryDir");
var TextFile = require("qbs.TextFile");
var Utilities = require("qbs.Utilities");
var WindowsUtils = require("qbs.WindowsUtils");

function guessArchitecture(macros)
{
    if (macros["__C51__"])
        return "mcs51";
    else if (macros["__CC_ARM"] === 1)
        return "arm";
}

function guessEndianness(macros)
{
    if (macros["__C51__"]) {
        // The 8051 processors are 8-bit. So, the data with an integer type
        // represented by more than one byte is stored as big endian in the
        // Keil toolchain. See for more info:
        // * http://www.keil.com/support/man/docs/c51/c51_ap_2bytescalar.htm
        // * http://www.keil.com/support/man/docs/c51/c51_ap_4bytescalar.htm
        return "big";
    } else if (macros["__ARMCC_VERSION"]) {
        return macros["__BIG_ENDIAN"] ? "big" : "little";
    }
}

function guessVersion(macros)
{
    if (macros["__C51__"]) {
        var version = macros["__C51__"];
        return { major: parseInt(version / 100),
            minor: parseInt(version % 100),
            patch: 0,
            found: true }
    } else if (macros["__CC_ARM"]) {
        var version = macros["__ARMCC_VERSION"];
        return { major: parseInt(version / 1000000),
            minor: parseInt(version / 10000) % 100,
            patch: parseInt(version) % 10000,
            found: true }
    }
}

// Note: The KEIL 8051 compiler does not support the predefined
// macros dumping. So, we do it with following trick where we try
// to compile a temporary file and to parse the console output.
function dumpC51CompilerMacros(compilerFilePath, tag) {
    // C51 compiler support only C language.
    if (tag === "cpp")
        return {};

    function createDumpMacrosFile() {
        var td = new TemporaryDir();
        var fn = FileInfo.fromNativeSeparators(td.path() + "/dump-macros.c");
        var tf = new TextFile(fn, TextFile.WriteOnly);
        tf.writeLine("#define VALUE_TO_STRING(x) #x");
        tf.writeLine("#define VALUE(x) VALUE_TO_STRING(x)");
        tf.writeLine("#define VAR_NAME_VALUE(var) \"\"\"|\"#var\"|\"VALUE(var)");
        tf.writeLine("#ifdef __C51__");
        tf.writeLine("#pragma message(VAR_NAME_VALUE(__C51__))");
        tf.writeLine("#endif");
        tf.close();
        return fn;
    }

    var fn = createDumpMacrosFile();
    var p = new Process();
    p.exec(compilerFilePath, [ fn ], false);
    var map = {};
    p.readStdOut().trim().split(/\r?\n/g).map(function(line) {
        var parts = line.split("\"|\"", 3);
        map[parts[1]] = parts[2];
    });
    return map;
}

function dumpArmCompilerMacros(compilerFilePath, tag, nullDevice) {
    var args = [ "-E", "--list-macros", nullDevice ];
    if (tag === "cpp")
        args.push("--cpp");

    var p = new Process();
    p.exec(compilerFilePath, args, false);
    var map = {};
    p.readStdOut().trim().split(/\r?\n/g).map(function (line) {
        if (!line.startsWith("#define"))
            return;
        var parts = line.split(" ", 3);
        map[parts[1]] = parts[2];
    });
    return map;
}

function dumpMacros(compilerFilePath, tag, nullDevice) {
    var map1 = dumpC51CompilerMacros(compilerFilePath, tag, nullDevice);
    var map2 = dumpArmCompilerMacros(compilerFilePath, tag, nullDevice);
    var map = {};
    for (var attrname in map1)
        map[attrname] = map1[attrname];
    for (var attrname in map2)
        map[attrname] = map2[attrname];
    return map;
}

function dumpDefaultPaths(compilerFilePath, architecture) {
    var includePaths = [];

    if (architecture === "mcs51") {
        var path = compilerFilePath.replace(/bin[\\\/](.*)$/i, "inc");
        includePaths.push(path);
    } else if (architecture === "arm") {
        var path = compilerFilePath.replace(/bin[\\\/](.*)$/i, "include");
        includePaths.push(path);
    }

    return {
        "includePaths": includePaths
    };
}

function adjustPathsToWindowsSeparators(sourcePaths) {
    var resulingPaths = [];
    sourcePaths.forEach(function(path) {
        resulingPaths.push(FileInfo.toWindowsSeparators(path));
    });
    return resulingPaths;
}

function getMaxExitCode(architecture) {
    if (architecture === "mcs51")
        return 1;
    else if (architecture === "arm")
        return 0;
}

function collectLibraryDependencies(product) {
    var seen = {};
    var result = [];

    function addFilePath(filePath) {
        result.push({ filePath: filePath });
    }

    function addArtifactFilePaths(dep, artifacts) {
        if (!artifacts)
            return;
        var artifactFilePaths = artifacts.map(function(a) { return a.filePath; });
        artifactFilePaths.forEach(addFilePath);
    }

    function addExternalStaticLibs(obj) {
        if (!obj.cpp)
            return;
        function ensureArray(a) {
            return Array.isArray(a) ? a : [];
        }
        function sanitizedModuleListProperty(obj, moduleName, propertyName) {
            return ensureArray(ModUtils.sanitizedModuleProperty(obj, moduleName, propertyName));
        }
        var externalLibs = [].concat(
                    sanitizedModuleListProperty(obj, "cpp", "staticLibraries"));
        var staticLibrarySuffix = obj.moduleProperty("cpp", "staticLibrarySuffix");
        externalLibs.forEach(function(staticLibraryName) {
            if (!staticLibraryName.endsWith(staticLibrarySuffix))
                staticLibraryName += staticLibrarySuffix;
            addFilePath(staticLibraryName);
        });
    }

    function traverse(dep) {
        if (seen.hasOwnProperty(dep.name))
            return;
        seen[dep.name] = true;

        if (dep.parameters.cpp && dep.parameters.cpp.link === false)
            return;

        var staticLibraryArtifacts = dep.artifacts["staticlibrary"];
        if (staticLibraryArtifacts) {
            dep.dependencies.forEach(traverse);
            addArtifactFilePaths(dep, staticLibraryArtifacts);
            addExternalStaticLibs(dep);
        }
    }

    product.dependencies.forEach(traverse);
    addExternalStaticLibs(product);
    return result;
}

function filterStdOutput(cmd) {
    cmd.stdoutFilterFunction = function(output) {
        var sourceLines = output.split("\n");
        var filteredLines = [];
        for (var i in sourceLines) {
            if (sourceLines[i].startsWith("***")
                || sourceLines[i].startsWith(">>")
                || sourceLines[i].startsWith("    ")
                || sourceLines[i].startsWith("Program Size:")
                || sourceLines[i].startsWith("A51 FATAL")
                || sourceLines[i].startsWith("C51 FATAL")
                || sourceLines[i].startsWith("ASSEMBLER INVOKED BY")
                || sourceLines[i].startsWith("LOC  OBJ            LINE     SOURCE")
                ) {
                    filteredLines.push(sourceLines[i]);
            }
        }
        return filteredLines.join("\n");
    };
}

function compilerOutputArtifacts(input) {
    var obj = {
        fileTags: ["obj"],
        filePath: Utilities.getHash(input.baseDir) + "/"
              + input.fileName + input.cpp.objectSuffix
    };
    return [obj];
}

function applicationLinkerOutputArtifacts(product) {
    var app = {
        fileTags: ["application"],
        filePath: FileInfo.joinPaths(
                      product.destinationDirectory,
                      PathTools.applicationFilePath(product))
    };
    var mem_map = {
        fileTags: ["mem_map"],
        filePath: FileInfo.joinPaths(
                      product.destinationDirectory,
                      product.targetName
                      + (product.cpp.architecture === "mcs51" ? ".m51" : ".map"))
    };
    return [app, mem_map]
}

function staticLibraryLinkerOutputArtifacts(product) {
    var staticLib = {
        fileTags: ["staticlibrary"],
        filePath: FileInfo.joinPaths(
                      product.destinationDirectory,
                      PathTools.staticLibraryFilePath(product))
    };
    return [staticLib]
}

function compilerFlags(project, product, input, output, explicitlyDependsOn) {
    // Determine which C-language we"re compiling.
    var tag = ModUtils.fileTagForTargetLanguage(input.fileTags.concat(output.fileTags));
    var architecture = input.qbs.architecture;
    var args = [];

    var allDefines = [];
    var platformDefines = input.cpp.platformDefines;
    if (platformDefines)
        allDefines = allDefines.uniqueConcat(platformDefines);
    var defines = input.cpp.defines;
    if (defines)
        allDefines = allDefines.uniqueConcat(defines);

    var allIncludePaths = [];
    var includePaths = input.cpp.includePaths;
    if (includePaths)
        allIncludePaths = allIncludePaths.uniqueConcat(includePaths);
    var systemIncludePaths = input.cpp.systemIncludePaths;
    if (systemIncludePaths)
        allIncludePaths = allIncludePaths.uniqueConcat(systemIncludePaths);
    var compilerIncludePaths = input.cpp.compilerIncludePaths;
    if (compilerIncludePaths)
        allIncludePaths = allIncludePaths.uniqueConcat(compilerIncludePaths);

    if (architecture === "mcs51") {
        args.push(FileInfo.toWindowsSeparators(input.filePath));
        args.push("OBJECT (" + FileInfo.toWindowsSeparators(output.filePath) + ")");

        switch (input.cpp.optimization) {
        case "small":
            args.push("OPTIMIZE (SIZE)");
            break;
        case "fast":
            args.push("OPTIMIZE (SPEED)");
            break;
        case "none":
            args.push("OPTIMIZE (0)");
            break;
        }

        if (input.cpp.debugInformation)
            args.push("DEBUG");

        var warnings = input.cpp.warningLevel;
        if (warnings === "none") {
            args.push("WARNINGLEVEL (0)");
        } else if (warnings === "all") {
            args.push("WARNINGLEVEL (2)");
            args.push("FARWARNING");
        }

        if (allDefines.length > 0)
            args = args.concat("DEFINE (" + allDefines.join(",") + ")");

        if (allIncludePaths.length > 0) {
            var adjusted = adjustPathsToWindowsSeparators(allIncludePaths);
            args = args.concat("INCDIR (" + adjusted.join(";") + ")");
        }
    } else if (architecture === "arm") {
        args.push("-c", input.filePath);
        args.push("-o", output.filePath);

        switch (input.cpp.optimization) {
        case "small":
            args.push("-Ospace")
            break;
        case "fast":
            args.push("-Otime")
            break;
        case "none":
            args.push("-O0")
            break;
        }

        if (input.cpp.debugInformation) {
            args.push("--debug");
            args.push("-g");
        }

        var warnings = input.cpp.warningLevel;
        if (warnings === "none") {
            args.push("-W");
        } else if (warnings === "all") {
            // By default all warnings are enabled.
        }

        if (tag === "c") {
            // Note: Here we use the '==' operator because the '==='
            // operator does not work!
            if (input.cpp.cLanguageVersion == "c99")
                args.push("--c99");
        } else if (tag === "cpp") {
            args.push("--cpp");
            // Note: Here we use the '==' operator because the '==='
            // operator does not work!
            if (input.cpp.cxxLanguageVersion == "c++11")
                args.push("--cpp11");

            var enableExceptions = input.cpp.enableExceptions;
            if (enableExceptions !== undefined)
                args.push(enableExceptions ? "--exceptions" : "--no_exceptions");

            var enableRtti = input.cpp.enableRtti;
            if (enableRtti !== undefined)
                args.push(enableRtti ? "--rtti" : "--no_rtti");
        }

        args = args.concat(allDefines.map(function(define) { return '-D' + define }));
        args = args.concat(allIncludePaths.map(function(include) { return '-I' + include }));
    }

    args = args.concat(ModUtils.moduleProperty(input, "platformFlags"),
                       ModUtils.moduleProperty(input, "flags"),
                       ModUtils.moduleProperty(input, "platformFlags", tag),
                       ModUtils.moduleProperty(input, "flags", tag),
                       ModUtils.moduleProperty(input, "driverFlags", tag));
    return args;
}

function assemblerFlags(project, product, input, output, explicitlyDependsOn) {
    // Determine which C-language we"re compiling
    var tag = ModUtils.fileTagForTargetLanguage(input.fileTags.concat(output.fileTags));
    var architecture = input.qbs.architecture;
    var args = [];

    var allDefines = [];
    var platformDefines = input.cpp.platformDefines;
    if (platformDefines)
        allDefines = allDefines.uniqueConcat(platformDefines);
    var defines = input.cpp.defines;
    if (defines)
        allDefines = allDefines.uniqueConcat(defines);

    var allIncludePaths = [];
    var includePaths = input.cpp.includePaths;
    if (includePaths)
        allIncludePaths = allIncludePaths.uniqueConcat(includePaths);
    var systemIncludePaths = input.cpp.systemIncludePaths;
    if (systemIncludePaths)
        allIncludePaths = allIncludePaths.uniqueConcat(systemIncludePaths);
    var compilerIncludePaths = input.cpp.compilerIncludePaths;
    if (compilerIncludePaths)
        allIncludePaths = allIncludePaths.uniqueConcat(compilerIncludePaths);

    if (architecture === "mcs51") {
        args.push(FileInfo.toWindowsSeparators(input.filePath));
        args.push("OBJECT (" + FileInfo.toWindowsSeparators(output.filePath) + ")");

        if (input.cpp.debugInformation)
            args.push("DEBUG");

        if (allDefines.length > 0)
            args = args.concat("DEFINE (" + allDefines.join(",") + ")");

        if (allIncludePaths.length > 0) {
            var adjusted = adjustPathsToWindowsSeparators(allIncludePaths);
            args = args.concat("INCDIR (" + adjusted.join(";") + ")");
        }
    } else if (architecture === "arm") {
        args.push(input.filePath);
        args.push("-o", output.filePath);

        if (input.cpp.debugInformation) {
            args.push("--debug");
            args.push("-g");
        }

        var warnings = input.cpp.warningLevel;
        if (warnings === "none")
            args.push("--no_warn");

        var endianness = input.cpp.endianness;
        if (endianness)
            args.push((endianness === "little") ? "--littleend" : "--bigend");

        allDefines.forEach(function(define) {
            var parts = define.split("=");
            args.push("--pd");
            if (parts[1] === undefined)
                args.push(parts[0] + " SETA " + 1);
            else if (parts[1].contains("\""))
                args.push(parts[0] + " SETS " + parts[1]);
            else
                args.push(parts[0] + " SETA " + parts[1]);
        });

        args = args.concat(allIncludePaths.map(function(include) { return '-I' + include }));
    }

    args = args.concat(ModUtils.moduleProperty(input, "platformFlags", tag),
                       ModUtils.moduleProperty(input, "flags", tag),
                       ModUtils.moduleProperty(input, "driverFlags", tag));
    return args;
}

function linkerFlags(project, product, input, outputs) {
    var args = [];
    var architecture = product.qbs.architecture;

    if (architecture === "mcs51") {
        var allObjectPaths = [];
        function addObjectPath(obj) {
            allObjectPaths.push(obj.filePath);
        }

        if (inputs.obj)
            inputs.obj.map(function(obj) { addObjectPath(obj) });

        var libraryDependencies = collectLibraryDependencies(product);
        libraryDependencies.forEach(function(dep) { addObjectPath(dep); })

        var adjusted = adjustPathsToWindowsSeparators(allObjectPaths);
        args = args.concat(adjusted.join(","));

        // We need to wrap an output file name with quotes. Otherwise
        // the linker will ignore a specified file name.
        args.push("TO", '"' + FileInfo.toWindowsSeparators(outputs.application[0].filePath) + '"');

        if (!product.cpp.generateMapFile)
            args.push("NOMAP");
    } else if (architecture === "arm") {
        if (inputs.obj)
            args = args.concat(inputs.obj.map(function(obj) { return obj.filePath }));

        args.push("--output", outputs.application[0].filePath);

        if (product.cpp.generateMapFile)
            args.push("--list", outputs.mem_map[0].filePath);

        var libraryPaths = product.cpp.libraryPaths;
        if (libraryPaths)
            args.push("--userlibpath=" + libraryPaths.join(","));

        var libraryDependencies = collectLibraryDependencies(product);
        args = args.concat(libraryDependencies.map(function(dep) { return dep.filePath; }));

        var linkerScripts = inputs.linkerscript
                ? inputs.linkerscript.map(function(a) { return a.filePath; }) : [];
        for (i in linkerScripts)
            args.push("--scatter", linkerScripts[i]);

        if (product.cpp.entryPoint)
            args.push("--entry", product.cpp.entryPoint);

        var debugInformation = product.cpp.debugInformation;
        if (debugInformation !== undefined)
            args.push(debugInformation ? "--debug" : "--no_debug");
    }

    args = args.concat(ModUtils.moduleProperty(product, "driverLinkerFlags"));
    return args;
}

function archiverFlags(project, product, input, outputs) {
    var args = [];
    var architecture = product.qbs.architecture;

    if (architecture === "mcs51") {
        args.push("TRANSFER");
        var allObjectPaths = [];

        function addObjectPath(obj) {
            allObjectPaths.push(obj.filePath);
        }

        if (inputs.obj)
            inputs.obj.map(function(obj) { addObjectPath(obj) });

        var adjusted = adjustPathsToWindowsSeparators(allObjectPaths);
        args = args.concat(adjusted.join(","));

        // We need to wrap a output file name with quotes. Otherwise
        // the linker will ignore a specified file name.
        args.push("TO", '"' + FileInfo.toWindowsSeparators(outputs.staticlibrary[0].filePath) + '"');
    } else if (architecture === "arm") {
        args.push("--create", outputs.staticlibrary[0].filePath);

        if (inputs.obj)
            args = args.concat(inputs.obj.map(function(obj) { return obj.filePath }));

        if (product.cpp.debugInformation)
            args.push("--debug_symbols");
    }

    return args;
}

function prepareCompiler(project, product, inputs, outputs, input, output, explicitlyDependsOn) {
    var args = compilerFlags(project, product, input, output, explicitlyDependsOn);
    var compilerPath = input.cpp.compilerPath;
    var architecture = input.cpp.architecture;
    var cmd = new Command(compilerPath, args)
    cmd.description = "compiling " + input.fileName;
    cmd.highlight = "compiler";
    cmd.maxExitCode = getMaxExitCode(architecture);
    filterStdOutput(cmd);
    return [cmd];
}


function prepareAssembler(project, product, inputs, outputs, input, output, explicitlyDependsOn) {
    var args = assemblerFlags(project, product, input, output, explicitlyDependsOn);
    var assemblerPath = input.cpp.assemblerPath;
    var cmd = new Command(assemblerPath, args)
    cmd.description = "assembling " + input.fileName;
    cmd.highlight = "compiler";
    filterStdOutput(cmd);
    return [cmd];
}

function prepareLinker(project, product, inputs, outputs, input, output) {
    var primaryOutput = outputs.application[0];
    var args = linkerFlags(project, product, input, outputs);
    var linkerPath = product.cpp.linkerPath;
    var cmd = new Command(linkerPath, args)
    cmd.description = "linking " + primaryOutput.fileName;
    cmd.highlight = "linker";
    filterStdOutput(cmd);
    return [cmd];
}

function prepareArchiver(project, product, inputs, outputs, input, output) {
    var args = archiverFlags(project, product, input, outputs);
    var archiverPath = product.cpp.archiverPath;
    var cmd = new Command(archiverPath, args)
    cmd.description = "linking " + output.fileName;
    cmd.highlight = "linker";
    filterStdOutput(cmd);
    return [cmd];
}
