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

// Module dependencies.
var _ = require('underscore');
var util = require('util');

var azureCommon = require('./../../common/common.core');
var azureutil = azureCommon.util;
var SR = azureCommon.SR;
var QueryStringConstants =  azureCommon.Constants.QueryStringConstants;

var edmHandler = require('./internal/edmhandler');
var TableUtilities = require('./tableutilities');
var QueryComparisons = TableUtilities.QueryComparisons;
var TableOperators = TableUtilities.TableOperators;
var EdmType = TableUtilities.EdmType;

/**
 * Creates a new TableQuery object.
 *
 * @constructor
 */
function TableQuery() {
  this._fields = [];
  this._where = [];
  this._top = null;
}

/**
* Specifies the select clause. If no arguments are given, all fields will be selected.
*
* @param {array} fields The fields to be selected.
* @return {TableQuery} A table query object with the select clause.
* @example
* var tableQuery = new TableQuery().select('field1', 'field2');
*/
TableQuery.prototype.select = function () {
  var self = this;
  if (arguments) {
    _.each(arguments, function (argument) {
      self._fields.push(argument);
    });
  }

  return this;
};

/**
 * Specifies the top clause.
 *
 * @param {int} top The number of items to fetch.
 * @return {TableQuery} A table query object with the top clause.
 * @example
 * var tableQuery = new TableQuery().top(10);
 *
 * // tasktable should already exist and have entities
 * tableService.queryEntities('tasktable', tableQuery, null \/*currentToken*\/, function(error, result) {
 *   if(!error) { 
 *     var entities = result.entities; // there will be 10 or less entities
 *     // do stuff with the returned entities if there are any
 *     // if result.continuationToken exists, to get the next 10 (or less) entities
 *     // call queryEntities as above, but with the returned token instead of null
 *   }
 * });
 */
TableQuery.prototype.top = function (top) {
  this._top = top;
  return this;
};

/**
 * Specifies the where clause.
 *
 * Valid type specifier strings include: ?string?, ?bool?, ?int32?, ?double?, ?date?, ?guid?, ?int64?, ?binary?
 * A type must be specified for guid, int64, and binaries or the filter produced will be incorrect.
 *
 * @param {string}       condition   The condition string.
 * @param {string|array} value       Value(s) to insert in question mark (?) parameters.
 * @return {TableQuery}  A table query object with the where clause.
 * @example
 * var tableQuery = new TableQuery().where(TableQuery.guidFilter('GuidField', QueryComparisons.EQUAL, guidVal));
 * OR
 * var tableQuery = new TableQuery().where('Name == ? or Name <= ?', name1, name2);
 * OR
 * var tableQuery = new TableQuery().where('Name == ?string? && Value == ?int64?', name1, int64Val);
 *
 * // tasktable should already exist and have entities
 * tableService.queryEntities('tasktable', tableQuery, null \/*currentToken*\/, function(error, result, response) {
 *   if(!error) { 
 *     var entities = result.entities;
 *     // do stuff with the returned entities if there are any
 *   }
 * });
 */
TableQuery.prototype.where = function (condition) {  
  this._where.push(TableQuery._encodeConditionString(condition, arguments));
  return this;
};

/**
 * Generates a property filter condition string for an 'int' value.
 *
 * @param {string}       propertyName   A string containing the name of the property to compare.
 * @param {string}       operation      A string containing the comparison operator to use. 
 *                                      See Constants.TableConstants.QueryComparisons for a list of allowed operations.
 * @param {string|int}   value          An 'int' containing the value to compare with the property.
 * @return {string} A string containing the formatted filter condition.
 * @example
 * var query = TableQuery.int32Filter('IntField', QueryComparisons.EQUAL, 5);
 */
TableQuery.int32Filter = function (propertyName, operation, value) {
  return TableQuery._concatFilterString(propertyName, operation, value, EdmType.INT32);
};

/**
 * Generates a property filter condition string for a 'int64' value.
 *
 * @param {string}       propertyName   A string containing the name of the property to compare.
 * @param {string}       operation      A string containing the comparison operator to use. 
 *                                      See Constants.TableConstants.QueryComparisons for a list of allowed operations.
 * @param {string|int64} value          An 'int64' containing the value to compare with the property.
 * @return {string} A string containing the formatted filter condition.
 * @example
 * var query = TableQuery.int64Filter('Int64Field', QueryComparisons.EQUAL, 123);
 */
TableQuery.int64Filter = function (propertyName, operation, value) {
  return TableQuery._concatFilterString(propertyName, operation, value, EdmType.INT64);
};

/**
 * Generates a property filter condition string for a 'double' value.
 *
 * @param {string}       propertyName   A string containing the name of the property to compare.
 * @param {string}       operation      A string containing the comparison operator to use. 
 *                                      See Constants.TableConstants.QueryComparisons for a list of allowed operations.
 * @param {string|double}value          A 'double' containing the value to compare with the property.
 * @return {string} A string containing the formatted filter condition.
 * @example
 * var query = TableQuery.doubleFilter('DoubleField', QueryComparisons.EQUAL, 123.45);
 */
TableQuery.doubleFilter = function (propertyName, operation, value) {
  return TableQuery._concatFilterString(propertyName, operation, value, EdmType.DOUBLE);
};

/**
 * Generates a property filter condition string for a 'boolean' value.
 *
 * @param {string}       propertyName   A string containing the name of the property to compare.
 * @param {string}       operation      A string containing the comparison operator to use. 
 *                                      See Constants.TableConstants.QueryComparisons for a list of allowed operations.
 * @param {string|boolean}       value          A 'boolean' containing the value to compare with the property.
 * @return {string} A string containing the formatted filter condition.
 * @example
 * var query = TableQuery.booleanFilter('BooleanField', QueryComparisons.EQUAL, false);
 */
TableQuery.booleanFilter = function (propertyName, operation, value) {
  return TableQuery._concatFilterString(propertyName, operation, value, EdmType.BOOLEAN);
};

/**
 * Generates a property filter condition string for a 'datetime' value.
 *
 * @param {string}       propertyName   A string containing the name of the property to compare.
 * @param {string}       operation      A string containing the comparison operator to use. 
 *                                      See Constants.TableConstants.QueryComparisons for a list of allowed operations.
 * @param {string|date}     value              A 'datetime' containing the value to compare with the property.
 * @return {string} A string containing the formatted filter condition.
 * @example
 * var query = TableQuery.dateFilter('DateTimeField', QueryComparisons.EQUAL, new Date(Date.UTC(2001, 1, 3, 4, 5, 6)));
 */
TableQuery.dateFilter = function (propertyName, operation, value) {
  return TableQuery._concatFilterString(propertyName, operation, value, EdmType.DATETIME);
};

/**
 * Generates a property filter condition string for a 'guid' value.
 *
 * @param {string}       propertyName   A string containing the name of the property to compare.
 * @param {string}       operation      A string containing the comparison operator to use. 
 *                                      See Constants.TableConstants.QueryComparisons for a list of allowed operations.
 * @param {string|guid}  value          A 'guid' containing the value to compare with the property.
 * @return {string} A string containing the formatted filter condition.
 * @example
 * var query = TableQuery.guidFilter('GuidField', QueryComparisons.EQUAL, guid.v1());
 */
TableQuery.guidFilter = function (propertyName, operation, value) {
  return TableQuery._concatFilterString(propertyName, operation, value, EdmType.GUID);
};

/**
 * Generates a property filter condition string for a 'binary' value.
 *
 * @param {string}       propertyName   A string containing the name of the property to compare.
 * @param {string}       operation      A string containing the comparison operator to use. 
 *                                      See Constants.TableConstants.QueryComparisons for a list of allowed operations.
 * @param {string|buffer}value          A 'buffer' containing the value to compare with the property.
 * @return {string} A string containing the formatted filter condition.
 * @example
 * var query = TableQuery.binaryFilter('BinaryField', QueryComparisons.EQUAL, Buffer.from('hello'));
 */
TableQuery.binaryFilter = function (propertyName, operation, value) {
  return TableQuery._concatFilterString(propertyName, operation, value, EdmType.BINARY);
};

/**
 * Generates a property filter condition string.
 *
 * @param {string}       propertyName   A string containing the name of the property to compare.
 * @param {string}       operation      A string containing the comparison operator to use. 
 *                                      See Constants.TableConstants.QueryComparisons for a list of allowed operations.
 * @param {string}       value          A 'string' containing the value to compare with the property.
 * @return {string} A string containing the formatted filter condition.
 * @example
 * var query = TableQuery.stringFilter('StringField', QueryComparisons.EQUAL, 'name');
 */
TableQuery.stringFilter = function (propertyName, operation, value) {
  return TableQuery._concatFilterString(propertyName, operation, value, EdmType.STRING);
};

/**
 * Creates a filter condition using the specified logical operator on two filter conditions.
 *
 * @param {string}       filterA          A string containing the first formatted filter condition.
 * @param {string}       operatorString   A string containing the operator to use (AND, OR).
 * @param {string}       filterB          A string containing the second formatted filter condition.
 * @return {string} A string containing the combined filter expression.
 * @example
 * var filter1 = TableQuery.stringFilter('Name', QueryComparisons.EQUAL, 'Person');
 * var filter2 = TableQuery.booleanFilter('Visible', QueryComparisons.EQUAL, true);
 * var combinedFilter = TableQuery.combineFilters(filter1, TableUtilities.TableOperators.AND, filter2);
 */
TableQuery.combineFilters = function (filterA, operatorString, filterB) {
  return filterA + ' ' + operatorString + ' ' + filterB;
};

/**
 * Specifies an AND where condition.
 *
 * @param {string}       condition   The condition string.
 * @param {array}        arguments   Any number of arguments to be replaced in the condition by the question mark (?).
 * @return {TableQuery} A table query object with the and clause.
 * @example
 * var tableQuery = new TableQuery()
 *                      .where('Name == ? or Name <= ?', 'Person1', 'Person2');
 *                      .and('Age >= ?', 18);
 */
TableQuery.prototype.and = function (condition) {
  if (this._where.length === 0) {
    throw new Error(util.format(SR.QUERY_OPERATOR_REQUIRES_WHERE, 'AND'));
  }

  this._where.push(' and ' + TableQuery._encodeConditionString(condition, arguments));
  return this;
};

/**
 * Specifies an OR where condition.
 *
 * @param {string}       condition   The condition.
 * @param {array}        arguments   Any number of arguments to be replaced in the condition by the question mark (?).
 * @return {TableQuery} A table query object with the or clause.
 * @example
 * var tableQuery = new TableQuery()
 *                      .where('Name == ? or Name <= ?', 'Person1', 'Person2');
 *                      .or('Age >= ?', 18);
 */
TableQuery.prototype.or = function (condition) {
  if (this._where.length === 0) {
    throw new Error(util.format(SR.QUERY_OPERATOR_REQUIRES_WHERE, 'OR'));
  }

  this._where.push(' or ' + TableQuery._encodeConditionString(condition, arguments));
  return this;
};

/**
 * Returns the query string object for the query.
 *
 * @return {object} JSON object representing the query string arguments for the query.
 */
TableQuery.prototype.toQueryObject = function () {
  var query = {};
  if (this._fields.length > 0) {
    query[QueryStringConstants.SELECT] = this._fields.join(',');
  }

  if (this._where.length > 0) {
    query[QueryStringConstants.FILTER] = this._where.join('');
  }

  if (this._top) {
    query[QueryStringConstants.TOP] = this._top;
  }

  return query;
};

// Functions

/**
* Concat the filter string parameters.
*
* @param {string}       propertyName   A string containing the name of the property to compare.
* @param {string}       operation      A string containing the comparison operator to use. 
*                                      See Constants.TableConstants.QueryComparisons for a list of allowed operations.
* @param {object}       value          The value to compare with the property.
* @param {string}       type           A string EdmType of the property to compare.
* @return {string} A string containing the formatted filter condition.
* @ignore
*/
TableQuery._concatFilterString = function (propertyName, operation, value, type) {
  if (azureutil.objectIsNull(propertyName)) {
    throw new Error(util.format(SR.ARGUMENT_NULL_OR_UNDEFINED, 'propertyName'));
  }

  if (azureutil.objectIsNull(operation)) {
    throw new Error(util.format(SR.ARGUMENT_NULL_OR_UNDEFINED, 'operation'));
  }

  if (azureutil.objectIsNull(value)) {
    throw new Error(util.format(SR.ARGUMENT_NULL_OR_UNDEFINED, 'value'));
  }

  var serializedValue = edmHandler.serializeQueryValue(value, type);
  return propertyName + ' ' + operation + ' ' + serializedValue;
};

/**
 * Encodes a condition string.
 *
 * @param {string}       condition   The condition.
 * @param {array}        arguments   Any number of arguments to be replaced in the condition by the question mark (?).
 * @return {TableQuery} A table query object with the or clause
 * @ignore
 */
TableQuery._encodeConditionString = function (condition, args) {
  var encodedCondition = TableQuery._replaceOperators(condition);
  if (args.length > 1) {
    var sections = encodedCondition.split(/(\?string\?|\?int32\?|\?int64\?|\?bool\?|\?double\?|\?date\?|\?binary\?|\?guid\?|\?)/);
    var count = 1;
    for (var i = 0; i < sections.length && count < args.length; i++) {
      if (sections[i].indexOf('?') === 0) {
        var type = TableQuery._getEdmType(sections[i]);
        sections[i] = edmHandler.serializeQueryValue(args[count], type);
        count++;
      }
    }
    encodedCondition = sections.join('');
  }

  return encodedCondition;
};

/**
 * Converts the query string type to an Edm type.
 *
 * @param {string} type The type included in the query string.
 * @return {string} The EdmType.
 * @ignore
 */
TableQuery._getEdmType = function (type) {
  switch (type) {
    case '?binary?':
      return EdmType.BINARY;
    case '?int64?':
      return EdmType.INT64;
    case '?date?':
      return EdmType.DATETIME;
    case '?guid?':
      return EdmType.GUID;
    case '?int32?':
      return EdmType.INT32;
    case '?double?':
      return EdmType.DOUBLE;
    case '?bool?':
      return EdmType.BOOLEAN;
    case '?string?':
      return EdmType.STRING;
    default:
      return undefined;
  }
};

/**
 * Replace operators.
 * @ignore
 * @param {string} whereClause The text where to replace the operators.
 * @return {string} The string with the replaced operators.
 * @ignore
 */
TableQuery._replaceOperators = function (whereClause) {
  var encodedWhereClause = whereClause.replace(/ == /g, ' ' + QueryComparisons.EQUAL + ' ');
  encodedWhereClause = encodedWhereClause.replace(/ != /g, ' ' + QueryComparisons.NOT_EQUAL + ' ');
  encodedWhereClause = encodedWhereClause.replace(/ >= /g, ' ' + QueryComparisons.GREATER_THAN_OR_EQUAL + ' ');
  encodedWhereClause = encodedWhereClause.replace(/ > /g, ' ' + QueryComparisons.GREATER_THAN + ' ');
  encodedWhereClause = encodedWhereClause.replace(/ <= /g, ' ' + QueryComparisons.LESS_THAN_OR_EQUAL + ' ');
  encodedWhereClause = encodedWhereClause.replace(/ < /g, ' ' + QueryComparisons.LESS_THAN + ' ');
  encodedWhereClause = encodedWhereClause.replace(/ \&\& /g, ' ' + TableOperators.AND + ' ');
  encodedWhereClause = encodedWhereClause.replace(/ \|\| /g, ' ' + TableOperators.OR + ' ');
  encodedWhereClause = encodedWhereClause.replace(/!/g, TableOperators.NOT);

  return encodedWhereClause;
};

module.exports = TableQuery;
