// 
// 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 _ = require('underscore');
var util = require('util');

var constants = require('./../util/constants');
var blobConstants = constants.BlobConstants;
var BlobUtilities = require('./../../services/blob/blobutilities');
var FileUtilities = require('./../../services/file/fileutilities');
var azureutil = require('./util');
var SR = require('./sr');
var check = require('validator');
var errors = require('../errors/errors');
var ArgumentError = errors.ArgumentError;
var ArgumentNullError = errors.ArgumentNullError;

exports = module.exports;

function initCallback(callbackParam, resultsCb) {
  var fail;
  if (callbackParam) {
    fail = function (err) {
      callbackParam(err);
      return false;
    };
  } else {
    fail = function (err) {
      throw err;
    };
    callbackParam = function () {};
  }

  resultsCb(fail, callbackParam);
}

/**
* Checks if the given value is a valid enumeration or not.
*
* @param {object} value     The value to validate.
* @param {object} list      The enumeration values.
* @return {boolean}
*/
exports.isValidEnumValue = function (value, list, callback) {
  var fail;
  
  initCallback(callback, function (f, cb) {
    fail = f;
    callback = cb;
  });

  if (!list.some(function (current) {
    return current.toLowerCase() === value.toLowerCase();
  })) {
    return fail(new RangeError(util.format('Invalid value: %s. Options are: %s.', value, list)));
  }

  callback();
  return true;
};

/**
* Creates a anonymous function that check if the given uri is valid or not.
*
* @param {string} uri     The uri to validate.
* @return {boolean}
*/
exports.isValidUri = function (uri) {
  if (!check.isURL(uri, { 'require_tld': false })){
    throw new URIError('The provided URI "' + uri + '" is invalid.');
  }
  return true;
};

/**
* Checks if the given host is valid or not.
*
* @param {string|object} host     The host to validate.
* @return {boolean}
*/
exports.isValidHost= function (host) {
  if (azureutil.objectIsNull(host)) {
    throw new ArgumentNullError('host', SR.STORAGE_HOST_LOCATION_REQUIRED);
  } else {
    var storageHost = {};
    storageHost.primaryHost = _.isString(host) ? host : host.primaryHost;
    if (storageHost.primaryHost && !check.isURL(storageHost.primaryHost, { 'require_tld': false })){
      throw new URIError('The provided URI "' + storageHost.primaryHost + '" is invalid.');
    }

    storageHost.secondaryHost = _.isString(host) ? undefined : host.secondaryHost;
    if (storageHost.secondaryHost && !check.isURL(storageHost.secondaryHost, { 'require_tld': false })){
      throw new URIError('The provided URI "' + storageHost.secondaryHost + '" is invalid.');
    }

    if (!storageHost.primaryHost && !storageHost.secondaryHost) {
      throw new ArgumentNullError('host', SR.STORAGE_HOST_LOCATION_REQUIRED);
    }
  } 

  return true;
};

/**
* Checks if the given value is a valid UUID or not.
*
* @param {string|object} uuid     The uuid to validate.
* @return {boolean}
*/
exports.isValidUuid = function(uuid, callback) {
  var validUuidRegex = /^[a-zA-Z0-9]{8}\-[a-zA-Z0-9]{4}\-[a-zA-Z0-9]{4}\-[a-zA-Z0-9]{4}\-[a-zA-Z0-9]{12}$/;

  var fail;

  initCallback(callback, function (f, cb) {
    fail = f;
    callback = cb;
  });

  if (!validUuidRegex.test(uuid)) {
    return fail(new SyntaxError('The value is not a valid UUID format.'));
  }
  
  callback();
  return true;
};

/**
* Creates a anonymous function that check if a given key is base 64 encoded.
*
* @param {string} key The key to validate.
* @return {function}
*/
exports.isBase64Encoded = function (key) {
  var isValidBase64String = key.match(/^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$/);

  if (isValidBase64String) {
    return true;
  } else {
    throw new SyntaxError('The provided account key ' + key + ' is not a valid base64 string.');
  }
};

/**
* Validates a function.
*
* @param {object} function The function to validate.
* @return {function}
*/
exports.isValidFunction = function (functionObject, functionName) {
  if (!functionObject) {
    throw new ArgumentNullError('functionObject', functionName + ' must be specified.');
  }
  if(!_.isFunction(functionObject)){
    throw new TypeError(functionName + ' specified should be a function.');
  }
  return true;
};

var getNameError = function(name, typeName) {
  // checks if name is null, undefined or empty
  if (azureutil.stringIsEmpty(name)) {
    return new ArgumentNullError('name', util.format('%s name must be a non empty string.', typeName));
  }

  // check if name is between 3 and 63 characters
  if (name.length < 3 || name.length > 63) {
    return new ArgumentError('name', util.format('%s name must be between 3 and 63 characters long.', typeName));
  }

  // check if name follows naming rules
  if (name.match(/^([a-z0-9]+(-[a-z0-9]+)*)$/) === null) {
    return new SyntaxError(util.format('%s name format is incorrect.', typeName));
  }

  return null;
};

/**
* Validates a container name.
*
* @param {string} containerName The container name.
*/
exports.containerNameIsValid = function (containerName, callback) {
  var fail;
  initCallback(callback, function (f, cb) {
    fail = f;
    callback = cb;
  });

  var nameError = getNameError(containerName, 'Container');

  if (!nameError || containerName.match(/^(\$root|\$logs|\$web)$/)) {
    callback();
    return true;
  } else {
    return fail(nameError);
  }
};

/**
* Validates a blob name.
*
* @param {string} containerName The container name.
* @param {string} blobname      The blob name.
*/
exports.blobNameIsValid = function (containerName, blobName, callback) {
  var fail;
  
  initCallback(callback, function (f, cb) {
    fail = f;
    callback = cb;
  });
  
  if (!blobName) {
    return fail(new ArgumentNullError('blobName', 'Blob name is not specified.'));
  }
  
  if (containerName === '$root' && blobName.indexOf('/') !== -1) {
    return fail(new SyntaxError('Blob name format is incorrect.'));
  }
  
  callback();
  return true;
};

/**
* Validates a blob tier name.
*
* @param {string} blobTier The blob tier name.
*/
exports.blobTierNameIsValid = function (blobTier, callback) {
  var fail;
  
  initCallback(callback, function (f, cb) {
    fail = f;
    callback = cb;
  });
  
  if (!blobTier) {
    return fail(new ArgumentNullError('blobTier', 'Blob tier is not specified.'));
  }

  if (!_.chain(_.union(
    _.values(BlobUtilities.BlobTier.PremiumPageBlobTier),
    _.values(BlobUtilities.BlobTier.StandardBlobTier)
  ))
    .map(function (val) { return val.toString().toUpperCase(); })
    .contains(blobTier.toString().toUpperCase())
    .value()) {
    return fail(new SyntaxError('Blob tier is incorrect. Refer to BlobUtilities.BlobTier for possible values.'));
  }

  callback();
  return true;
};

/**
* Validates a share name.
*
* @param {string} shareName The share name.
*/
exports.shareNameIsValid = function (shareName, callback) {
  var fail;
  initCallback(callback, function (f, cb) {
    fail = f;
    callback = cb;
  });

  var nameError = getNameError(shareName, 'Share');

  if (!nameError) {
    callback();
    return true;
  } else {
    return fail(nameError);
  }
};

/**
* Validates a queue name.
*
* @param {string} queueName The queue name.
*/
exports.queueNameIsValid = function (queueName, callback) {
  var fail;

  initCallback(callback, function (f, cb) {
    fail = f;
    callback = cb;
  });

  var nameError = getNameError(queueName, 'Queue');

  if (!nameError) {
    callback();
    return true;
  } else {
    return fail(nameError);
  }
};

/**
* Validates a table name.
*
* @param {string} table  The table name.
*/
exports.tableNameIsValid = function (table, callback) {
  var fail;

  initCallback(callback, function (f, cb) {
    fail = f;
    callback = cb;
  });

  if (azureutil.stringIsEmpty(table)) {
    return fail(new ArgumentNullError('table', 'Table name must be a non empty string.'));
  }

  if (table.length < 3 || table.length > 63) {
    return fail(new ArgumentError('table', 'Table name must be between 3 and 63 characters long.'));
  }

  if(table.toLowerCase() === 'tables') {
    return fail(new RangeError('Table name cannot be \'Tables\'.'));
  }

  if (table.match(/^([A-Za-z][A-Za-z0-9]{2,62})$/) !== null || table === '$MetricsCapacityBlob' || table.match(/^(\$Metrics(HourPrimary|MinutePrimary|HourSecondary|MinuteSecondary)?(Transactions)(Blob|Queue|Table|File))$/) !== null)
  {
    callback();
    return true;
  } else {
    return fail(new SyntaxError('Table name format is incorrect.'));
  }
};

/**
* Validates an HTML File object.
*
* @param {File} browserFile  The HTML File object.
*/
exports.browserFileIsValid = function (browserFile, callback) {
  var fail;

  initCallback(callback, function (f, cb) {
    fail = f;
    callback = cb;
  });

  // IE doesn't support File.constructor.name
  if (!azureutil.isBrowser() ||
    !browserFile ||
    !browserFile.constructor ||
    (!azureutil.isIE() && !browserFile.constructor.name) ||
    (!azureutil.isIE() && browserFile.constructor.name !== 'File' && browserFile.constructor.name !== 'Blob') ||
    !azureutil.objectIsInt(browserFile.size)) {
    return fail(new ArgumentError('type', 'Invalid HTML File object.'));
  } else {
    callback();
    return true;
  }
};

/**
* Validates page ranges.
*
* @param {int} rangeStart             The range starting position.
* @param {int} rangeEnd               The range ending position.
* @param {int} writeBlockSizeInBytes  The block size.
*/
exports.pageRangesAreValid = function (rangeStart, rangeEnd, writeBlockSizeInBytes, callback) {
  var fail;

  initCallback(callback, function (f, cb) {
    fail = f;
    callback = cb;
  });

  if (rangeStart % 512 !== 0) {
    return fail(new RangeError('Start byte offset must be a multiple of 512.'));
  }

  var size = null;
  if (!azureutil.objectIsNull(rangeEnd)) {
    if ((rangeEnd + 1) % 512 !== 0) {
      return fail(new RangeError('End byte offset must be a multiple of 512 minus 1.'));
    }

    size = (rangeEnd - rangeStart) + 1;
    if (size > writeBlockSizeInBytes) {
      return fail(new RangeError('Page blob size cannot be larger than ' + writeBlockSizeInBytes + ' bytes.'));
    }
  }
  
  callback();
  return true;
};

/**
* Validates a blob type.
*
* @param {string} type  The type name.
*/
exports.blobTypeIsValid = function (type, callback) {
  var getEnumValues = function (obj) {
    var values = [];
    for (var prop in obj) {
      if (obj.hasOwnProperty(prop)) {
        values.push(obj[prop]);
      }
    }
    return values;
  };
  
  return this.isValidEnumValue(type, getEnumValues(blobConstants.BlobTypes), callback);
};

/**
* Validates share ACL type.
*
* @param {string} type  The type name.
*/
exports.shareACLIsValid = function (type, callback) {
  var fail;
  
  initCallback(callback, function (f, cb) {
    fail = f;
    callback = cb;
  });
  
  if (type != FileUtilities.SharePublicAccessType.OFF) {
    fail(new ArgumentError('type', 'The access type is not supported.'));
  }

  callback();
  return true;
};

/**
* Validates share quota value.
*
* @param {int} type  The quota value.
*/
exports.shareQuotaIsValid = function (quota, callback) {
  var fail;
  
  initCallback(callback, function (f, cb) {
    fail = f;
    callback = cb;
  });
  
  if (quota && quota <= 0) {
    fail(new RangeError('The share quota value, in GB, must be greater than 0.'));
  }
  
  callback();
  return true;
};

// common functions for validating arguments

function throwMissingArgument(name, func) {
  throw new ArgumentNullError(name, 'Required argument ' + name + ' for function ' + func + ' is not defined');
}

function ArgumentValidator(functionName) {
  this.func = functionName;
}

_.extend(ArgumentValidator.prototype, {
  string: function (val, name) {
    this.exists(val, name);
    if (typeof val !== 'string') {
      throw new TypeError('Parameter ' + name + ' for function ' + this.func + ' should be a non-empty string');
    }
  },

  stringAllowEmpty: function (val, name) {
    if (typeof val !== 'string') {
      throw new TypeError('Parameter ' + name + ' for function ' + this.func + ' should be a string');
    }
  },

  object: function (val, name) {
    this.exists(val, name);
    if (typeof val !== 'object') {
      throw new TypeError('Parameter ' + name + ' for function ' + this.func + ' should be an object');
    }
  },

  exists: function (val, name) {
    if (!val) {
      throwMissingArgument(name, this.func);
    }
  },

  function: function (val, name) {
    this.exists(val, name);
    if (typeof val !== 'function') {
      throw new TypeError('Parameter ' + name + ' for function ' + this.func + ' should be a function');
    }
  },

  value: function (val, name) {
    if (!val && val !== 0) {
      throwMissingArgument(name, this.func);
    }
  },

  nonEmptyArray: function (val, name) {
    if (!val || val.length === 0) {
      throw new TypeError('Required array argument ' + name + ' for function ' + this.func + ' is either not defined or empty');
    }
  },

  callback: function (val) {
    this.exists(val, 'callback');
    this.function(val, 'callback');
  },

  test: function (predicate, message) {
    if (!predicate()) {
      throw new Error(message + ' in function ' + this.func);
    }
  },

  tableNameIsValid: exports.tableNameIsValid,
  browserFileIsValid: exports.browserFileIsValid,
  containerNameIsValid: exports.containerNameIsValid,
  shareNameIsValid: exports.shareNameIsValid,
  blobNameIsValid: exports.blobNameIsValid,
  blobTierNameIsValid: exports.blobTierNameIsValid,
  pageRangesAreValid: exports.pageRangesAreValid,
  queueNameIsValid: exports.queueNameIsValid,
  blobTypeIsValid: exports.blobTypeIsValid,
  shareACLIsValid: exports.shareACLIsValid,
  shareQuotaIsValid: exports.shareQuotaIsValid,
  isValidEnumValue: exports.isValidEnumValue
});

function validateArgs(functionName, validationRules) {
  var validator = new ArgumentValidator(functionName);
  validationRules(validator);
}

exports.ArgumentValidator = ArgumentValidator;
exports.validateArgs = validateArgs;