'use strict'

/** @module extensions */

var asArray = require('./utils.js').asArray

/**
 * Builds a {@link http://docs.marklogic.com/guide/rest-dev/search#id_69918 combined query}
 *
 * @static
 * @param {Object} query - a structured query (from {@link MLQueryBuilder#where})
 * @param {String} [qtext] - a query text string, to be parsed server-side
 * @param {Object} [options] - search options
 * @return {Object} {@link http://docs.marklogic.com/guide/rest-dev/search#id_69918 combined query}
 */
function combined (query, qtext, options) {
  if (!options && qtext && typeof qtext !== 'string') {
    options = qtext
    qtext = null
  }

  return {
    search: {
      query: query && query.query || query,
      qtext: qtext || '',
      options: options && options.options || options
    }
  }
}

/**
 * Builds a {@link http://docs.marklogic.com/guide/search-dev/structured-query#id_38268 `range-constraint-query`}
 *
 * @static
 * @param {String} name - constraint name
 * @param {String} [operator] - operator for matching constraint to `values`; one of `LT`, `LE`, `GT`, `GE`, `EQ`, `NE` (defaults to `EQ`)
 * @param {String|Array<String>} values - the values the constraint should equal (logical OR)
 * @param {String|Array<String>} [options] - range options: {@link http://docs.marklogic.com/guide/rest-dev/appendixa#id_84264}
 * @return {Object} {@link http://docs.marklogic.com/guide/search-dev/structured-query#id_38268 range-constraint-query}
 */
function rangeConstraint (name, operator, values, options) {
  if (!values && !options) {
    values = operator
    operator = null
  }

  // TODO: use comparisons from qb.range

  if (operator && ['LT', 'LE', 'GT', 'GE', 'EQ', 'NE'].indexOf(operator) === -1) {
    throw new TypeError('invalid rangeConstraint query operator: ' + operator)
  }

  return {
    'range-constraint-query': {
      'constraint-name': name,
      'range-operator': operator || 'EQ',
      'value': asArray(values),
      'range-option': asArray(options)
    }
  }
}

/**
 * Builds a {@link http://docs.marklogic.com/guide/search-dev/structured-query#id_63420 `value-constraint-query`}
 *
 * @static
 * @param {String} name - constraint name
 * @param {String|Number|Array<String>|Array<Number>|null} values - the values the constraint should equal (logical OR)
 * @return {Object} {@link http://docs.marklogic.com/guide/search-dev/structured-query#id_63420 value-constraint-query}
 */
function valueConstraint (name, values) {
  var query = {
    'value-constraint-query': {
      'constraint-name': name
    }
  }

  var type

  if (values === null) {
    type = 'null'
    values = []
  } else {
    values = asArray(values)
    type = typeof values[0]
    type = ((type === 'string') && 'text') || type
  }

  query['value-constraint-query'][type] = values

  return query
}

/**
 * Builds a {@link http://docs.marklogic.com/guide/search-dev/structured-query#id_66833 `word-constraint-query`}
 *
 * @static
 * @param {String} name - constraint name
 * @param {String|Array<String>} values - the values the constraint should equal (logical OR)
 * @return {Object} {@link http://docs.marklogic.com/guide/search-dev/structured-query#id_66833 word-constraint-query}
 */
function wordConstraint (name, values) {
  return {
    'word-constraint-query': {
      'constraint-name': name,
      'text': asArray(values)
    }
  }
}

/**
 * Builds a {@link http://docs.marklogic.com/guide/search-dev/structured-query#id_30776 `collection-constraint-query`}
 *
 * @static
 * @param {String} name - constraint name
 * @param {String|Array<String>} values - the values the constraint should equal (logical OR)
 * @return {Object} {@link http://docs.marklogic.com/guide/search-dev/structured-query#id_30776 collection-constraint-query}
 */
function collectionConstraint (name, values) {
  return {
    'collection-constraint-query': {
      'constraint-name': name,
      'uri': asArray(values)
    }
  }
}

/**
 * Builds a {@link http://docs.marklogic.com/guide/search-dev/structured-query#id_28778 `custom-constraint-query`}
 *
 * @static
 * @param {String} name - constraint name
 * @param {...String|Array<String>|Array<Object>} values - the values the constraint should equal (logical OR)
 * @return {Object} {@link http://docs.marklogic.com/guide/search-dev/structured-query#id_28778 custom-constraint-query}
 */
function customConstraint () {
  var args = asArray.apply(null, arguments)

  var constraintName = args.shift()

  // horrible hack for when arguments.length === 2 and arguments[1] is an array
  if (args.length === 1 && Array.isArray(args[0])) {
    args = args[0]
  }

  // args instanceof Array<Object>
  var shouldExtend = args.map(function (arg) {
    return arg && typeof arg === 'object'
  })
  .reduce(function (a, b) {
    return a && b
  })

  var query = {
    'custom-constraint-query': {
      'constraint-name': constraintName
    }
  }

  if (shouldExtend) {
    while (args.length) {
      Object.assign(query['custom-constraint-query'], args.shift())
    }
  } else {
    query['custom-constraint-query'].text = args.filter(function (arg) {
      return !(arg && typeof arg === 'object')
    })
  }

  return query
}

/**
 * Helper method: builds an object of `points`, `boxes`, `circles`, and `polygons`,
 * used by {@link MLQueryBuilder.ext.geospatialConstraint}, for use with
 * {@link MLQueryBuilder.ext.customConstraint}
 *
 * examples:
 *
 *   ```
 *   qb.ext.geospatialConstraint('name',
 *     { latitude: 1, longitude: 2 },
 *     { south: 1, west: 2, north: 3, east: 4 }
 *   )
 *   ```
 *
 *   ```
 *   qb.ext.customConstraint('name', qb.ext.geospatialValues(
 *     { latitude: 1, longitude: 2 },
 *     { south: 1, west: 2, north: 3, east: 4 }
 *   ))
 *   ```
 *
 * @static
 * @param {...Object} values - the geospatial values to parse
 * @return {Object} parsed geospatial values
 */
function geospatialValues () {
  var shapes = asArray.apply(null, arguments)

  var points = []
  var boxes = []
  var circles = []
  var polygons = []
  var shape

  for (var i = 0; i < shapes.length; i++) {
    shape = shapes[i]

    if (shape.latitude) {
      points.push(shape)
    } else if (shape.south) {
      boxes.push(shape)
    } else if (shape.radius) {
      circles.push(shape)
    } else if (shape.point) {
      polygons.push(shape)
    }
  }

  return {
    point: points,
    box: boxes,
    circle: circles,
    polygon: polygons
  }
}

/**
 * Builds a {@link http://docs.marklogic.com/guide/search-dev/structured-query#id_88775 `geospatial-constraint-query`}
 *
 * @static
 * @param {String} name - constraint name
 * @param {...Object} values - the geospatial values to parse
 * @return {Object} [geospatial-constraint-query](http://docs.marklogic.com/guide/search-dev/structured-query#id_88775)
 */
function geospatialConstraint () {
  var args = asArray.apply(null, arguments)

  var constraintName = args.shift()

  // horrible hack for when arguments.length === 2 and arguments[1] is an array
  if (args.length === 1 && Array.isArray(args[0])) {
    args = args[0]
  }

  var geoValues = this.geospatialValues.apply(this, args)

  var query = {
    'geospatial-constraint-query': {
      'constraint-name': constraintName
    }
  }

  Object.assign(query['geospatial-constraint-query'], geoValues)

  return query
}

/**
 * constraint query function factory
 *
 * @static
 * @param {String} type - constraint type (`'value' || 'word' || collection' || 'custom' || '*'`)
 * @return {Function} a constraint query builder function, one of:
 *   - {@link MLQueryBuilder.ext.rangeConstraint}
 *   - {@link MLQueryBuilder.ext.valueConstraint}
 *   - {@link MLQueryBuilder.ext.wordConstraint}
 *   - {@link MLQueryBuilder.ext.collectionConstraint}
 *   - {@link MLQueryBuilder.ext.customConstraint}
 */
function constraint (type) {
  switch (type) {
    case 'value':
      return this.valueConstraint
    case 'word':
      return this.wordConstraint
    case 'custom':
      return this.customConstraint
    case 'collection':
      return this.collectionConstraint
    default:
      return this.rangeConstraint
  }
}

/**
 * Builds an {@link http://docs.marklogic.com/guide/search-dev/structured-query#id_45570 `operator-state` query component}
 *
 * @static
 * @param {String} name - operator name
 * @param {String} stateName - operator-state name
 * @return {Object} {@link http://docs.marklogic.com/guide/search-dev/structured-query#id_45570 operator-state query component}
 */
function operatorState (name, stateName) {
  return {
    'operator-state': {
      'operator-name': name,
      'state-name': stateName
    }
  }
}

module.exports = {
  combined: combined,
  rangeConstraint: rangeConstraint,
  valueConstraint: valueConstraint,
  wordConstraint: wordConstraint,
  collectionConstraint: collectionConstraint,
  customConstraint: customConstraint,
  geospatialValues: geospatialValues,
  geospatialConstraint: geospatialConstraint,
  constraint: constraint,
  operatorState: operatorState
}