/*
	Licensed Material - Property of IBM

	(C) Copyright 2016 IBM Corp.

	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.
*/


'use strict';

var moduleName = 'webUpdate'; // - name for clarification during logging

//private modules
var createFile = require('./createFile');
var createZip = require('./createZip');
var copyWWWFolder = require('./copyWWWFolder');
var messages = require('./externalizedStrings');
var generateDeploymentData = require('./generateDeploymentData');
var temporaryDirectory = require('./temporaryDirectory');
var cordovaPrepare = require('mdo-cordova-support').prepare;
var generateChecksum = require('mdo-cordova-support').generateChecksum;
var strings = require('ibm-strings');
var errors = require('mdo-errors');

//3rd party modules
var fs = require('fs');
var log = require('npmlog');
var path = require('path');
var Q = require('q');

var moduleName = 'webupdate';

/**
// Function to perform the webupdate flow, this is the flow where the user did not supply the --file flag
   This function runs once for each platform in the platforms array of the projectObj
   @param projectObj - the projectObj return from the detectPlatforms function
   @param serverDetails - the server details object returned from getServerDetails
   @param buildOnly - the result of the --build flag passed into the args array

**/
module.exports = function webUpdate(projectObj, serverDetails, buildOnly) {
  var filteredPlatforms,
      registeredApplications,
      windowsNotSupportedMessageShown = false,
      browserNotSupportedMessageShown = false,
      d = Q.defer(),
      getApplicationsPromise = Q.defer();

  //If we are not in buildOnly mode (--build) then we should get the applications from the server
  if(!buildOnly) {
    getApplicationsPromise = serverDetails.getApplications();
  } else {
    getApplicationsPromise.resolve();
    getApplicationsPromise = getApplicationsPromise.promise;
  }

  getApplicationsPromise.then(function (applications) {
    log.silly(moduleName, 'retrieved applications if we are not in build only mode' + JSON.stringify(applications));
    registeredApplications = applications;

    log.silly(moduleName, 'Mapping series');

    //Filter out Windows and browser and display a 'platform unsupported' message only once
    filteredPlatforms = projectObj.platforms.filter(function (el) {
      if(el.platform.indexOf('windows') >= 0 && !windowsNotSupportedMessageShown) {
        var windowsNotSupportedErr = errors.getError("WINDOWSNOTSUPPORTEDWEBUPDATE", 'windows');
        strings.warn(windowsNotSupportedErr.description);
        windowsNotSupportedMessageShown = true;
      } else if(el.platform.indexOf('browser') >= 0 && !browserNotSupportedMessageShown) {
          var browserNotSupportedErr = errors.getError("BROWSERNOTSUPPORTEDWEBUPDATE", 'browser');
          strings.warn(browserNotSupportedErr.description);
          browserNotSupportedMessageShown = true;
      }
      return el.platform.indexOf('windows') < 0 && el.platform.indexOf('browser') < 0;
    });

    return mapSeries(filteredPlatforms, buildFilesForUpdate, serverDetails, registeredApplications, buildOnly);
  })
  .then(function () {
    d.resolve();
  })
  .catch(function (err) {
    log.silly(moduleName, 'Webupdate catch block executed, err: ' + JSON.stringify(err));
    d.reject(err);
  });

  return d.promise;
};

/**
  The core of the webupdate logic.  This function does the heavy work of doing the Cordova prepare, checksum, building the project assets
    and deploying them.
  This function runs for each platform present in the projectObj.platforms array,

  @param platform - the current platform object, for instance projectObj.platforms[0], etc
  @param serverDetails - the serverDetails object
  @param registeredApplications - the current applications that are on the server, this can be undefined if the --build flag is enabled
  @param buildOnly - the result of the --build flag
**/
function buildFilesForUpdate(platform, serverDetails, registeredApplications, buildOnly) {
  var tempDir,
      tempMetaDir,
      deploymentData,
      mobilefirstDir,
      webResourceFileName,
      d = Q.defer();

  log.silly(moduleName, 'Generate checksum for platform: ' + platform.platform);
  var configXMLDir = path.resolve(platform.configXMLPath, '..');

  cordovaPrepare(platform.platform)
  .then(function () {
    //, '--path', configXMLDir, '--platforms', platform];
    var checksumArgs = ['--path', configXMLDir, '--platforms', platform.platform];
    return generateChecksum(configXMLDir, checksumArgs);
  })
  .then(function () {
    log.silly(moduleName, 'Building tempdir for platform: ' + platform.platform);
    return createTemporaryDir(platform);
  })
  .then(function (dirPath) {
    log.silly(moduleName, 'temporary directory created at: ' + dirPath);
    tempDir = dirPath;
    tempMetaDir = path.resolve(tempDir, 'meta');
    //Do the checksum,
    log.silly(moduleName, 'copying www folder to temporary directory');
    return copyWWWFolder(platform, tempDir);
  })
  .then(function (unpackedSize) {
    log.silly(moduleName, 'generating deployment.data file for platform: ' + platform.platform);
    return generateDeploymentData(platform, unpackedSize);
  })
  .then(function (retDeploymentData) {
    deploymentData = retDeploymentData;
    log.silly(moduleName, 'writing deployment.data info to file');
    var fileName = 'deployment.data';
    return createFile(deploymentData, fileName, tempMetaDir);
  })
  .then(function () {
    mobilefirstDir = processMobileFirstDir(platform);

    log.silly(moduleName, 'creating zip file for web resource deployment');
    webResourceFileName = deploymentData['app.id'] + '-' + deploymentData['app.platform'] + '-' + deploymentData['app.version'] + '.zip';
    return createZip(tempDir, mobilefirstDir, webResourceFileName);
  })
  .then(function () {
    strings.info(strings.format(messages.builtApp, platform.platform));
    if(!buildOnly) {
      log.silly(moduleName, 'Proceed with the deployment of the zip file');
      var serverJSON = generateWebUpdateJSON(platform, deploymentData);
      var webResourcesFilePath = path.resolve(mobilefirstDir, webResourceFileName);

      //If the server is not registered do not deploy
      var isRegistered = verifyApplicationIsRegistered(registeredApplications, deploymentData);

      if(isRegistered) {
        return serverDetails.deployWebupdate(serverJSON.applicationKey, webResourcesFilePath);
      } else {
        return Q.reject(errors.getError("APPNOTREGISTEREDBEFOREDEPLOYMENT"));
      }
    }
  })
  .then(function () {
    //If we are supposed to only build the project and not deploy then don't show deployed message
    if(!buildOnly) {
      strings.info(strings.format(messages.deployedApp, platform.platform));
    }
  })
  .catch(function (err) {
    log.silly(moduleName, 'buildFilesForUpdate catch block executed, err: ' + JSON.stringify(err));
    if(buildOnly) {
      strings.fatal(strings.format(messages.failedBuild, platform.platform));
    } else {
      strings.fatal(strings.format(messages.failedDeployment, platform.platform));
    }
    //The checksum code has a chance to throw an error.  If a expected folder doesn't exist for the checksum script it will
    //throw a err.code -2, any other error it will be non-zero.  On Unix environments this is translated to an unsigned in, so -2 -> 254,
    //The isNaN is needed because the error could be an mdo-error which err.code will not be a number and we want to exclude those errors
    //  So we focus on just the err.code that the prepare, and checksum scripts can throw, which are numeric
    if(!isNaN(err.code) && (err.code === 254 || err.code === -2)) {
      //Checksum code is returning just an error code, so we get a corresponding error
      err = errors.getError("INVALIDPLATFORMVERSION", platform.platform);
    } else if(!isNaN(err.code) && err.code !== 0) {
      err = errors.getError('ERRORDURINGEXTERNALSCRIPT', platform.platform);
    }
    return err;
  })
  .done(function (err) {
    //Try to clean up the directory, if we can't we just log it. If there was an error from above we reject, else we resolve
    temporaryDirectory.cleanUp(tempDir)
    .done(function (tempDirError) {
      if(tempDirError) {
        log.silly(moduleName, 'Error cleaning up temporary directory: ' + tempDirError);
      }
      //If we had an error from our catch block we d.reject with that error
      if(err && err.code !== 0) {
        d.reject(err);
      } else {
        d.resolve();
      }
    });
  });

  return d.promise;
}

/**
  Function to run a promise sequence in syncronous order. This is what allows each platform to
   execute the webupdate function, one at a time
**/
function mapSeries (arr, iterator, serverDetails, registeredApplications, buildOnly) {
  // create a empty promise to start our series (so we can use `then`)
  var currentPromise = new Q();
  var promises = arr.map(function (el) {
    return currentPromise = currentPromise.then(function () {
      // execute the next function after the previous has resolved successfully
      log.silly(moduleName, 'iterating buildFileFunction for platform: ' + el.platform);
      return iterator(el, serverDetails, registeredApplications, buildOnly);
    });
  });
  // group the results and return the group promise
  return Q.all(promises);
}

/**
  Function to generate the JSON to send to the server to let it know what app to deploy the resources for
  @param platform - the platform of the file
  @param configXMLPath - the path to the config.xml.
**/
function generateWebUpdateJSON(platformObj, appInfo) {
    var idName;
    if(platformObj.platform === 'android') {
        idName = 'packageName';
    } else if(platformObj.platform === 'ios') {
        idName = 'bundleId';
    } else {
        idName = 'assemblyName';
    }

    var registerJSON = {
        applicationKey: {
            version: appInfo['app.version'],
            clientPlatform: platformObj.platform,
        }
    };
    registerJSON.applicationKey[idName] = appInfo['app.id'];

    return registerJSON;
}

/**
  Function to check if the mobilefirst directory needs to be created or if it already exists
  @param platform - the current platform object from the platforms array of the projectObj
**/
function processMobileFirstDir(platform) {
  var mobilefirstDir = path.resolve(platform.configXMLPath, '..', 'mobilefirst');
  try {
    fs.statSync(mobilefirstDir);
  } catch (err) {
    fs.mkdirSync(mobilefirstDir);
  }

  return mobilefirstDir;
}

/**
  Function to create a temporary directory, so that the deployment.data and www folder can be copied to it and then zipped up
  @param platform - the current platform object from the platforms array of the projectObj
**/
function createTemporaryDir(platform) {
  var d = Q.defer();

  var tempDirName = 'tmp-' + platform.appName + '-' + platform.platform + '-' + new Date().getTime();
  temporaryDirectory.create(tempDirName)
  .then(function (tempdir) {
    var metaPath = path.resolve(tempdir, 'meta');
    Q.nfcall(fs.mkdir, metaPath)
    .then(d.resolve(tempdir));
  })
  .catch(function (err) {
    log.silly(moduleName, 'Error making meta directory inside temp dir: ' + err);
    d.reject(err);
  });

  return d.promise;
}

/**
  Function to check that the app we are trying to deploy the resources of already exists on the server
  If it does not exist we throw an error
  @param registeredApplications - JSON of the registered applications on the server, returned from getApplications function
  @param currentPlatform - the deployment.data object of the current platform executing the webupdate block
**/
function verifyApplicationIsRegistered(registeredApplications, currentPlatform) {
  var isRegistered = false;

  registeredApplications.items.forEach(function (app) {
    if(app.name === currentPlatform['app.id']) {
      app.envs.forEach(function (env) {
        if(env.environment === currentPlatform['app.platform']) {
          isRegistered = true;
        }
      });
    }
  });

  return isRegistered;
}
