/****************************************************************************
**
** Copyright (C) 2015 The Qt Company Ltd.
** 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 BundleTools = require("qbs.BundleTools");
var DarwinTools = require("qbs.DarwinTools");
var File = require("qbs.File");
var FileInfo = require("qbs.FileInfo");
var ModUtils = require("qbs.ModUtils");
var Process = require("qbs.Process");
var PropertyList = require("qbs.PropertyList");

function artifactsFromInputs(inputs) {
    var artifacts = [];
    for (var tag in inputs) {
        artifacts = artifacts.concat(inputs[tag]);
    }
    return artifacts;
}

function tiffutilScalesMap(inputs) {
    return artifactsFromInputs(inputs).map(function (a) {
        var m = a.filePath.match(/^(.+?)(@(\d+)x)?(\..+?)$/);
        var basePath = m[1];
        var scale = m[2] || "";
        var nscale = m[3];
        var extension = m[4];
        if (scale && scale < 1)
            throw new Error("Invalid scale '" + nscale + "' for image '" + a.filePath + "'");
        return {
            basePath: basePath,
            extension: extension,
            scale: scale
        };
    }).reduce(function (previous, current) {
        previous[current["basePath"]] = (previous[current["basePath"]] || []).concat([{
            extension: current["extension"],
            scale: current["scale"]
        }]);
        return previous;
    }, {});
}

function tiffutilOutputFilePath(product, basePath) {
    return FileInfo.joinPaths(product.destinationDirectory,
                              "hidpi-images",
                              FileInfo.relativePath(product.sourceDirectory, basePath) +
                              product.ib.tiffSuffix);
}

function tiffutilArtifacts(product, inputs) {
    var artifacts = [];
    var map = tiffutilScalesMap(inputs);
    for (var key in map) {
        artifacts.push({
            filePath: tiffutilOutputFilePath(product, key),
            fileTags: ["tiff"]
        });
    }
    return artifacts;
}

function prepareTiffutil(project, product, inputs, outputs, input, output) {
    var cmds = [];
    var map = tiffutilScalesMap(inputs);
    for (var key in map) {
        var args = ["-cat" + (product.ib.combineHidpiImages ? "hidpicheck" : "")];
        var count = 0;
        map[key].forEach(function (obj) {
            args.push(key + obj["scale"] + obj["extension"]);
            ++count;
        });
        args.push("-out", tiffutilOutputFilePath(product, key));
        var cmd = new Command(product.ib.tiffutilPath, args);
        cmd.description = "creating " + output.fileName;
        cmd.count = count;
        cmd.outputFilePath = output.filePath;
        cmd.stderrFilterFunction = function (output) {
            return output.replace(count + " images written to " + outputFilePath + ".", "");
        };
        cmds.push(cmd);
    }
    return cmds;
}

function ibtooldArguments(product, inputs, input, outputs, overrideOutput) {
    var i;
    var args = [];
    var allInputs = artifactsFromInputs(inputs);

    var outputFormat = ModUtils.moduleProperty(product, "outputFormat");
    if (outputFormat) {
        if (!["binary1", "xml1", "human-readable-text"].includes(outputFormat))
            throw("Invalid ibtoold output format: " + outputFormat + ". " +
                  "Must be in [binary1, xml1, human-readable-text].");

        args.push("--output-format", outputFormat);
    }

    var debugFlags = ["warnings", "errors", "notices"];
    for (var j in debugFlags) {
        var flag = debugFlags[j];
        if (ModUtils.modulePropertyFromArtifacts(product, allInputs, product.moduleName, flag)) {
            args.push("--" + flag);
        }
    }

    if (inputs.assetcatalog) {
        args.push("--platform", DarwinTools.applePlatformName(
                      product.moduleProperty("qbs", "targetOS"),
                      product.moduleProperty("xcode", "platformType")));

        var appIconName = ModUtils.modulePropertyFromArtifacts(product, inputs.assetcatalog, product.moduleName, "appIconName");
        if (appIconName)
            args.push("--app-icon", appIconName);

        var launchImageName = ModUtils.modulePropertyFromArtifacts(product, inputs.assetcatalog, product.moduleName, "launchImageName");
        if (launchImageName)
            args.push("--launch-image", launchImageName);

        // Undocumented but used by Xcode (only for iOS?), probably runs pngcrush or equivalent
        if (ModUtils.modulePropertyFromArtifacts(product, inputs.assetcatalog, product.moduleName, "compressPngs"))
            args.push("--compress-pngs");
    } else {
        var sysroot = product.moduleProperty("qbs", "sysroot");
        if (sysroot)
            args.push("--sdk", sysroot);

        args.push("--flatten", ModUtils.modulePropertyFromArtifacts(product, allInputs, product.moduleName, "flatten") ? 'YES' : 'NO');

        // --module and --auto-activate-custom-fonts were introduced in Xcode 6.0
        if (ModUtils.moduleProperty(product, "ibtoolVersionMajor") >= 6) {
            var module = ModUtils.moduleProperty(product, "module");
            if (module)
                args.push("--module", module);

            if (ModUtils.modulePropertyFromArtifacts(product, allInputs, product.moduleName, "autoActivateCustomFonts"))
                args.push("--auto-activate-custom-fonts");
        }
    }

    // --minimum-deployment-target was introduced in Xcode 5.0
    var minimumDarwinVersion = product.moduleProperty("cpp", "minimumDarwinVersion");
    if (minimumDarwinVersion && ModUtils.moduleProperty(product, "ibtoolVersionMajor") >= 5)
        args.push("--minimum-deployment-target", minimumDarwinVersion);

    // --target-device and -output-partial-info-plist were introduced in Xcode 6.0 for ibtool
    if (ModUtils.moduleProperty(product, "ibtoolVersionMajor") >= 6 || inputs.assetcatalog) {
        args.push("--output-partial-info-plist", (outputs && outputs.partial_infoplist)
                  ? outputs.partial_infoplist[outputs.partial_infoplist.length - 1].filePath
                  : "/dev/null");

        // For iOS, we'd normally only output the devices specified in TARGETED_DEVICE_FAMILY
        // We can't get this info from Info.plist keys due to dependency order, so use the qbs prop
        var targetDevices = ModUtils.moduleProperty(product, "targetDevices");
        for (i in targetDevices) {
            args.push("--target-device", targetDevices[i]);
        }
    }

    args = args.concat(ModUtils.modulePropertiesFromArtifacts(product, allInputs,
                                                              product.moduleName, "flags"));

    if (overrideOutput) {
        args.push("--compile", overrideOutput);
    } else {
        if (outputs.compiled_assetcatalog)
            args.push("--compile", product.buildDirectory + "/actool.dir");
        else // compiled_ibdoc
            args.push("--compile", product.buildDirectory + "/ibtool.dir/"
                      + ibtoolCompiledDirSuffix(product, input));
    }

    for (i in allInputs)
        args.push(allInputs[i].filePath);

    return args;
}

function ibtoolFileTaggers(fileTags) {
    var ext;
    if (fileTags.includes("nib") && !fileTags.includes("storyboard"))
        ext = "nib";
    if (fileTags.includes("storyboard") && !fileTags.includes("nib"))
        ext = "storyboard";

    if (!ext)
        throw "unknown ibtool input file tags: " + fileTags;

    var t = ["bundle.input", "compiled_ibdoc"];
    return {
        ".nib": t.concat(["compiled_" + ext + (ext !== "nib" ? "_nib" : "")]),
        ".plist": t.concat(["compiled_" + ext + "_infoplist"]),
        ".storyboard": t.concat(["compiled_" + ext])
    };
}

function ibtoolCompiledDirSuffix(product, input) {
    var suffix = input.completeBaseName;
    if (input.fileTags.includes("nib"))
        suffix += ModUtils.moduleProperty(product, "compiledNibSuffix");
    else if (input.fileTags.includes("storyboard"))
        suffix += ModUtils.moduleProperty(product, "compiledStoryboardSuffix");
    return suffix;
}

function ibtoolOutputArtifacts(product, inputs, input) {
    var suffix = ibtoolCompiledDirSuffix(product, input);
    var tracker = new ModUtils.BlackboxOutputArtifactTracker();
    tracker.hostOS = product.moduleProperty("qbs", "hostOS");
    tracker.shellPath = product.moduleProperty("qbs", "shellPath");
    tracker.fileTaggers = ibtoolFileTaggers(input.fileTags);
    tracker.command = ModUtils.moduleProperty(product, "ibtoolPath");
    tracker.commandArgsFunction = function (outputDirectory) {
        // Last --output-format argument overrides any previous ones
        // Append the name of the base output since it can be either a file or a directory
        // in the case of XIB compilations
        return ibtooldArguments(product, inputs, input,
                                undefined, FileInfo.joinPaths(outputDirectory, suffix))
            .concat(["--output-format", "xml1"]);
    };

    var ibtoolBuildDirectory = product.buildDirectory + "/ibtool.dir";
    var main = BundleTools.destinationDirectoryForResource(product, input);

    var artifacts = tracker.artifacts(ibtoolBuildDirectory);

    if (product.moduleProperty("ib", "ibtoolVersionMajor") >= 6) {
        var prefix = input.fileTags.includes("storyboard") ? "SB" : "";
        var path = FileInfo.joinPaths(product.destinationDirectory, input.completeBaseName +
                                      "-" + prefix + "PartialInfo.plist");
        artifacts.push({ filePath: path, fileTags: ["partial_infoplist"] });
    }

    // Let the output artifacts known the "main" output
    // This can be either a file or directory so the artifact might already exist in the output list
    for (var i = 0; i < artifacts.length; ++i) {
        if (artifacts[i].fileTags.includes("compiled_ibdoc"))
            artifacts[i].bundle = {
                _bundleFilePath: artifacts[i].filePath.replace(ibtoolBuildDirectory, main)
            };
    }

    return artifacts;
}

function ibtoolCommands(project, product, inputs, outputs, input, output, explicitlyDependsOn)
{
    var cmd = new Command(ModUtils.moduleProperty(product, "ibtoolPath"),
                          ibtooldArguments(product, inputs, input, outputs));
    cmd.description = "compiling " + input.fileName;

    // Also display the language name of the nib/storyboard being compiled if it has one
    var localizationKey = DarwinTools.localizationKey(input.filePath);
    if (localizationKey)
        cmd.description += ' (' + localizationKey + ')';

    cmd.highlight = 'compiler';

    // May not be strictly needed, but is set by some versions of Xcode
    if (input.fileTags.includes("storyboard"))
        cmd.environment.push("IBSC_MINIMUM_COMPATIBILITY_VERSION=" +
                             (product.moduleProperty("cpp", "minimumDarwinVersion") || ""));

    cmd.stdoutFilterFunction = function(output) {
        return "";
    };

    return cmd;
}

function actoolOutputArtifacts(product, inputs) {
    // actool has no --dry-run option (rdar://21786925),
    // so compile to a fake temporary directory in order to extract the list of output files
    var tracker = new ModUtils.BlackboxOutputArtifactTracker();
    tracker.hostOS = product.moduleProperty("qbs", "hostOS");
    tracker.shellPath = product.moduleProperty("qbs", "shellPath");
    tracker.command = ModUtils.moduleProperty(product, "actoolPath");
    tracker.commandArgsFunction = function (outputDirectory) {
        // Last --output-format argument overrides any previous ones
        return ibtooldArguments(product, inputs, undefined,
                                undefined, outputDirectory).concat(["--output-format", "xml1"]);
    };
    tracker.processStdOutFunction = parseActoolOutput;
    var artifacts = tracker.artifacts(product.buildDirectory + "/actool.dir");

    // Newer versions of actool don't generate *anything* if there's no input;
    // in that case a partial Info.plist would not have been generated either
    if (artifacts && artifacts.length > 0) {
        artifacts.push({
            filePath: FileInfo.joinPaths(product.destinationDirectory,
                                         "assetcatalog_generated_info.plist"),
            fileTags: ["partial_infoplist"]
        });
    }

    for (var i = 0; i < artifacts.length; ++i) {
        if (artifacts[i].fileTags.includes("compiled_assetcatalog")) {
            artifacts[i].bundle = {
                _bundleFilePath: artifacts[i].filePath.replace(
                    product.buildDirectory + "/actool.dir",
                    BundleTools.destinationDirectoryForResource(product, inputs.assetcatalog[0]))
            };
        }
    }

    return artifacts;
}

function parseActoolOutput(output) {
    var propertyList = new PropertyList();
    try {
        propertyList.readFromString(output);

        var plist = propertyList.toObject();
        if (plist)
            plist = plist["com.apple.actool.compilation-results"];
        if (plist) {
            var artifacts = [];
            files = plist["output-files"];
            for (var i in files) {
                if (files[i] === "/dev/null")
                    continue;
                var tags = files[i].endsWith(".plist")
                        ? ["partial_infoplist"]
                        : ["bundle.input", "compiled_assetcatalog"];
                artifacts.push({
                    // Even though we pass in a canonical base dir, the paths in the XML File
                    // are non-canonical. See QBS-1417.
                    filePath: FileInfo.canonicalPath(files[i]),
                    fileTags: tags
                });
            }

            return artifacts;
        }
    } finally {
        propertyList.clear();
    }
}

function ibtoolVersion(ibtool) {
    var process;
    var version;
    try {
        process = new Process();
        if (process.exec(ibtool, ["--version", "--output-format", "xml1"], true) !== 0)
            console.error(process.readStdErr());

        var propertyList = new PropertyList();
        try {
            propertyList.readFromString(process.readStdOut());

            var plist = propertyList.toObject();
            if (plist)
                plist = plist["com.apple.ibtool.version"];
            if (plist)
                version = plist["short-bundle-version"];
        } finally {
            propertyList.clear();
        }
    } finally {
        process.close();
    }
    return version;
}

function compileAssetCatalogCommands(project, product, inputs, outputs, input, output,
                                     explicitlyDependsOn)
{
    var mkdir = new JavaScriptCommand();
    mkdir.silent = true;
    mkdir.sourceCode = function () {
        File.makePath(FileInfo.joinPaths(product.buildDirectory, "actool.dir"));
    };

    var cmd = new Command(ModUtils.moduleProperty(product, "actoolPath"),
                          ibtooldArguments(product, inputs, input, outputs));
    cmd.description = inputs["assetcatalog"].map(function (input) {
        return "compiling " + input.fileName;
    }).join('\n');
    cmd.highlight = "compiler";

    cmd.stdoutFilterFunction = function(output) {
        return "";
    };

    return [mkdir, cmd];
}
