//
// Copyright (c) Microsoft and contributors.  All rights reserved.
//
// Licensed 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 crypto = require('crypto');
var fs = require('fs');
var path = require('path');
var url = require('url');
var util = require('util');
var uuid = require('node-uuid');

var azureCommon = require('azure-common');
var _ = require('underscore');

var blobUtils = require('./blobUtils');
var constants = require('./constants');
var log = require('./logging');
var utilsCore = require('./utilsCore');

var locale = require('../locales/en-us.json');

var BEGIN_CERT = '-----BEGIN CERTIFICATE-----';
var END_CERT = '-----END CERTIFICATE-----';

exports.POLL_REQUEST_INTERVAL = 1000;

var moduleVersion = require('../../package.json').version;

exports.moduleVersion = moduleVersion;

exports.azureDir = utilsCore.azureDir;
exports.camelcase = utilsCore.camelcase;
exports.ignoreCaseEquals = utilsCore.ignoreCaseEquals;
exports.stringStartsWith = utilsCore.stringStartsWith;
exports.pathExistsSync = utilsCore.pathExistsSync;

var getUserAgent = exports.getUserAgent = function () {
  var userAgent = util.format('WindowsAzureXplatCLI/%s', moduleVersion);
  if (process.env.AZURE_HTTP_USER_AGENT) {
    userAgent += ';' + process.env.AZURE_HTTP_USER_AGENT;
  }
  return userAgent;
};

exports.createClient = function (factoryMethod, credentials, endpoint, options) {
  if(!options) {
    options = {};
  }
  
  var client = factoryMethod(credentials,
    exports.stringTrimEnd(endpoint, '/'))
    .withFilter(exports.certAuthFilter(credentials))
    .withFilter(azureCommon.UserAgentFilter.create(exports.getUserAgent()))
    .withFilter(exports.createPostBodyFilter())
    .withFilter(polishErrorCausedByArmProviderNotRegistered());
  
  if(!options.disableAutoRedirect) {
    client = client.withFilter(exports.createFollowRedirectFilter());
  }
  
  if(!options.disableLogFilter) {
    client = client.withFilter(log.createLogFilter());
  }
  
  return client;
};

/**
 * Create Autorest client
 * @param {function} Method to create an instance of the client
 * @param {object} Subscription object
 * @param {object} [options] - Optional parameters
 * @param {boolean} [options.isTenantBased] - A Boolean value (true) indicating that the client is tenant based
 * @param {string} [options.endPoint] - The url specifying the endpoint (baseUrl) for the client
 */

exports.createAutoRestClient = function (FactoryMethod, subscription, options) {
  ensureAADBackedSubscription(subscription);
  var credentials = null;
  var baseUri = null;
  var subscriptionOrTenantId = null;

  if (options && options.isTenantBased && options.isTenantBased === true) {
    if (!options.endPoint) {
      options.endPoint = subscription.resourceManagerEndpointUrl;
    }
    subscriptionOrTenantId = subscription.tenantId;
    credentials = subscription._createCredentials(options.endPoint);
    baseUri = exports.stringTrimEnd(options.endPoint, '/');
  } else {
    subscriptionOrTenantId = subscription.id;
    credentials = subscription._createCredentials();
    baseUri = exports.stringTrimEnd(subscription.resourceManagerEndpointUrl, '/');
  }

  var factoryMethodOptions = {};
  factoryMethodOptions.filters = [
    exports.certAuthFilter(credentials),
    log.createLogFilter(),
    azureCommon.UserAgentFilter.create(exports.getUserAgent()),
    exports.createPostBodyFilter(),
    exports.createFollowRedirectFilter(),
    polishErrorCausedByArmProviderNotRegistered()
  ];
  
  // there are a subset of clients that do not require a subscription
  // TODO: need to figure out a way to pass options into factoryMethodOptions.
  if(options && options.noSubscription) {
    return new FactoryMethod(credentials, factoryMethodOptions);
  }
  else {
    return new FactoryMethod(credentials, subscriptionOrTenantId, baseUri, factoryMethodOptions);
  }
};

function _createAsmClient(factoryMethod, subscription) {
  return exports.createClient(factoryMethod,
    subscription._createCredentials(),
    subscription.managementEndpointUrl);
}

function _createArmClient(factoryMethod, subscription, options) {
  ensureAADBackedSubscription(subscription);
  if(!options) {
    options = {};
  }
  
  if(!options.endpoint) {
    options.endpoint = subscription.resourceManagerEndpointUrl;
  }
  
  return exports.createClient(factoryMethod,
    subscription._createCredentials(),
    options.endpoint,
    options);
}

function ensureAADBackedSubscription(subscription) {
  if (subscription.wasCreatedFromPublishSettingsFile()) {
    throw new Error('The current cmdlet requires you to log in using Azure Active Directory account, ' + 
      'not from a .publishsettings file. Please run \'azure login\' or use \'azure account set\' ' + 
      'to select a correct subscription.');
  }
}

exports.getHDInsightClusterManagementClient = function (cloudServiceName, subscription) {
  var factoryMethod = require('azure-asm-hdinsight').createHDInsightClusterManagementClient;
  var client = _createAsmClient(factoryMethod, subscription);
  client.cloudServiceName = cloudServiceName;
  return client;
};

exports.getHDInsightCluster2ManagementClient = function (cloudServiceName, subscription) {
  var factoryMethod = require('azure-asm-hdinsight').createHDInsightCluster2ManagementClient;
  var client = _createAsmClient(factoryMethod, subscription);
  client.cloudServiceName = cloudServiceName;
  return client;
};

exports.getHDInsightJobManagementClient = function (clusterDnsName, userName, password) {
  var hdinsight = require('azure-asm-hdinsight');
  var client = hdinsight.createHDInsightJobManagementClient(clusterDnsName,
    new hdinsight.createBasicAuthenticationCloudCredentials({ username: userName, password: password }));
  return client;
};

exports.createCdnManagementClient = function (subscription) {
  var factoryMethod = require('azure-arm-cdn');
  return this.createAutoRestClient(factoryMethod, subscription);
};

exports.createRedisCacheManagementClient = function (subscription) {
  var factoryMethod = require('azure-arm-rediscache');
  return this.createAutoRestClient(factoryMethod, subscription);
};

exports.createHDInsightManagementClient = function (subscription) {
    var factoryMethod = require('azure-arm-hdinsight').createHDInsightManagementClient;
    return _createArmClient(factoryMethod, subscription);
};

exports.createHDInsightJobManagementClient = function (subscription) {
    var factoryMethod = require('azure-arm-hdinsight-jobs').createHDInsightJobManagementClient;
    return _createArmClient(factoryMethod, subscription);
};

exports.createInsightsClient = function (subscription) {
  var factoryMethod = require('azure-arm-insights').createInsightsClient;
  return _createArmClient(factoryMethod, subscription);
};

exports.createInsightsManagementClient = function (subscription) {
  var factoryMethod = require('azure-arm-insights').createInsightsManagementClient;
  return _createArmClient(factoryMethod, subscription);
};

//website provider might not be registered yet, so make sure to register it
exports.createWebsiteClient = function (subscription, callback) {
  var client;
  subscription.registerAsmProvider('website', function (err) {
    if (err) {
      return callback(err);
    }
    var factoryMethod = require('azure-asm-website').createWebSiteManagementClient;
    client = _createAsmClient(factoryMethod, subscription);
    return callback(null, client);
  });
};

exports.createWebSiteExtensionsClient = function (siteName, hostNameSuffix, username, password) {
  var baseUri = util.format('https://%s.scm.%s:443', siteName, hostNameSuffix);
  var azureWebSite = require('azure-asm-website');
  var service = azureWebSite.createWebSiteExtensionsClient(siteName, new azureWebSite.createBasicAuthenticationCloudCredentials({
    username: username,
    password: password,
  }), baseUri)
    .withFilter(log.createLogFilter())
    .withFilter(azureCommon.UserAgentFilter.create(getUserAgent()))
    .withFilter(createPostBodyFilter())
    .withFilter(createFollowRedirectFilter());

  return service;
};

exports.createWebappManagementClient = function (subscription) {
  var factoryMethod = require('azure-arm-website').createWebSiteManagementClient;
  return _createArmClient(factoryMethod, subscription);
};

exports.createUsageManagementClient = function (subscription) {
  var factoryMethod = require('azure-arm-commerce').createUsageAggregationManagementClient;
  return _createArmClient(factoryMethod, subscription);
};

exports.createSqlClient = function (subscription) {
  var factoryMethod = require('azure-asm-sql').createSqlManagementClient;
  return _createAsmClient(factoryMethod, subscription);
};

exports.createServiceBusClient = function (subscription) {
  var factoryMethod = require('azure-asm-sb').createServiceBusManagementClient;
  return _createAsmClient(factoryMethod, subscription);
};

exports.createManagementClient = function (subscription) {
  var factoryMethod = require('azure-asm-mgmt').createManagementClient;
  return _createAsmClient(factoryMethod, subscription);
};

exports.createStorageClient = function (subscription) {
  var factoryMethod = require('azure-asm-storage').createStorageManagementClient;
  return _createAsmClient(factoryMethod, subscription);
};

exports.createStorageResourceProviderClient = function (subscription) {
  var clientConstructor = require('azure-arm-storage');
  return this.createAutoRestClient(clientConstructor, subscription);
};

exports.createServerManagementClient = function(subscription) { 
  var clientConstructor = require('azure-arm-servermanagement');
  return this.createAutoRestClient(clientConstructor, subscription);
};

exports.createBatchResourceProviderClient = function (subscription) {
  var factoryMethod = require('azure-arm-batch');
  return this.createAutoRestClient(factoryMethod, subscription);
};

exports.createBatchClient = function (accountName, accountKey, taskUri) {
  var batch = require('azure-batch');
  var credentials = new batch.SharedKeyCredentials(accountName, accountKey);

  var options = {};
  options.filters = [
      log.createLogFilter(),
      azureCommon.UserAgentFilter.create(exports.getUserAgent()),
      exports.createPostBodyFilter(),
      exports.createFollowRedirectFilter(),
  ];
  options.generateClientRequestId = false;
  
  var client = new batch.ServiceClient(credentials, taskUri, options);
  return client;
};

/**
 * Deprecated, use createComputeManagementClient instead.
 */
exports.createComputeResourceProviderClient = function (subscription) {
  // TODO: delete 'armsdk' folder after migration to autorest SDK
  var factoryMethod = require('./../commands/arm/armsdk/compute').createComputeResourceProviderClient;
  return _createArmClient(factoryMethod, subscription);
};

exports.createComputeManagementClient = function (subscription) {
  var factoryMethod = require('azure-arm-compute');
  return this.createAutoRestClient(factoryMethod, subscription);
};

exports.createNetworkManagementClient = function (subscription) {
  var factoryMethod = require('azure-arm-network');
  return this.createAutoRestClient(factoryMethod, subscription);
};

exports.createPowerbiManagementClient = function (subscription) {
  var factoryMethod = require('azure-arm-powerbiembedded');
  return this.createAutoRestClient(factoryMethod, subscription);
};

exports.createGraphManagementClient = function (subscription) {
  var factoryMethod = require('azure-graph');
  // Specify tenant based client and endpoint in options
  var options = { isTenantBased: true, endPoint: subscription.activeDirectoryGraphResourceId };
  return this.createAutoRestClient(factoryMethod, subscription, options);
};

exports.createTrafficManagerResourceProviderClient = function (subscription) {
  var factoryMethod = require('azure-arm-trafficmanager').createTrafficManagerManagementClient;
  return _createArmClient(factoryMethod, subscription);
};

exports.createDnsResourceProviderClient = function (subscription) {
  var factoryMethod = require('azure-arm-dns').createDnsManagementClient;
  return _createArmClient(factoryMethod, subscription);
};

exports.createComputeClient = function (subscription) {
  var factoryMethod = require('azure-asm-compute').createComputeManagementClient;
  return _createAsmClient(factoryMethod, subscription);
};

exports.createNetworkClient = function (subscription) {
  var factoryMethod = require('azure-asm-network').createNetworkManagementClient;
  return _createAsmClient(factoryMethod, subscription);
};

exports.createTrafficManagerClient = function (subscription) {
  var factoryMethod = require('azure-asm-trafficmanager').createTrafficManagerManagementClient;
  return _createAsmClient(factoryMethod, subscription);
};

exports.createResourceClient = function (subscription) {
  var factoryMethod = require('azure-arm-resource').ResourceManagementClient;
  return this.createAutoRestClient(factoryMethod, subscription);
};

exports.createPolicyClient = function (subscription) {
  var factoryMethod = require('azure-arm-resource').PolicyClient;
  return this.createAutoRestClient(factoryMethod, subscription);
};

exports.createResourceManagerClient = function (subscription) {
  // TODO: delete 'armsdk' folder after migration to autorest SDK
  var factoryMethod = require('./../commands/arm/armsdk/resource').createResourceManagementClient;
  return _createArmClient(factoryMethod, subscription);
};

exports.createResourceFeatureClient = function (subscription) {
  var factoryMethod = require('azure-arm-resource').FeatureClient;
  return this.createAutoRestClient(factoryMethod, subscription);
};

exports.createEventsClient = function (subscription) {
  var factoryMethod = require('azure-monitoring').createEventsClient;
  return _createArmClient(factoryMethod, subscription);
};

exports.createKeyVaultClient = function (subscription, vaultUri) {
  var keyvault = require('azure-keyvault');
  var factoryMethod = keyvault.createKeyVaultClient;
  
  var authenticator = function (challenge, callback) {
    // challenge.resource contains keyvault resourceId
    var retrieveTokenCallback = function(err, scheme, token) {
      return callback(err, util.format('%s %s', scheme, token));
    };
    var tokenCredentials = subscription._createCredentials(challenge.resource);
    return tokenCredentials.retrieveTokenFromCache(retrieveTokenCallback);
  };
  
  var credentials;
  if(subscription.user) {
    credentials = new keyvault.KeyVaultCredentials(authenticator);
  } else if(subscription.managementCertificate) {
    credentials = subscription._createCredentials();
  } else {
    throw new Error('Unsupported subscription object.');
  }
  return exports.createClient(factoryMethod,
                              credentials,
                              vaultUri);
};

exports.createGalleryClient = function (subscription) {
  var factoryMethod = require('azure-gallery').createGalleryClient;
  return exports.createClient(factoryMethod,
    new azureCommon.AnonymousCloudCredentials(),
    subscription.galleryEndpointUrl);
};

exports.createMobileClient = function (subscription) {
  return _createAsmClient(function (credentials) {

    //propagates errors that ErrorHandlingFilter in azureCommon.Service would swallow
    var errorPropagationFilter = function handle(resource, next, callback) {
      return next(resource, function (err, response, body) {
        if (body !== undefined && response !== undefined && (response.statusCode < 200 || response.statusCode >= 300)) {
          callback(body, response, body);
        } else {
          callback(err, response, body);
        }
      });
    };

    var client = new azureCommon.Service(credentials, [errorPropagationFilter]);
    client.longRunningOperationRetryTimeout = 5000;
    return client;
  }, subscription);
};

exports.createDataLakeStoreFileSystemManagementClient = function (subscription) {
  var factoryMethod = require('azure-arm-datalake-store').DataLakeStoreFileSystemClient;
  var options = {
    noSubscription: true,
    dnsSuffix: subscription.azureDataLakeStoreFileSystemEndpointSuffix,
  };
  
  return this.createAutoRestClient(factoryMethod, subscription, options);
};

exports.createDataLakeStoreManagementClient = function (subscription) {
  var factoryMethod = require('azure-arm-datalake-store').DataLakeStoreAccountClient;
  return this.createAutoRestClient(factoryMethod, subscription);
};

exports.createDataLakeAnalyticsManagementClient = function (subscription) {
  var factoryMethod = require('azure-arm-datalake-analytics').DataLakeAnalyticsAccountClient;
  return this.createAutoRestClient(factoryMethod, subscription);
};

exports.createDataLakeAnalyticsJobManagementClient = function (subscription) {
  var options = {
    noSubscription: true,
    dnsSuffix: subscription.azureDataLakeAnalyticsCatalogAndJobEndpointSuffix
  };
  var factoryMethod = require('azure-arm-datalake-analytics').DataLakeAnalyticsJobClient;
  return this.createAutoRestClient(factoryMethod, subscription, options);
};

exports.createDataLakeAnalyticsCatalogManagementClient = function (subscription) {
  var options = {
    noSubscription: true,
    dnsSuffix: subscription.azureDataLakeAnalyticsCatalogAndJobEndpointSuffix
  };
  
  var factoryMethod = require('azure-arm-datalake-analytics').DataLakeAnalyticsCatalogClient;
  return this.createAutoRestClient(factoryMethod, subscription, options);
};

exports.createDevTestLabsClient = function (subscription) {
    var clientConstructor = require('azure-arm-devtestlabs');
    return this.createAutoRestClient(clientConstructor, subscription);
};

/**
 * Create old-style service object
 * @param {string} serviceFactoryName name of factory function off azure module
 */
function createService(factoryMethod, subscription) {
  var managementEndpoint = url.parse(subscription.managementEndpointUrl);
  var service = factoryMethod(subscription.id, {
    keyvalue: subscription.managementCertificate.key,
    certvalue: subscription.managementCertificate.cert,
  },
    {
      host: managementEndpoint.hostname,
      port: managementEndpoint.port,
      serializetype: 'XML'
    }).withFilter(new RequestLogFilter(log));
  return service;
}

exports.createWebsiteManagementService = function (subscription) {
  var factoryMethod = require('azure-asm-website').createWebsiteManagementService;
  return createService(factoryMethod, subscription);
};

// TODO: workaround for release 0.7.4. Remove in vnext and fix underlying issue in SDK.
function createPostBodyFilter() {
  return function handle(resource, next, callback) {
    if ((resource.method === 'POST' || resource.method === 'PUT' || resource.method === 'PATCH') && !resource.body) {
      resource.body = '';
    }

    var stream = next(resource, callback);
    stream.on('error', function () {
    });
    return stream;
  };
}

exports.createPostBodyFilter = createPostBodyFilter;

function certAuthFilter(credentials) {
  return function handle(resource, next, callback) {
    if (credentials && credentials.credentials && credentials.credentials.key && credentials.credentials.cert) {
      resource.key = credentials.credentials.key;
      resource.cert = credentials.credentials.cert;
    }
    return next(resource, callback);
  };
}

exports.certAuthFilter = certAuthFilter;

function createFollowRedirectFilter() {
  return function handle(resource, next, callback) {
    function handleRedirect(err, response, body) {
      if (response &&
        response.headers.location &&
        response.statusCode >= 300 &&
        response.statusCode < 400) {

        resource.url = response.headers.location;
        next(resource, handleRedirect);
      } else if (callback) {
        callback(err, response, body);
      }
    }

    return next(resource, handleRedirect);
  };
}

exports.createFollowRedirectFilter = createFollowRedirectFilter;

polishErrorCausedByArmProviderNotRegistered = function () {
  return function handle(resource, next, callback) {
    var nextStream = next(resource, function (err, response, body) {
      if (err && err.message && response && response.statusCode === 409) {
        var re = /.*registered to use namespace \'(.+)\'/i;
        var found = re.exec(err.message);
        var providerName = (found && found.length === 2) ? found[1] : null;
        if (providerName) {
          err.message = util.format('The subscription must be registered to use namespace "%s". ' +
                                    'This can be achieved by executing the command: "azure provider ' +
                                    'register %s".', providerName, providerName);
        }
      }
      callback(err, response, body);
    });
    return nextStream;
  };
};

exports.createScmManagementService = function (repository, auth) {
  var authentication = auth.split(':');
  var repositoryUrl = url.parse(repository);
  var azureWebSite = require('azure-asm-website');

  var service = azureWebSite.createScmService({
    user: authentication[0],
    pass: authentication[1]
  }, {
    host: repositoryUrl.hostname,
    port: repositoryUrl.port
  });

  service.userAgent = getUserAgent();

  return service;
};

exports.createBlobService = function () {
  var storage = require('azure-storage');
  var blobService = storage.createBlobService.apply(this, arguments);
  blobService.userAgent = getUserAgent();
  return blobService;
};

exports.createSqlService = function () {
  var azureSqlMgmt = require('azure-asm-sql');
  var sqlService = azureSqlMgmt.createSqlService.apply(this, arguments);
  sqlService.userAgent = getUserAgent();
  return sqlService;
};

exports.getLocaleString = function (string) {
  var result = locale[string];
  if (!result) {
    if (process.env.AZURE_DEBUG_LABELS) {
      throw new Error(util.format('Invalid resource %s', string));
    } else {
      return string;
    }
  }

  return result;
};

function RequestLogFilter(logger) {
  this.logger = logger;
}

RequestLogFilter.prototype.handle = function (requestOptions, next) {
  var self = this;

  this.logger.silly('requestOptions');
  this.logger.json('silly', requestOptions);
  if (next) {
    next(requestOptions, function (returnObject, finalCallback, nextPostCallback) {
      self.logger.silly('returnObject');
      self.logger.json('silly', returnObject);

      if (nextPostCallback) {
        nextPostCallback(returnObject);
      } else if (finalCallback) {
        finalCallback(returnObject);
      }
    });
  }
};

exports.RequestLogFilter = RequestLogFilter;

exports.isSha1Hash = function (str) {
  return (/\b([a-fA-F0-9]{40})\b/).test(str);
};

exports.webspaceFromName = function (name) {
  return (name.replace(/ /g, '').toLowerCase() + 'webspace');
};

exports.getCertFingerprint = function (pem) {
  var certBase64 = exports.extractBase64CertFromPEM(pem);
  // Calculate sha1 hash of the cert
  var cert = new Buffer(certBase64, 'base64');
  var sha1 = crypto.createHash('sha1');
  sha1.update(cert);
  return sha1.digest('hex');
};

exports.isPemCert = function (data) {
  return data.indexOf(BEGIN_CERT) !== -1 && data.indexOf(END_CERT) !== -1;
};

exports.extractBase64CertFromPEM = function (pem) {
  // Extract the base64 encoded cert out of pem file
  var beginCert = pem.indexOf(BEGIN_CERT) + BEGIN_CERT.length;
  if (pem[beginCert] === '\n') {
    beginCert = beginCert + 1;
  } else if (pem[beginCert] === '\r' && pem[beginCert + 1] === '\n') {
    beginCert = beginCert + 2;
  }

  var endCert = '\n' + pem.indexOf(END_CERT);
  if (endCert === -1) {
    endCert = '\r\n' + pem.indexOf(END_CERT);
  }

  return pem.substring(beginCert, endCert);
};

exports.getOrCreateBlobStorage = function (cli, storageClient, location, affinityGroup, name, storagePrefix, callback) {
  var progress;

  /*jshint camelcase:false*/
  function callback_(error, blobStorageUrl) {
    progress.end();
    callback(error, blobStorageUrl);
  }

  function createNewStorageAccount_() {
    if (exports.stringIsNullOrEmpty(storagePrefix)) {
      storagePrefix = name;
    }
    var storageAccountName = blobUtils.normalizeServiceName(storagePrefix + (new Date()).getTime().toString());
    cli.output.verbose('Creating a new storage account \'' + storageAccountName + '\'');
    var storageOptions = {
      name: storageAccountName,
      label: storageAccountName,
      geoReplicationEnabled: false,
      accountType: 'Standard_LRS'
    };

    if (affinityGroup) {
      storageOptions.affinityGroup = affinityGroup;
    } else if (location) {
      storageOptions.location = location;
    } else {
      throw new Error('location or affinityGroup must be specified');
    }

    progress = cli.interaction.progress('Creating a new storage account \'' + storageAccountName + '\'');
    storageClient.storageAccounts.create(storageOptions, function (error) {
      if (error) {
        callback_(error);
      } else {
        cli.output.verbose('Storage account successfully created');
        cli.output.verbose('Getting properties for \'' + storageAccountName + '\' storage account');

        storageClient.storageAccounts.get(storageAccountName, function (error, response) {
          if (error) {
            callback_(error);
          } else {
            var storageAccount = response.storageAccount;
            if (storageAccount) {
              var blobStorageUrl = storageAccount.properties.endpoints[0];
              if (blobStorageUrl.slice(-1) === '/') {
                blobStorageUrl = blobStorageUrl.slice(0, -1);
              }

              callback_(null, blobStorageUrl);
            } else {
              callback_(new Error('No storage account found'));
            }
          }
        });
      }
    });
  }

  progress = cli.interaction.progress('Retrieving storage accounts');
  cli.output.verbose('Getting list of available storage accounts');
  storageClient.storageAccounts.list(function (error, response) {
    if (error) {
      callback_(error);
    } else {
      var storageAccounts = response.storageAccounts;
      for (var i = 0; i < storageAccounts.length; i++) {
        if ((location && storageAccounts[i].properties.location && storageAccounts[i].properties.location.toLowerCase() === location.toLowerCase()) ||
          affinityGroup && storageAccounts[i].properties.affinityGroup && storageAccounts[i].properties.affinityGroup.toLowerCase() === affinityGroup.toLowerCase()) {
          if (!exports.stringIsNullOrEmpty(storagePrefix)) {
            if (!exports.stringStartsWith(storageAccounts[i].name, storagePrefix)) {
              continue;
            }
          }
          var blobStorageUrl = storageAccounts[i].properties.endpoints[0];
          if (blobStorageUrl.slice(-1) === '/') {
            blobStorageUrl = blobStorageUrl.slice(0, -1);
          }

          callback_(null, blobStorageUrl);
          return;
        }
      }
      createNewStorageAccount_();
    }
  });
};

exports.writeFileSyncMode = function writeFileSyncMode(path, data, encoding, mode) {
  mode = mode || parseInt('600', 8); // maximum protection by default
  var fd = fs.openSync(path, 'w', mode);
  try {
    if (typeof data === 'string') {
      fs.writeSync(fd, data, 0, encoding);
    } else {
      fs.writeSync(fd, data, 0, data.length, 0);
    }
  } finally {
    fs.closeSync(fd);
  }
};

exports.getDnsPrefix = function (dnsName, allowEmpty) {
  if (dnsName) {
    // remove protocol if any, take the last element
    dnsName = dnsName.split('://').slice(-1)[0];
    // take first element
    dnsName = dnsName.split('.', 1)[0];
  }
  if (!dnsName && !allowEmpty) {
    throw new Error('Missing or invalid dns-name');
  }
  return dnsName;
};

/**
 * Resolve location name if 'name' is location display name.
 *
 * @param {string}   name       The display name or location name. Required
 * @param {function} callback   The callback function called on completion. Required.
 */
exports.resolveLocationName = function (managementClient, name, callback) {
  managementClient.locations.list(function (error, response) {
    var resolvedLocation = null;
    if (!error) {
      if (response.locations.length > 0) {
        for (var i = 0; i < response.locations.length; i++) {
          var locationInfo = response.locations[i];
          if (exports.ignoreCaseEquals(locationInfo.name, name)) {
            callback(null, locationInfo);
            return;
          } else if (!resolvedLocation && (exports.ignoreCaseEquals(locationInfo.DisplayName, name))) {
            // This is the first matched display name save the corresponding location
            // We ignore further matched display name, but will continue with location
            // matching
            resolvedLocation = locationInfo;
          }
        }

        if (resolvedLocation) {
          callback(null, resolvedLocation);
        } else {
          callback({
            message: 'No location found which has DisplayName or Name same as value of --location',
            code: 'Not Found'
          }, name);
        }
      } else {
        // Return a valid error
        callback({ message: 'Server returns empty location list', code: 'Not Found' }, name);
      }
    } else {
      callback(error, null);
    }
  });
};

exports.parseInt = function (value) {
  var intValue = parseInt(value, 10);
  if (intValue != value || value >= 65536 * 65536) { // just some limits
    return NaN;
  }
  return intValue;
};

exports.parseBool = function (value, paramName) {
  if (!this.ignoreCaseEquals(value, 'true') && !this.ignoreCaseEquals(value, 'false')) {
    throw new Error(util.format('%s parameter must be true or false', paramName));
  }
  return value.toLowerCase() === 'true';
};

/**
 * Creates a anonymous function that validator if the given string is a valid datetime.
 *
 * @param {string} stringDateTime The datetime string.
 * @return {Date}
 */
exports.parseDateTime = function (stringDateTime) {
  try {
    return new Date(stringDateTime);
  } catch (e) {
    throw new Error($('The date format is incorrect'));
  }
};

exports.getUTCTimeStamp = function () {
  var now = new Date();
  return (now.getUTCFullYear() + '-' +
  ('0' + (now.getUTCMonth() + 1)).slice(-2) + '-' +
  ('0' + now.getUTCDate()).slice(-2) + ' ' +
  ('0' + now.getUTCHours()).slice(-2) + ':' +
  ('0' + now.getUTCMinutes()).slice(-2));
};

exports.logLineFormat = function logLineFormat(object, logFunc, prefix) {
  prefix = prefix || '';
  switch (typeof object) {
    case 'object':
      // if this is a date then we call toISOString and print that
      if (_.isDate(object)) {
        logFunc(prefix.cyan + object.toISOString().green);
      } else {
        for (var i in object) {
          logLineFormat(object[i], logFunc, prefix + i + ' ');
        }
      }
      return;
    case 'string':
      logFunc(prefix.cyan + ('"' + object + '"').green);
      return;
    case 'boolean':
      logFunc(prefix.cyan + object.toString().green);
      return;
    case 'number':
      logFunc(prefix.cyan + object.toString().green);
      return;
    case 'undefined':
      return;
    default:
      logFunc(prefix.cyan + '?' + object + '?'); // unknown type
  }
};

exports.validateEndpoint = function (endpoint) {
  if (!exports.stringStartsWith(endpoint, 'http://') && !exports.stringStartsWith(endpoint, 'https://')) {
    // Default to https
    endpoint = 'https://' + endpoint;
  }

  var parts = url.parse(endpoint);
  if (!parts.hostname) {
    throw new Error('Invalid endpoint format.');
  }

  parts.port = (parts.port && parseInt(parts.port, 10)) || (/https/i.test(parts.protocol) ?
    constants.DEFAULT_HTTPS_PORT :
    constants.DEFAULT_HTTP_PORT);

  return url.format(parts);
};

/**
 * Determines if a string is null or empty.
 *
 * @param {string}       text      The string to test.
 * @return {Bool} True if the string string is null or empty; false otherwise.
 */
exports.stringIsNullOrEmpty = function (text) {
  return text === null ||
    text === undefined ||
    text.trim() === '';
};


exports.stripBOM = function (content) {
  if (Buffer.isBuffer(content)) {
    content = content.toString();
  }
  if (content.charCodeAt(0) === 0xFEFF) {
    content = content.slice(1);
  }
  return content;
};

/**
 * Determines if a string ends with another.
 *
 * @param {string}       text      The string to assert.
 * @param {string}       suffix    The string suffix.
 * @param {bool}         ignoreCase Boolean value indicating if casing should be ignored.
 * @return {Bool} True if the string ends with the suffix; false otherwise.
 */
exports.stringEndsWith = function (text, suffix, ignoreCase) {
  if (_.isNull(suffix)) {
    return true;
  }

  if (ignoreCase) {
    text = text.toLowerCase();
    suffix = suffix.toLowerCase();
  }

  return text.substr(text.length - suffix.length) === suffix;
};

exports.stringTrimEnd = function (text, charToTrim) {
  if (!text) {
    return text;
  }

  if (!charToTrim) {
    charToTrim = ' ';
  }

  var subtract = 0;
  while (subtract < text.length && text[text.length - (subtract + 1)] === charToTrim) {
    subtract++;
  }

  return text.substr(0, text.length - subtract);
};


exports.logError = function (log, message, err) {
  if (arguments.length == 1) {
    err = message;
    message = undefined;
  } else {
    log.error(message);
  }

  if (err) {
    if (err.message) {
      //                log.error(err.message);
      log.verbose('stack', err.stack);
      log.json('silly', err);
    }
    else if (err.Message) {
      //                log.error(err.Message);
      log.json('verbose', err);
    }
  }
};

exports.clearConfig = function () {
  var azureConfigPath = path.join(exports.azureDir(), 'config.json');

  if (exports.pathExistsSync(azureConfigPath)) {
    fs.unlinkSync(azureConfigPath);
    return true;
  }
};

exports.copyIisNodeWhenServerJsPresent = function (log, rootPath, callback) {
  try {
    var iisnodeyml = 'iisnode.yml';
    log.silly('copyWebConfigWhenServerJsPresent');
    if (!exports.pathExistsSync(iisnodeyml) && (exports.pathExistsSync(path.join(rootPath, 'server.js')) || exports.pathExistsSync(path.join(rootPath, 'app.js')))) {
      log.info('Creating default ' + iisnodeyml + ' file');
      var sourcePath = path.join(__dirname, '../templates/node/' + iisnodeyml);
      fs.readFile(sourcePath, function (err, result) {
        if (err) {
          callback(err);
          return;
        }

        fs.writeFile(path.join(rootPath, iisnodeyml), result, callback);
      });
    }
    else {
      callback();
    }
  }
  catch (e) {
    callback(e);
  }
};

exports.normalizeParameters = function (paramDescription) {
  var key, positionalValue, optionValue;
  var paramNames = Object.keys(paramDescription);
  var finalValues = {};

  for (var i = 0; i < paramNames.length; ++i) {
    key = paramNames[i];
    positionalValue = paramDescription[key][0];
    optionValue = paramDescription[key][1];
    if (!_.isUndefined(positionalValue) && !_.isUndefined(optionValue)) {
      return { err: new Error('You must specify ' + key + ' either positionally or by name, but not both') };
    } else {
      finalValues[key] = positionalValue || optionValue;
    }
  }

  return { values: finalValues };
};

/**
 * fs.exists wrapper for streamline
 */
exports.fileExists = function (filePath, cb) {
  var func = fs.exists;
  if (!func) {
    func = path.exists;
  }
  func(filePath, function (exists) {
    cb(null, exists);
  });
};

/**
 * Wildcard Util only support two wildcard character * and ?
 */
exports.Wildcard = {
  /**
   * does the specified the character contain wildcards
   */
  containWildcards: function (str) {
    var wildcardReg = /[*?]/img;
    return str !== null && wildcardReg.test(str);
  },

  /**
   * Get the max prefix string of the specified string which doesn't contain wildcard
   */
  getNonWildcardPrefix: function (str) {
    var nonWildcardReg = /[^*?]*/img;
    var prefix = '';

    if (str !== null) {
      var result = str.match(nonWildcardReg);
      if (result !== null && result.length > 0) {
        prefix = result[0];
      }
    }

    return prefix;
  },

  /**
   * Convert wildcard pattern to regular expression
   */
  wildcardToRegexp: function (str, isCaseInsensitive) {
    var strRegexp = '';
    if (str !== null) {
      strRegexp = str.replace(/\?/g, '.').replace(/\*/g, '.*');
    }

    var regexp = new RegExp();
    if (isCaseInsensitive && isCaseInsensitive === true) {
      regexp.compile('^' + strRegexp + '$', 'ig');
    }
    else {
      regexp.compile('^' + strRegexp + '$');
    }
    return regexp;
  },

  /**
   * Is the specified string match the case sensitive specified wildcard pattern
   */
  isMatch: function (str, pattern) {
    var reg = exports.Wildcard.wildcardToRegexp(pattern);
    return reg.test(str);
  },
  
  /**
   * Is the specified string match the case insensitive specified wildcard pattern
   */
  isMatchCaseInsensitive: function (str, pattern) {
    var reg = exports.Wildcard.wildcardToRegexp(pattern, true);
    return reg.test(str);
  }
};

/**
 * Invalid file name chars in windows.
 * http://msdn.microsoft.com/en-us/library/system.io.path.getinvalidfilenamechars.aspx
 */
exports.invalidFileNameChars = [34, 60, 62, 124, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 58, 42, 63, 92, 47];

/**
 * Reserved file name in windows
 */
exports.reservedBaseFileNamesInWindows = ['con', 'prn', 'aux', 'nul', 'com1', 'com2', 'com3', 'com4', 'com5', 'com6', 'com7', 'com8', 'com9', 'lpt1', 'lpt2', 'lpt3', 'lpt4', 'lpt5', 'lpt6', 'lpt7', 'lpt8', 'lpt9'];

/**
 * Is the reserved file name in windows
 */
exports.isReservedFileNameInWindows = function (name) {
  name = (name || '').toLowerCase();
  var index = exports.reservedBaseFileNamesInWindows.indexOf(name);
  return index !== -1;
};

/*
 * Escape file path
 */
exports.escapeFilePath = function (name) {
  if (exports.isWindows()) {
    //only escape file name on windows
    var regExp = exports.getReplaceRegExpFromCharCode(exports.invalidFileNameChars);
    name = name.replace(regExp, function (code) {
      return '%' + code.charCodeAt(0).toString(16);
    });
    var extName = path.extname(name);
    var baseName = path.basename(name, extName);
    if (exports.isReservedFileNameInWindows(baseName)) {
      name = util.format('%s (1)%s', baseName, extName);
    }
  }
  return name;
};

/**
 * Is windows platform
 */
exports.isWindows = function () {
  return !!process.platform.match(/^win/);
};

/**
 * Join the char code into a replace regular expression
 * For example,
 *   [65,66] => /A|B/img
 *   [63,66] => /\?|B/img
 */
exports.getReplaceRegExpFromCharCode = function (charCodeArray) {
  function charCodeToRegChar(charCode) {
    var str = String.fromCharCode(charCode);
    switch (str) {
      case '*':
      case '?':
      case '.':
      case '\\':
      case '|':
      case '/':
        str = '\\' + str;
        break;
    }
    return str;
  }

  var regExp = new RegExp();
  if (charCodeArray.length) {
    var regStr = charCodeToRegChar(charCodeArray[0]);
    for (var i = 1; i < charCodeArray.length; i++) {
      regStr += '|' + charCodeToRegChar(charCodeArray[i]);
    }
    regExp.compile(regStr, 'gim');
  }
  return regExp;
};

/**
 * Add function overloads to an object that vary by
 * declared argument length.
 *
 * @param {function} func the function overloads.
 *
 * @returns The 'overloaded' function
 */
exports.overload = function () {

  function final() {
    throw new Error(util.format($('Unknown overload for %s parameters'), arguments.length));
  }

  var func = final;
  /* jshint loopfunc: true */
  for (var i = 0; i < arguments.length; ++i) {
    func = (function (old, func) {
      return function () {
        if (func.length === arguments.length) {
          return func.apply(this, arguments);
        } else if (typeof old === 'function') {
          return old.apply(this, arguments);
        }
      };
    })(func, arguments[i]);
  }

  return func;
};

//"<root>\test\framework\cli-test.js" contains associated test stubs. Please keep them in sync.
exports.uuidGen = function () {
  return uuid.v4();
};

exports.toLowerCaseAndRemoveSpace = function (str) {
  if (!str) {
    return str;
  }

  return str.replace(/ /gi, '').toLowerCase();
};

exports.ignoreCaseAndSpaceEquals = function (a, b) {
  return a === b || (exports.toLowerCaseAndRemoveSpace(a) === exports.toLowerCaseAndRemoveSpace(b));
};

exports.atLeastOneParameIsSet = function (params) {
  for (var i = 0; i < params.length; i++) {
    if (!exports.stringIsNullOrEmpty(params[i])) {
      return true;
    }
  }
  return false;
};

exports.allParamsAreSet = function (params) {
  for (var i = 0; i < params.length; i++) {
    if (exports.stringIsNullOrEmpty(params[i])) {
      return false;
    }
  }

  return true;
};

// Many of the commands accepts arguments whose valid values can be from an enum. For example
// protocol argument for endpoints create, valid values are ['tcp', 'udp', 'http'].
// This method can be used to validate user provided input against the possible valid
// values (supportedTypes). This method do case ignore comparison and return matched enum
// value. If no match found exception will be thrown and exception message will inform valid
// values.
exports.verifyParamExistsInCollection = function (supportedTypes, paramToCheck, paramName) {
  var i = _.indexOf(_.map(supportedTypes, function (s) {
    return s.toLowerCase();
  }), paramToCheck.toLowerCase());
  if (i === -1) {
    throw new Error(util.format(exports.getLocaleString('Given %s "%s" is invalid, supported values are: %s'), paramName, paramToCheck, supportedTypes.join(', ')));
  }

  return supportedTypes[i];
};

exports.hasValidProperty = function (object, propName) {
  if (object === null || object === undefined) {
    return false;
  }

  if (!object.hasOwnProperty(propName)) {
    return false;
  }

  if (object[propName] === null || object[propName] === undefined) {
    return false;
  }

  return true;
};

exports.parseResourceReferenceUri = function (referenceUri) {
  var parts = referenceUri.split('/');
  return {
    subscriptionId: parts[2],
    resourceGroupName: parts[4],
    provider: parts[6], // e.g. Microsoft.Network
    parentResource: parts.slice(7, parts.length - 1).join('/'), // e.g. virtualNetworks/<vnet-name>/subnets
    resourceName: parts[parts.length - 1] // e.g. <subnet-name>
  };
};

exports.isAttributesMatched = function (item, attributes) {
  for (var key in attributes) {
    var value1 = attributes[key];
    var value2 = item[key];
    if (typeof value1 === 'string' && typeof value2 === 'string') {
      value1 = value1.toLowerCase();
      value2 = value2.toLowerCase();
    }
    if (value1 !== value2) return false;
  }

  return true;
};

/**
 * Looks through the list and returns the element that matches attributes,
 * case-insensitive.
 */
exports.findFirstCaseIgnore = function (list, attributes) {
  if (_.isEmpty(attributes)) {
    return undefined;
  }
  return _.find(list, function (item) {
    return exports.isAttributesMatched(item, attributes);
  });
};

/**
 * Looks through the list and returns the index of element that matches attributes or -1 if it is not present,
 * case-insensitive.
 */
exports.indexOfCaseIgnore = function (list, attributes) {
  if (_.isEmpty(list) || _.isEmpty(attributes)) {
    return -1;
  }
  for (var i = 0; i < list.length; i++) {
    var item = list[i];
    var match = exports.isAttributesMatched(item, attributes);
    if (match) return i;
  }
  return -1;
};

exports.argHasValue = function (argument) {
  return (argument !== true && argument !== '\'\'');
};

exports.trimTrailingChar = function (str, charToTrim) {
  while (str.length > 0 && str.charAt(str.length - 1) == charToTrim) {
    str = str.substr(0, str.length - 1);
  }
  return str;
};

exports.appendArray = function (array, otherArray) {
  for (var i = 0; i < otherArray.length; i++) {
    array.push(otherArray[i]);
  }
};

exports.capitalizeFirstLetter = function (str) {
  return str.charAt(0).toUpperCase() + str.slice(1);
};

exports.toRange = function (array) {
  return '[' + array[0] + '-' + array[1] + ']';
};

exports.setIndent = function(num) {
  if (num > 0) {
    return new Array(num + 1).join(' ');
  }
  return '';
};

exports.getHash = function (name) {
  // Define Utility Functions - Reference: http://www.cse.yorku.ca/~oz/hash.html
  var hash = 5381;
  var seedstr = name;
  for (var i = 0; i < seedstr.length; i++) {
    var c = parseInt(seedstr.charCodeAt(i));
    hash = ((((hash << 5) + hash) + c) % Number.MAX_VALUE) >>> 0;
  }
  return hash;
};

exports.vmImageAliasUrl = 'https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/arm-compute/quickstart-templates/aliases.json';

exports.getImageAliasFilePath = function() {
  return __dirname + '/aliases/image.json';
};

exports.getImageAliasFileContent = function() {
    var content = fs.readFileSync(exports.getImageAliasFilePath(), 'utf8');
    content = content.replace(/^\uFEFF/, '');
    return content;
};

exports.parseImageAliasDict = function(aliasObj) {
  var aliasDict = {};
  var key = null;
  for (key in aliasObj.outputs.aliases.value.Windows) {
    aliasDict[key] = aliasObj.outputs.aliases.value.Windows[key];
  }
  for (key in aliasObj.outputs.aliases.value.Linux) {
    aliasDict[key] = aliasObj.outputs.aliases.value.Linux[key];
  }
  return aliasDict;
};

exports.getImageAliasUrn = function(alias, aliasObj) {
  if (!aliasObj) {
    var content = exports.getImageAliasFileContent();
    aliasObj = JSON.parse(content);
  }
  var aliasArr = exports.parseImageAliasDict(aliasObj);
  for (var i in aliasArr) {
    if (i.toLowerCase() === alias.toLowerCase()) {
      return [aliasArr[i].publisher, aliasArr[i].offer, aliasArr[i].sku, aliasArr[i].version].join(':');
    }
  }
  return null;
};

exports.getImageAliasList = function() {
  var content = null;
  if (fs.existsSync(exports.getImageAliasFilePath())) {
    content = exports.getImageAliasFileContent();
  }
  else {
    var request = require('sync-request');
    var response = request('GET', exports.vmImageAliasUrl);
    content = response.getBody();
    exports.writeFileSyncMode(exports.getImageAliasFilePath(), content, 'utf-8');
  }
  var aliasObj = JSON.parse(content);
  var list = [];
  var aliasArr = exports.parseImageAliasDict(aliasObj);
  for (var i in aliasArr) {
    list.push(i + ' = ' + exports.getImageAliasUrn(i, aliasObj));
  }
  return list;
};

exports.takeDefault = function(output, defValue, paramName) {
  output.warn(util.format(('Using default %s %s'), paramName, defValue));
  return defValue;
};

exports.getPublicIp = function(vm) {
  var publicIPAddress;
  if(!vm.networkProfile.networkInterfaces[0].expanded.ipConfigurations[0].publicIPAddress) {
    publicIPAddress = 'N/A';
  }
  else {
    publicIPAddress = vm.networkProfile.networkInterfaces[0].expanded.ipConfigurations[0].publicIPAddress.expanded.ipAddress;
  }
  return publicIPAddress;
};


/**
 * trunc
 *
 * truncates the string with '...' endings
 * @returns {string}
 */
function trunc () {
  var maxLen = arguments[0];
  return (this.length > maxLen) ? this.substr(0,maxLen-1)+'...' : this;
}
String.prototype.trunc = trunc;