text: 'use strict';
  
  /* jshint esnext: true */
  
  const exec = require('child-process-promise').exec;
  const path = require('path');
  const fs = require('fs-extra-promise');
  const thaliConfig = require('../package.json');
  const os = require('os');
  const http = require('http');
  const url = require('url');
  const assert = require('assert');
  
  const versions =
  {
    xcode: '8.3.3',
    xcodeCommandLineTools: ' ',
    macOS: '10.12.6',
    node: '6.11.1',
    npm: '3.10.10',
    brew: '1.2.',
    ruby: '2.4.1p111',
    wget: '1.18',
    jxcore: '0.3.1.14',
    androidHome: ' ',
    androidBuildTools: thaliConfig.thaliInstall.androidConfig.buildToolsVersion,
    androidPlatform: thaliConfig.thaliInstall.androidConfig.compileSdkVersion,
    // We don't have an easy way to identify the version of the support libraries
    // we have but if they were installed recently enough then they will have
    // what we need.
    androidSupportLibraries: '47.0.0',
    python: '2.7.10',
    cordova: '6.5.0',
    java: '1.8.0_141',
    git: '2.13.1',
    swiftlint: '0.20.1',
    carthage: '0.24.0',
    sinopiaNode: ' ',
    sinopiaJxCore: ' '
  };
  
  module.exports.versions = versions;
  
  function sinopiaVersionCheck(sinopiaUrl) {
    return new Promise((resolve, reject) => {
      var parsedUrl = url.parse(sinopiaUrl);
      if (parsedUrl.protocol !== 'http:') {
        return Promise.reject();
      }
      var testThaliUrl =
        sinopiaUrl + (sinopiaUrl.endsWith('/') ? '' : '/') + 'thali';
      http.get(testThaliUrl, (res) => {
        let result = '';
        res.on('data', (chunk) => result += chunk);
        res.on('end', () => {
          try {
            var thaliResponse = JSON.parse(result);
            thaliResponse.name === 'thali' ? resolve() : reject();
          } catch (e) {
            reject(e);
          }
        });
        res.on('error', (err) => reject(err));
      });
    });
  }
  
  function boolToPromise(result) {
    return result ? Promise.resolve() : Promise.reject();
  }
  
  const commandsAndResults =
  {
    xcode: {
      platform: ['darwin'],
      versionCheck: 'xcodebuild -version',
      versionValidate:
        (result, version) =>
          boolToPromise(result.startsWith('Xcode '+ version + '\n'))
    },
    xcodeCommandLineTools: {
      platform: ['darwin'],
      // I couldn't find any reliable way to validate which versions of the
      // tools are installed. The best I could do was find out which directory
      // they are supposed to be in. I tried http://stackoverflow.com/questions/15371925/how-to-check-if-command-line-tools-is-installed
      // and xcode-select -p returns a directory inside of XCode and none of
      // the pkgutil commands worked properly on my machine.
      // Also note that in some circumstances this directory can also contain
      // a subdirectory called SDKs. It's not clear if it's always there so we
      // don't check for it.
      versionCheck: () => fs.readdirAsync('/Library/Developer/CommandLineTools'),
      versionValidate:
        (result) => boolToPromise(result && result.length >= 2 &&
                      result.indexOf('Library') !== -1 &&
                      result.indexOf('usr') !== -1)
    },
    macOS: {
      platform: ['darwin'],
      versionCheck: 'sw_vers -productVersion',
      versionValidate:
        (result, version) => boolToPromise(version === result.trim())
    },
    node: {
      versionCheck: 'node -v',
      versionValidate:
        (result, version) => boolToPromise('v' + version === result.trim())
    },
    npm: {
      versionCheck: 'npm -v',
      versionValidate:
        (result, version) => boolToPromise(version === result.trim())
    },
    brew: {
      platform: ['darwin'],
      versionCheck: 'brew -v',
      versionValidate:
        (result, version) =>
          boolToPromise(result.startsWith('Homebrew ' + version))
    },
    ruby: {
      platform: ['darwin'],
      versionCheck: 'ruby -v',
      versionValidate:
        (result, version) =>
          boolToPromise(result.startsWith('ruby ' + version + ' '))
    },
    wget: {
      versionCheck: 'wget -V',
      versionValidate:
        (result, version) =>
          boolToPromise(result.startsWith('GNU Wget ' + version + ' '))
    },
    jxcore: {
      versionCheck: 'jx -jxv',
      versionValidate:
        (result, version) => boolToPromise('v' + version === result.trim())
    },
    androidHome: {
      versionCheck: () => process.env.ANDROID_HOME,
      versionValidate:
        (result) => {
          if (result) {
            return fs.readdirAsync(result);
          } else {
            return Promise.reject();
          }
        }
    },
    androidBuildTools: {
      versionCheck: () => fs.readdirAsync(path.join(process.env.ANDROID_HOME,
        'build-tools')),
      versionValidate:
        (result, version) => boolToPromise(result.indexOf(version) !== -1)
    },
    androidPlatform: {
      versionCheck: () => fs.readdirAsync(path.join(process.env.ANDROID_HOME,
        'platforms')),
      versionValidate:
        (result, version) => boolToPromise(result.indexOf(version) !== -1)
    },
    androidSupportLibraries: {
      versionCheck: () => {
        const sourcePropertiesLocation =
          path.join(process.env.ANDROID_HOME,
            'extras/android/m2repository/source.properties');
        return fs.readFileAsync(sourcePropertiesLocation, 'utf8')
          .then((sourcePropertiesFileContents) => {
            const regEx =
              sourcePropertiesFileContents.match(/^Pkg\.Revision=(.*)$/m);
            if (!regEx[1]) {
              return Promise.reject();
            }
            return Promise.resolve(regEx[1]);
          });
      },
      versionValidate:
        (result, version) => boolToPromise(version === result.trim())
    },
    python: {
      versionCheck: 'python -V',
      checkStdErr: true, // http://bugs.python.org/issue28160 - fixed in 3.4
      versionValidate:
        (result, version) => boolToPromise('Python ' + version === result.trim())
    },
    cordova: {
      versionCheck: 'cordova -v',
      versionValidate:
        (result, version) => boolToPromise(version === result.trim())
    },
    java: {
      versionCheck: 'java -version',
      checkStdErr: true, // http://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8166116
      versionValidate:
        (result, version) => boolToPromise(result.startsWith('java version "' +
                                            version + '"\n'))
    },
    git: {
      versionCheck: 'git --version',
      versionValidate:
        (result, version) => boolToPromise(
                                      'git version ' + version === result.trim())
    },
    swiftlint: {
      platform: ['darwin'],
      versionCheck: 'swiftlint version',
      versionValidate:
        (result, version) => boolToPromise(version === result.trim())
    },
    carthage: {
      // We can run into the situation when carthage proposes new updates
      // In this case output is similar to:
      //  *** Please update to the latest Carthage version: 0.20.1. You currently are on 0.20.0
      //  0.20.0
      // This is why we're reading only the last line from output
      platform: ['darwin'],
      versionCheck: 'carthage version | tail -n 1',
      versionValidate:
        (result, version) => boolToPromise(version === result.trim())
    },
    sinopiaNode: {
      versionCheck: 'npm get registry',
      versionValidate:
        (result) => sinopiaVersionCheck(result.trim())
    },
    sinopiaJxCore: {
      // The first time jx npm is run it can do an install, to simplify things
      // we just call it twice so the second call will be normal and give us the
      // url
      versionCheck: 'jx npm get registry > /dev/null && jx npm get registry',
      versionValidate:
        (result) => sinopiaVersionCheck(result.trim())
    }
  
  };
  
  
  function execAndCheck(command, checkStdErr, version, validator) {
    return exec(command)
      .then((result) => {
        const output = checkStdErr ? result.stderr : result.stdout;
        return validator(output, version);
      })
      .then(() => Promise.resolve(true))
      .catch(() => Promise.reject(new Error('Command: ' + command + ' failed')));
  }
  
  function promiseTry(fn) {
    try {
      return Promise.resolve(fn());
    } catch (error) {
      return Promise.reject(error);
    }
  }
  
  /**
   * Checks if the named object is installed with the named version, if any. If
   * versionNumber isn't given then we default to checking the versions global
   * object.
   * @param {string} objectName Name of the object to validate
   * @param {string} [versionNumber] An optional string specifying the desired
   * version. If omitted we will check the versions structure.
   * @returns {Promise<Error|boolean>} If the desired object is found at the
   * desired version then a resolve will be returned set to true. Otherwise an
   * error will be returned specifying what went wrong.
   */
  function checkVersion(objectName, versionNumber) {
    const desiredVersion = versionNumber ? versionNumber : versions[objectName];
    const commandAndResult = commandsAndResults[objectName];
    if (!commandAndResult) {
      return Promise.reject(
        new Error('Unrecognized objectName in commandsAndResults'));
    }
    if (!desiredVersion) {
      return Promise.reject(new Error('Unrecognized objectName in versions'));
    }
    if (commandAndResult.platform &&
        !commandAndResult.platform.includes(os.platform()))
    {
      return Promise.reject(new Error('Requested object is not supported on ' +
        'this platform'));
    }
    if (typeof commandAndResult.versionCheck === 'function') {
      return promiseTry(commandAndResult.versionCheck)
        .catch(() =>
          Promise.reject(new Error('Version Check failed on ' + objectName)))
        .then((versionCheckResult) =>
          commandAndResult.versionValidate(versionCheckResult, desiredVersion))
        .then(() => Promise.resolve(true))
        .catch(() => Promise.reject(
                          new Error('Version not installed of ' + objectName)));
    }
    return execAndCheck(commandAndResult.versionCheck,
                        commandAndResult.checkStdErr,
                        desiredVersion,
                        commandAndResult.versionValidate);
  }
  
  module.exports.checkVersion = checkVersion;
  
  function processCommandsAndResults(commandsAndResults) {
    let passed = true;
    let errorMessage = '';
    let runningPromise = Object.getOwnPropertyNames(commandsAndResults)
          .reduce((runningPromise, name) => {
            if (commandsAndResults[name].platform &&
              !commandsAndResults[name].platform.includes(os.platform())) {
              return runningPromise;
            }
            return runningPromise.then(() =>
              checkVersion(name)
                .catch((err) => {
                  passed = false;
                  errorMessage += '\n' + 'Object Name: ' + name + ' : ' + err;
                  return Promise.resolve();
                }));
          }, Promise.resolve());
    return runningPromise
      .then(() => passed ? Promise.resolve() : Promise.reject(errorMessage));
  }
  
  // Detects if we were called from the command line
  if (require.main === module) {
    if (os.platform() !== 'darwin') {
      console.log('WARNING: WE ONLY SUPPORT OS/X AS A BUILD PLATFORM, USING ANY' +
        'OTHER PLATFORM IS NOT OFFICIALLY SUPPORTED. WE STILL CHECK A FEW ' +
        'THINGS BUT YOU ARE REALLY ON YOUR OWN');
    }
    assert.deepStrictEqual(Object.getOwnPropertyNames(versions),
                           Object.getOwnPropertyNames(commandsAndResults),
                           'Versions and commandsAndResults keys must be equal');
    processCommandsAndResults(commandsAndResults)
      .then(() => {
        // Good to clean this up in case we have changed the version of jxcore
        const home = process.env.HOME;
        const jx = path.join(home, '.jx');
        const jxc = path.join(home, '.jxc');
        const nodeGyp = path.join(home, '.node-gyp');
        return Promise.all([fs.removeAsync(jx), fs.removeAsync(jxc),
                           fs.removeAsync(nodeGyp)]);
      })
      .then(() => {
        console.log('Environment validated');
        process.exit(0);
      })
      .catch((err) => {
        console.log('Environment not valid: ' + err);
        process.exit(-1);
      });
  }
  