/*
       Licensed to the Apache Software Foundation (ASF) under one
       or more contributor license agreements.  See the NOTICE file
       distributed with this work for additional information
       regarding copyright ownership.  The ASF licenses this file
       to you under the Apache License, Version 2.0 (the
       "License"); you may not use this file except in compliance
       with the License.  You may obtain a copy of the License at

         http://www.apache.org/licenses/LICENSE-2.0

       Unless required by applicable law or agreed to in writing,
       software distributed under the License is distributed on an
       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
       KIND, either express or implied.  See the License for the
       specific language governing permissions and limitations
       under the License.
*/

var Q = require('q');
var os = require('os');
var path = require('path');
var shell = require('shelljs');
var spawn = require('cordova-common').superspawn.spawn;
var CordovaError = require('cordova-common').CordovaError;

var ConfigParser, MSBuildTools, Version;
try {
    ConfigParser = require('../../template/cordova/lib/ConfigParser');
    MSBuildTools = require('../../template/cordova/lib/MSBuildTools');
    Version = require('../../template/cordova/lib/Version');
} catch (ex) {
    // If previous import fails, we're probably running this script
    // from installed platform and the module location is different.
    ConfigParser = require('./ConfigParser');
    MSBuildTools = require('./MSBuildTools');
    Version = require('./Version');
}

// The constant for VS2013 Upd2 PackageVersion. See MSDN for
// reference: https://msdn.microsoft.com/en-us/library/bb164659(v=vs.120).aspx
var VS2013_UPDATE2_RC = new Version(12, 0, 30324);
var REQUIRED_VERSIONS = {
    '8.1': {
        os: '6.3',
        msbuild: '12.0',
        visualstudio: '12.0',
        windowssdk: '8.1',
        phonesdk: '8.1'
    },
    '10.0': {
        // Note that Windows 10 target is also supported on Windows 7, so this should look
        // like '6.1 || >=6.3', but due to Version module restricted functionality we handle
        // this case separately in checkOS function below.
        os: '6.3',
        msbuild: '14.0',
        visualstudio: '14.0',
        windowssdk: '10.0',
        phonesdk: '10.0'
    }
};

function getMinimalRequiredVersionFor (requirement, windowsTargetVersion, windowsPhoneTargetVersion) {
    if (windowsTargetVersion === '8' || windowsTargetVersion === '8.0') {
        throw new CordovaError('windows8 platform is deprecated. To use windows-target-version=8.0 you may downgrade to cordova-windows@4.');
    }

    if (windowsPhoneTargetVersion === '8' || windowsPhoneTargetVersion === '8.0') {
        throw new CordovaError('8.0 is not a valid version for windows-phone-target-version (use the wp8 Cordova platform instead)');
    }
    var windowsReqVersion = Version.tryParse(REQUIRED_VERSIONS[windowsTargetVersion][requirement]);
    var phoneReqVersion = Version.tryParse(REQUIRED_VERSIONS[windowsPhoneTargetVersion][requirement]);

    // If we're searching for Windows SDK, we're not
    // interested in Phone's version and and vice versa.
    if (requirement === 'windowssdk') return windowsReqVersion;
    if (requirement === 'phonesdk') return phoneReqVersion;

    // If both windowsReqVersion and phoneReqVersion is valid Versions, choose the max one
    if (windowsReqVersion && phoneReqVersion) {
        return windowsReqVersion.gt(phoneReqVersion)
            ? windowsReqVersion
            : phoneReqVersion;
    }

    // Otherwise return that one which is defined and valid
    return windowsReqVersion || phoneReqVersion;
}

function getHighestAppropriateVersion (versions, requiredVersion) {
    return versions.map(function (version) {
        return Version.tryParse(version);
    })
        .sort(Version.comparer)
        .filter(function (toolVersion) {
            return toolVersion.gte(requiredVersion);
        }).reverse()[0];
}

/**
 * Return Version object for current Windows version. User 'ver' binary or
 *   os.release() in case of errors.
 *
 * @return  {Version}  Version information for current OS.
 */
function getWindowsVersion () {
    return spawn('ver').then(function (output) {
        var match = /\[Version (.*)\]\s*$/.exec(output);
        return Version.fromString(match[1]);
    }).fail(function () {
        return Version.fromString(os.release());
    });
}

/**
 * Lists all Visual Studio versions insalled. For VS 2013 if it present, also
 *   checks if Update 2 is installed.
 *
 * @return  {String[]}  List of installed Visual Studio versions.
 */
function getInstalledVSVersions () {
    // Query all keys with Install value equal to 1, then filter out
    // those, which are not related to VS itself
    return spawn('reg', ['query', 'HKLM\\SOFTWARE\\Microsoft\\DevDiv\\vs\\Servicing', '/s', '/v', 'Install', '/f', '1', '/d', '/e', '/reg:32'])
        .fail(function () { return ''; })
        .then(function (output) {
            return output.split('\n')
                .reduce(function (installedVersions, line) {
                    var match = /(\d+\.\d+)\\(ultimate|professional|premium|community)/.exec(line);
                    if (match && match[1] && installedVersions.indexOf(match[1]) === -1) { installedVersions.push(match[1]); }
                    return installedVersions;
                }, []);
        }).then(function (installedVersions) {
            // If there is no VS2013 installed, the we have nothing to do
            if (installedVersions.indexOf('12.0') === -1) return installedVersions;

            // special case for VS 2013. We need to check if VS2013 update 2 is installed
            return spawn('reg', ['query', 'HKLM\\SOFTWARE\\Microsoft\\Updates\\Microsoft Visual Studio 2013\\vsupdate_KB2829760', '/v', 'PackageVersion', '/reg:32'])
                .then(function (output) {
                    var updateVer = Version.fromString(/PackageVersion\s+REG_SZ\s+(.*)/i.exec(output)[1]);
                    // if update version is lover than Update2, reject the promise
                    if (VS2013_UPDATE2_RC.gte(updateVer)) return Q.reject();
                    return installedVersions;
                }).fail(function () {
                    // if we got any errors on previous steps, we're assuming that
                    // required VS update is not installed.
                    installedVersions.splice(installedVersions.indexOf('12.0'), 1);
                    return installedVersions;
                });
        })
        .then(function (installedVersions) {
            var willowVersions = MSBuildTools.getWillowInstallations().map(function (installation) {
                return installation.version;
            });
            return installedVersions.concat(willowVersions);
        });
}

/**
 * Gets list of installed Windows SDKs
 *
 * @return  {Version[]}  List of installed SDKs' versions
 */
function getInstalledWindowsSdks () {
    var installedSdks = [];
    return spawn('reg', ['query', 'HKLM\\SOFTWARE\\Microsoft\\Microsoft SDKs\\Windows', '/s', '/v', 'InstallationFolder', '/reg:32'])
        .fail(function () { return ''; })
        .then(function (output) {
            var re = /\\Microsoft SDKs\\Windows\\v(\d+\.\d+)\s*InstallationFolder\s+REG_SZ\s+(.*)/gim;
            var match;
            while ((match = re.exec(output))) {
                var sdkPath = match[2];
                // Verify that SDKs is really installed by checking SDKManifest file at SDK root
                if (shell.test('-e', path.join(sdkPath, 'SDKManifest.xml'))) {
                    installedSdks.push(Version.tryParse(match[1]));
                }
            }
        }).thenResolve(installedSdks);
}

/**
 * Gets list of installed Windows Phone SDKs. Separately searches for 8.1 Phone
 *   SDK and Windows 10 SDK, because the latter is needed for both Windows and
 *   Windows Phone applications.
 *
 * @return  {Version[]}  List of installed Phone SDKs' versions.
 */
function getInstalledPhoneSdks () {
    var installedSdks = [];
    return spawn('reg', ['query', 'HKLM\\SOFTWARE\\Microsoft\\Microsoft SDKs\\Windows Phone\\v8.1', '/v', 'InstallationFolder', '/reg:32'])
        .fail(function () { return ''; })
        .then(function (output) {
            var match = /\\Microsoft SDKs\\Windows Phone\\v(\d+\.\d+)\s*InstallationFolder\s+REG_SZ\s+(.*)/gim.exec(output);
            if (match && shell.test('-e', path.join(match[2], 'SDKManifest.xml'))) {
                installedSdks.push(Version.tryParse(match[1]));
            }
        }).then(function () {
            return spawn('reg', ['query', 'HKLM\\SOFTWARE\\Microsoft\\Microsoft SDKs\\Windows\\v10.0', '/v', 'InstallationFolder', '/reg:32']);
        }).fail(function () {
            return '';
        }).then(function (output) {
            var match = /\\Microsoft SDKs\\Windows\\v(\d+\.\d+)\s*InstallationFolder\s+REG_SZ\s+(.*)/gim.exec(output);
            if (match && shell.test('-e', path.join(match[2], 'SDKManifest.xml'))) {
                installedSdks.push(Version.tryParse(match[1]));
            }
        }).thenResolve(installedSdks);
}

/**
 * Shortens version string or Version object by leaving only first two segments
 *   (major and minor).
 * @param   {String|Version}  version  The version identifier. Either Version
 *   object or string that looks like "12.5.6"
 * @return  {String}          Shortened version, or undefined if provided
 *   parameter is not a valid version
 */
function shortenVersion (version) {
    return /^(\d+(?:\.\d+)?)/.exec(version.toString())[1];
}

function mapWindowsVersionToName (version) {
    var map = {
        '6.2': 'Windows 8',
        '6.3': 'Windows 8.1',
        '10.0': 'Windows 10'
    };
    var majorMinor = shortenVersion(version);
    return map[majorMinor];
}

function mapVSVersionToName (version) {
    var map = {
        '11.0': '2012 Express for Windows',
        '12.0': '2013 Express for Windows Update2',
        '14.0': '2015 Community'
    };
    var majorMinor = shortenVersion(version);
    return map[majorMinor];
}

/**
 * Check if current OS is supports building windows platform
 * @return {Promise} Promise either fullfilled or rejected with error message.
 */
var checkOS = function (windowsTargetVersion, windowsPhoneTargetVersion) {
    if (process.platform !== 'win32') {
        // Build Universal windows apps available for windows platform only, so we reject on others platforms
        return Q.reject('Cordova tooling for Windows requires Windows OS to build project');
    }

    return getWindowsVersion().then(function (actualVersion) {
        var requiredOsVersion = getMinimalRequiredVersionFor('os', windowsTargetVersion, windowsPhoneTargetVersion);
        if ((actualVersion.gte(requiredOsVersion)) ||
            // Special case for Windows 10/Phone 10  targets which can be built on Windows 7 (version 6.1)
            (actualVersion.major === 6 && actualVersion.minor === 1 && getConfig().getWindowsTargetVersion() === '10.0')) {
            return mapWindowsVersionToName(actualVersion);
        }

        return Q.reject('Current Windows version doesn\'t support building this project. ' +
            'Consider upgrading your OS to ' + mapWindowsVersionToName(requiredOsVersion));
    });
};

/**
 * Checks if MSBuild tools is available.
 * @return {Promise} Promise either fullfilled with MSBuild version
 *                           or rejected with error message.
 */
var checkMSBuild = function (windowsTargetVersion, windowsPhoneTargetVersion) {
    return MSBuildTools.findAllAvailableVersions()
        .then(function (msbuildToolsVersions) {
            // console.log('msbuildToolsVersions', msbuildToolsVersions);
            var msbuildRequiredVersion = getMinimalRequiredVersionFor('msbuild', windowsTargetVersion, windowsPhoneTargetVersion);
            // console.log('msbuildRequiredVersion', msbuildRequiredVersion);
            msbuildToolsVersions = msbuildToolsVersions.map(function (msbuildToolsVersion) {
                return msbuildToolsVersion.version;
            });
            // console.log('msbuildToolsVersions', msbuildToolsVersions);

            var appropriateVersion = getHighestAppropriateVersion(msbuildToolsVersions, msbuildRequiredVersion);
            // console.log('appropriateVersion', appropriateVersion);
            return appropriateVersion
                ? shortenVersion(appropriateVersion)
                : Q.reject('MSBuild tools v.' + shortenVersion(msbuildRequiredVersion) + ' not found. ' +
                    'Please install Visual Studio ' + mapVSVersionToName(getMinimalRequiredVersionFor('visualstudio', windowsTargetVersion, windowsPhoneTargetVersion)) +
                    ' or higher from https://www.visualstudio.com/downloads/download-visual-studio-vs');
        });
};

var checkVS = function (windowsTargetVersion, windowsPhoneTargetVersion) {
    var vsRequiredVersion = getMinimalRequiredVersionFor('visualstudio', windowsTargetVersion, windowsPhoneTargetVersion);

    if (process.env.VSINSTALLDIR) {
        return Q('(user-specified via VSINSTALLDIR)');
    }
    return getInstalledVSVersions()
        .then(function (installedVersions) {
            var appropriateVersion = getHighestAppropriateVersion(installedVersions, vsRequiredVersion);
            return appropriateVersion
                ? shortenVersion(appropriateVersion)
                : Q.reject('Required version of Visual Studio not found. Please install Visual Studio ' +
                    mapVSVersionToName(vsRequiredVersion) +
                    ' or higher from https://www.visualstudio.com/downloads/download-visual-studio-vs');
        });
};

var checkWinSdk = function (windowsTargetVersion, windowsPhoneTargetVersion) {
    return getInstalledWindowsSdks()
        .then(function (installedSdks) {
            var requiredVersion = getMinimalRequiredVersionFor('windowssdk', windowsTargetVersion, windowsPhoneTargetVersion);
            var hasSdkInstalled = installedSdks.some(function (installedSdk) {
                return installedSdk.eq(requiredVersion);
            });
            if (!hasSdkInstalled) {
                return Q.reject('Windows SDK not found. Ensure that you have installed ' +
                    'Windows ' + shortenVersion(requiredVersion) + ' SDK along with Visual Studio or install ' +
                    'Windows ' + shortenVersion(requiredVersion) + ' SDK separately from ' +
                    'https://dev.windows.com/en-us/downloads');
            }

            return shortenVersion(requiredVersion);
        });
};

var checkPhoneSdk = function (windowsTargetVersion, windowsPhoneTargetVersion) {
    var requiredVersion = getMinimalRequiredVersionFor('phonesdk', windowsTargetVersion, windowsPhoneTargetVersion);
    return getInstalledPhoneSdks()
        .then(function (installedSdks) {
            var hasSdkInstalled = installedSdks.some(function (installedSdk) {
                return installedSdk.eq(requiredVersion);
            });

            return hasSdkInstalled
                ? shortenVersion(requiredVersion)
                : Q.reject();
        })
        .fail(function () {
            return Q.reject('Windows Phone SDK not found. Ensure that you have installed ' +
                'Windows Phone ' + shortenVersion(requiredVersion) + ' SDK along with Visual Studio or install ' +
                'Windows Phone ' + shortenVersion(requiredVersion) + ' SDK separately from ' +
                'https://dev.windows.com/develop/download-phone-sdk');
        });
};

module.exports.run = function () {
    return checkOS('10.0', '10.0').then(function () {
        return MSBuildTools.findAvailableVersion();
    });
};

/** Checks if Windows SDK required to build the target_platform is present
 * @param {String}  target_platorm        Target platform ('8.1' or '10.0')
 */
module.exports.isWinSDKPresent = function (target_platform) {
    return checkWinSdk(target_platform, '8.1');
};

// Checks if min SDK required to build Windows Phone 8.1 project is present
module.exports.isPhoneSDKPresent = function () {
    return checkPhoneSdk('8.1', '8.1');
};

/**
 * Object that represents one of requirements for current platform.
 * @param {String}  id        The unique identifier for this requirements.
 * @param {String}  name      The name of requirements. Human-readable field.
 * @param {Boolean} isFatal   Marks the requirement as fatal. If such requirement will fail
 *                            next requirements' checks will be skipped.
 */
var Requirement = function (id, name, isFatal) {
    this.id = id;
    this.name = name;
    this.installed = false;
    this.metadata = {};
    this.isFatal = isFatal || false;
};

var requirements = [
    new Requirement('os', 'Windows OS', true),
    new Requirement('msbuild', 'MSBuild Tools'),
    new Requirement('visualstudio', 'Visual Studio'),
    new Requirement('windowssdk', 'Windows SDK'),
    new Requirement('phonesdk', 'Windows Phone SDK')
];

// Define list of checks needs to be performed
var checkFns = [checkOS, checkMSBuild, checkVS, checkWinSdk, checkPhoneSdk];

var config = null;
function getConfig () {
    try {
        config = config || new ConfigParser(path.join(__dirname, '../../config.xml'));
        return Q(config);
    } catch (e) {
        // try again to cover case of being called from command line
        try {
            config = config || new ConfigParser(path.join(__dirname, '../../template/config.xml'));
            return Q(config);
        } catch (e) {
            // yeah, really no config.xml
            return Q.reject(new CordovaError('Can\'t check requirements for Windows platform.' +
                'The config.xml file is either missing or malformed.'));
        }
    }
}

/**
 * Methods that runs all checks one by one and returns a result of checks
 * as an array of Requirement objects. This method intended to be used by cordova-lib check_reqs method.
 * @return Promise<Requirement[]> Array of requirements. Due to implementation, promise is always fulfilled.
 */
module.exports.check_all = function () {
    var result = [];
    var fatalIsHit = false;

    // Then execute requirement checks one-by-one
    return checkFns.reduce(function (promise, checkFn, idx) {
        return promise.then(function () {
            // If fatal requirement is failed,
            // we don't need to check others
            if (fatalIsHit) return Q();
            var requirement = requirements[idx];
            return getConfig()
                .then(function (config) {
                    return checkFn(config.getWindowsTargetVersion(), config.getWindowsPhoneTargetVersion())
                        .then(function (version) {
                            requirement.installed = true;
                            requirement.metadata.version = version;
                            result.push(requirement);
                        }).catch(function (err) {
                            if (requirement.isFatal) fatalIsHit = true;
                            requirement.metadata.reason = err;
                            result.push(requirement);
                        });
                });
        });
    }, Q())
        .then(function () {
            // When chain is completed, return requirements array to upstream API
            return result;
        });
};

module.exports.help = function () {
    console.log('Usage: check_reqs or node check_reqs');
};
