import qbs.BundleTools
import qbs.Environment
import qbs.File
import qbs.FileInfo
import qbs.DarwinTools
import qbs.ModUtils
import qbs.Probes
import qbs.PropertyList
import qbs.Utilities
import 'xcode.js' as Xcode

Module {
    id: xcodeModule

    Probes.XcodeProbe {
        id: xcodeProbe
        developerPath: xcodeModule.developerPath
        platformType: xcodeModule.platformType
        platformPath: xcodeModule.platformPath
        devicePlatformPath: xcodeModule.devicePlatformPath
        xcodebuildPath: xcodeModule.xcodebuildPath
        sdksPath: xcodeModule.sdksPath
    }

    condition: qbs.targetOS.contains("darwin") &&
               qbs.toolchain && qbs.toolchain.contains("xcode")

    version: xcodeProbe.xcodeVersion

    property path developerPath: "/Applications/Xcode.app/Contents/Developer"
    property string sdk: DarwinTools.applePlatformName(qbs.targetOS, platformType)
    property stringList targetDevices: DarwinTools.targetDevices(qbs.targetOS)

    property string platformType: {
        if (qbs.targetOS.containsAny(["ios-simulator", "tvos-simulator", "watchos-simulator"]))
            return "simulator";
        if (qbs.targetOS.containsAny(["ios", "tvos", "watchos"]))
            return "device";
    }

    readonly property string sdkName: {
        if (_sdkSettings) {
            return _sdkSettings["CanonicalName"];
        }
    }

    readonly property string sdkVersion: {
        if (_sdkSettings) {
            return _sdkSettings["Version"];
        }
    }
    readonly property string shortSdkVersion: {
        var v = sdkVersion;
        if (v && v.split('.').length > 2)
            v = v.slice(0, v.lastIndexOf('.'));
        return v;
    }

    readonly property string latestSdkName: {
        if (_latestSdk) {
            return _latestSdk["CanonicalName"];
        }
    }

    readonly property string latestSdkVersion: {
        if (_latestSdk) {
            return _latestSdk["Version"];
        }
    }

    readonly property stringList availableSdkNames: {
        if (_availableSdks) {
            return _availableSdks.map(function (obj) { return obj["CanonicalName"]; });
        }
    }

    readonly property stringList availableSdkVersions: {
        if (_availableSdks) {
            return _availableSdks.map(function (obj) { return obj["Version"]; });
        }
    }

    property string signingIdentity
    readonly property string actualSigningIdentity: {
        if (_actualSigningIdentity && _actualSigningIdentity.length === 2)
            return _actualSigningIdentity[0];
    }

    readonly property string actualSigningIdentityDisplayName: {
        if (_actualSigningIdentity && _actualSigningIdentity.length === 2)
            return _actualSigningIdentity[1];
    }

    property string signingTimestamp: "none"

    property string provisioningProfile

    property string xcodebuildName: "xcodebuild"
    property string xcodebuildPath: FileInfo.joinPaths(developerPath, "usr", "bin", xcodebuildName)

    property string securityName: "security"
    property string securityPath: securityName

    property string codesignName: "codesign"
    property string codesignPath: codesignName
    property stringList codesignFlags

    readonly property path toolchainPath: FileInfo.joinPaths(toolchainsPath,
                                                             "XcodeDefault" + ".xctoolchain")
    readonly property path platformPath: FileInfo.joinPaths(platformsPath,
                                                            DarwinTools.applePlatformDirectoryName(
                                                                qbs.targetOS, platformType)
                                                            + ".platform")
    readonly property path devicePlatformPath: FileInfo.joinPaths(
                                                   platformsPath,
                                                   DarwinTools.applePlatformDirectoryName(
                                                       qbs.targetOS, "device")
                                                   + ".platform")
    readonly property path simulatorPlatformPath: FileInfo.joinPaths(
                                                      platformsPath,
                                                      DarwinTools.applePlatformDirectoryName(
                                                          qbs.targetOS, "simulator")
                                                      + ".platform")
    readonly property path sdkPath: FileInfo.joinPaths(sdksPath,
                                                       DarwinTools.applePlatformDirectoryName(
                                                           qbs.targetOS, platformType,
                                                           shortSdkVersion) + ".sdk")

    // private properties
    readonly property path toolchainsPath: FileInfo.joinPaths(developerPath, "Toolchains")
    readonly property path platformsPath: FileInfo.joinPaths(developerPath, "Platforms")
    readonly property path sdksPath: FileInfo.joinPaths(platformPath, "Developer", "SDKs")

    readonly property path platformInfoPlist: FileInfo.joinPaths(platformPath, "Info.plist")
    readonly property path sdkSettingsPlist: FileInfo.joinPaths(sdkPath, "SDKSettings.plist")
    readonly property path toolchainInfoPlist: FileInfo.joinPaths(toolchainPath,
                                                                  "ToolchainInfo.plist")

    readonly property stringList _actualSigningIdentity: {
        if (/^[A-Fa-f0-9]{40}$/.test(signingIdentity)) {
            return [signingIdentity, signingIdentity];
        }

        var result = [];

        if (signingIdentity) {
            var identities = Utilities.signingIdentities();
            for (var key in identities) {
                if (identities[key].subjectInfo.CN === signingIdentity) {
                    result.push([key, signingIdentity]);
                }
            }

            if (result.length == 0) {
                throw "Unable to find signingIdentity '" + signingIdentity + "'";
            }

            if (result.length > 1) {
                throw "Signing identity '" + signingIdentity + "' is ambiguous";
            }
        }

        return result[0];
    }

    property path provisioningProfilesPath: {
        return FileInfo.joinPaths(Environment.getEnv("HOME"), "Library/MobileDevice/Provisioning Profiles");
    }

    readonly property stringList standardArchitectures: _architectureSettings["ARCHS_STANDARD"]

    readonly property var _architectureSettings: xcodeProbe.architectureSettings

    readonly property var _availableSdks: xcodeProbe.availableSdks

    readonly property var _latestSdk: _availableSdks[_availableSdks.length - 1]

    readonly property var _sdkSettings: {
        if (_availableSdks) {
            for (var i in _availableSdks) {
                if (_availableSdks[i]["Version"] === sdk)
                    return _availableSdks[i];
                if (_availableSdks[i]["CanonicalName"] === sdk)
                    return _availableSdks[i];
            }

            // Latest SDK available for the platform
            if (DarwinTools.applePlatformName(qbs.targetOS, platformType) === sdk)
                return _latestSdk;
        }
    }

    qbs.sysroot: sdkPath

    validate: {
        if (!_availableSdks) {
            throw "There are no SDKs available for this platform in the Xcode installation.";
        }

        if (!_sdkSettings) {
            throw "There is no matching SDK available for " + sdk + ".";
        }

        var validator = new ModUtils.PropertyValidator("xcode");
        validator.setRequiredProperty("developerPath", developerPath);
        validator.setRequiredProperty("sdk", sdk);
        validator.setRequiredProperty("sdkName", sdkName);
        validator.setRequiredProperty("sdkVersion", sdkVersion);
        validator.setRequiredProperty("toolchainsPath", toolchainsPath);
        validator.setRequiredProperty("toolchainPath", toolchainPath);
        validator.setRequiredProperty("platformsPath", platformsPath);
        validator.setRequiredProperty("platformPath", platformPath);
        validator.setRequiredProperty("sdksPath", sdkPath);
        validator.setRequiredProperty("sdkPath", sdkPath);
        validator.addVersionValidator("sdkVersion", sdkVersion, 2, 3);
        validator.addCustomValidator("sdkName", sdkName, function (value) {
            return value === DarwinTools.applePlatformDirectoryName(
                        qbs.targetOS, platformType, shortSdkVersion, false).toLowerCase();
        }, "is '" + sdkName + "', but target OS is [" + qbs.targetOS.join(",")
        + "] and Xcode SDK version is '" + sdkVersion + "'");
        validator.addCustomValidator("sdk", sdk, function (value) {
            return value === sdkName || (value + shortSdkVersion) === sdkName;
        }, "is '" + sdk + "', but canonical SDK name is '" + sdkName + "'");
        validator.validate();
    }

    property var buildEnv: {
        var env = {
            "DEVELOPER_DIR": developerPath,
            "SDKROOT": sdkPath
        };

        var prefixes = [platformPath + "/Developer", toolchainPath, developerPath];
        for (var i = 0; i < prefixes.length; ++i) {
            var codesign_allocate = prefixes[i] + "/usr/bin/codesign_allocate";
            if (File.exists(codesign_allocate)) {
                env["CODESIGN_ALLOCATE"] = codesign_allocate;
                break;
            }
        }

        return env;
    }

    setupBuildEnvironment: {
        var v = new ModUtils.EnvironmentVariable("PATH", product.qbs.pathListSeparator, false);
        v.prepend(product.xcode.platformPath + "/Developer/usr/bin");
        v.prepend(product.xcode.developerPath + "/usr/bin");
        v.set();

        for (var key in product.xcode.buildEnv) {
            v = new ModUtils.EnvironmentVariable(key);
            v.value = product.xcode.buildEnv[key];
            v.set();
        }
    }

    Group {
        name: "Provisioning Profiles"
        prefix: xcode.provisioningProfilesPath + "/"
        files: ["*.mobileprovision", "*.provisionprofile"]
        fileTags: [] // HACK: provisioning profile handling is not yet ready and can break autotests
    }

    FileTagger {
        fileTags: ["xcode.entitlements"]
        patterns: ["*.entitlements"]
    }

    FileTagger {
        fileTags: ["xcode.provisioningprofile"]
        patterns: ["*.mobileprovision", "*.provisionprofile"]
    }

    Rule {
        inputs: ["xcode.provisioningprofile"]

        Artifact {
            filePath: FileInfo.joinPaths(product.destinationDirectory,
                                         "provisioning-profiles",
                                         input.fileName + ".xml")
            fileTags: ["xcode.provisioningprofile.data"]
        }

        prepare: {
            var cmds = [];

            var cmd = new Command("openssl", ["smime", "-verify", "-noverify", "-inform", "DER",
                                  "-in", input.filePath, "-out", output.filePath]);
            cmd.silent = true;
            cmd.stderrFilterFunction = function (output) {
                return output.replace("Verification successful\n", "");
            };
            cmds.push(cmd);

            cmd = new JavaScriptCommand();
            cmd.silent = true;
            cmd.inputFilePath = input.filePath;
            cmd.outputFilePath = output.filePath;
            cmd.sourceCode = function() {
                var propertyList = new PropertyList();
                try {
                    propertyList.readFromFile(outputFilePath);
                    propertyList.readFromObject({
                        data: propertyList.toObject(),
                        fileName: FileInfo.fileName(inputFilePath),
                        filePath: inputFilePath
                    });
                    propertyList.writeToFile(outputFilePath, "xml1");
                } finally {
                    propertyList.clear();
                }
            };
            cmds.push(cmd);

            return cmds;
        }
    }

    Rule {
        multiplex: true
        inputs: ["xcode.provisioningprofile.data"]
        outputFileTags: ["xcode.provisioningprofile.main", "xcode.provisioningprofile.data.main"]

        outputArtifacts: {
            var artifacts = [];
            for (var i = 0; i < inputs["xcode.provisioningprofile.data"].length; ++i) {
                var dataFile = inputs["xcode.provisioningprofile.data"][i].filePath;
                var query = product.moduleProperty("xcode", "provisioningProfile");
                var obj = Xcode.provisioningProfilePlistContents(dataFile);
                if (obj.data && (obj.data.UUID === query || obj.data.Name === query)) {
                    console.log("Using provisioning profile: " + obj.filePath);

                    artifacts.push({
                        filePath: FileInfo.joinPaths(product.destinationDirectory, obj.fileName),
                        fileTags: ["xcode.provisioningprofile.main", obj.filePath]
                    });

                    artifacts.push({
                        filePath: FileInfo.joinPaths(product.destinationDirectory, obj.fileName + ".xml"),
                        fileTags: ["xcode.provisioningprofile.data.main", dataFile]
                    });
                }
            }
            return artifacts;
        }

        prepare: {
            var cmds = [];
            for (var tag in outputs) {
                for (var i = 0; i < outputs[tag].length; ++i) {
                    var output = outputs[tag][i];
                    var cmd = new JavaScriptCommand();
                    cmd.silent = true;
                    cmd.inputFilePath = output.fileTags.filter(function(f) { return f.startsWith('/'); })[0] // QBS-754
                    cmd.outputFilePath = output.filePath;
                    cmd.sourceCode = function() {
                        File.copy(inputFilePath, outputFilePath);
                    };
                    cmds.push(cmd);
                }
            }
            return cmds;
        }
    }

    Rule {
        inputs: ["xcode.entitlements", "xcode.provisioningprofile.data.main"]

        Artifact {
            filePath: FileInfo.joinPaths(product.destinationDirectory,
                                         product.targetName + ".xcent")
            fileTags: ["xcent", "bundle.input"]
        }

        prepare: {
            var cmd = new JavaScriptCommand();
            cmd.description = "generating entitlements";
            cmd.highlight = "codegen";
            cmd.bundleIdentifier = product.moduleProperty("bundle", "identifier");
            cmd.signingEntitlements = inputs["xcode.entitlements"].map(function (a) { return a.filePath; });
            cmd.platformPath = ModUtils.moduleProperty(product, "platformPath");
            cmd.sdkPath = ModUtils.moduleProperty(product, "sdkPath");
            cmd.sourceCode = function() {
                var i;
                var provData = Xcode.provisioningProfilePlistContents(input.filePath);
                if (provData)
                    provData = provData.data;

                var aggregateEntitlements = {};

                // Start building up an aggregate entitlements plist from the files in the SDKs,
                // which contain placeholders in the same manner as Info.plist
                function entitlementsFileContents(path) {
                    return File.exists(path) ? BundleTools.infoPlistContents(path) : undefined;
                }
                var entitlementsSources = [
                    entitlementsFileContents(FileInfo.joinPaths(platformPath, "Entitlements.plist")),
                    entitlementsFileContents(FileInfo.joinPaths(sdkPath, "Entitlements.plist"))
                ];

                for (i = 0; i < signingEntitlements.length; ++i) {
                    entitlementsSources.push(entitlementsFileContents(signingEntitlements[i]));
                }

                for (i = 0; i < entitlementsSources.length; ++i) {
                    var contents = entitlementsSources[i];
                    for (var key in contents) {
                        if (contents.hasOwnProperty(key))
                            aggregateEntitlements[key] = contents[key];
                    }
                }

                contents = provData["Entitlements"];
                for (key in contents) {
                    if (contents.hasOwnProperty(key) && !aggregateEntitlements.hasOwnProperty(key))
                        aggregateEntitlements[key] = contents[key];
                }

                // Expand entitlements variables with data from the provisioning profile
                var env = {
                    "AppIdentifierPrefix": provData["ApplicationIdentifierPrefix"] + ".",
                    "CFBundleIdentifier": bundleIdentifier
                };
                DarwinTools.expandPlistEnvironmentVariables(aggregateEntitlements, env, true);

                // Anything with an undefined or otherwise empty value should be removed
                // Only JSON-formatted plists can have null values, other formats error out
                // This also follows Xcode behavior
                DarwinTools.cleanPropertyList(aggregateEntitlements);

                var plist = new PropertyList();
                try {
                    plist.readFromObject(aggregateEntitlements);
                    plist.writeToFile(outputs.xcent[0].filePath, "xml1");
                } finally {
                    plist.clear();
                }
            };
            return [cmd];
        }
    }
}
