text: 'use strict';
  
  /** @module where */
  
  const _ = require('lodash');
  const parseKey = require('../util/parse-key');
  const ops = require('./operations');
  
  /**
   * Generate a predicate for a query against an ordinary table.
   *
   * @param {Object} condition - A condition object (generated by parseKey).
   * @return {Object} A predicate object.
   */
  const tableGenerator = function (condition) {
    if (condition.appended.mutator) {
      condition = condition.appended.mutator(condition);
    } else if (condition.value) {
      condition.params.push(condition.value);
      condition.value = `$${condition.offset}`;
    }
  
    return {
      predicate: `${condition.field} ${condition.appended.operator} ${condition.value}`,
      params: condition.params
    };
  };
  
  /**
   * Generate a predicate for a document query.
   *
   * @param {Object} condition - A condition object (generated by parseKey).
   * @param {String} key - The key corresponding to condition in criteria.
   * @param {Array} criteria - The full criteria object for this WHERE clause.
   * @return {Object} A predicate object.
   */
  const docGenerator = function (condition, key, criteria) {
    // case to check if key exist or not
    if (condition.appended.operator === 'IS' || condition.appended.operator === 'IS NOT') {
      condition.rawField = `("body" ->> '${condition.rawField}')`;
    } else if (_.isArray(condition.value) && _.isPlainObject(condition.value[0])) {
      // if we have an array of objects, this is a deep traversal
      // we'll need to use a contains query to be sure we flex the index
      condition.rawField = '"body"';
      condition.appended.operator = '@>';
  
      // only the array of objects matters when we're matching a nested
      // structure, other keys in criteria must be handled separately
      condition.params.push(JSON.stringify(_.pick(criteria, [key])));
      condition.value = `$${condition.offset}`;
    } else if (
      condition.appended.operator === '=' &&
      condition.elements.length === 0 &&
      !_.isArray(condition.value)
    ) {
      // if we have equality here and no traversal, just use a JSON contains
      const param = {};
      param[condition.rawField] = condition.value;
      condition.params.push(JSON.stringify(param));
      condition.rawField = '"body"';
      condition.appended.operator = '@>';
      condition.value = `$${condition.offset}`;
    } else {
      // we're querying a key on the document body!
      // first build the full path; because the rawField is actually the
      // outermost JSON key, prepend it to the rest of the elements.
      const elements = condition.elements.slice();
  
      elements.unshift(condition.rawField);
  
      let cast = '';
  
      // comparison stuff - same as method above but this time
      // we'll be coercing the document key values using pg's ::
      // not ideal, but it works nicely
      if (_.isBoolean(condition.value)) {
        cast = '::boolean';
      } else if (_.isNumber(condition.value)) {
        cast = '::decimal';
      } else if (_.isDate(condition.value)) {
        cast = '::timestamptz';
        condition.params.push(condition.value);
        condition.value = `$${condition.offset}`;
      } else if (condition.appended.mutator) {
        condition = condition.appended.mutator(condition);
      } else {
        condition.params.push(condition.value);
        condition.value = `$${condition.offset}`;
      }
  
      if (elements.length === 1) {
        condition.rawField = `("body" ->> '${elements[0]}')${cast}`;
      } else {
        condition.rawField = `("body" #>> '{${elements.join(',')}}')${cast}`;
      }
    }
  
    return {
      predicate: `${condition.rawField} ${condition.appended.operator} ${condition.value}`,
      params: condition.params
    };
  };
  
  /**
   * Build a disjunction (logical OR).
   *
   * @param {Array} conditions - An array of nested criteria objects. Individual
   * objects will be arrayified (so an 'or' key can work with a single object).
   * @param {Number} offset - Offset prepared statement parameter ordinals.
   * @param {Function} generator - Generator function to use to build SQL
   * predicates.
   * @return {Object} A disjunction node.
   */
  const generateDisjunction = (conditions, offset, generator) => {
    return _.reduce(conditions, (disjunction, subconditions) => {
      // each member of an 'or' array is itself a conjunction, so build it and
      // integrate it into the disjunction predicate structure
      /* eslint-disable no-use-before-define */
      const conjunction = generateConjunction(subconditions, disjunction.offset + disjunction.params.length, generator);
      /* eslint-enable no-use-before-define */
  
      disjunction.params = disjunction.params.concat(conjunction.params);
      disjunction.predicates.push(`(${conjunction.predicates.join(' AND ')})`);
  
      return disjunction;
    }, {
      params: [],
      predicates: [],
      offset
    });
  };
  
  /**
   * Build an inner conjunction (logical OR).
   *
   * @param {Array} conditions - An array of nested criteria objects. Individual
   * objects will be arrayified (so an 'and' key can work with a single object).
   * @param {Number} offset - Offset prepared statement parameter ordinals.
   * @param {Function} generator - Generator function to use to build SQL
   * predicates.
   * @return {Object} A nested conjunction node.
   */
  const generateInnerConjunction = (conditions, offset, generator) => {
    return _.reduce(conditions, (conjunction, subconditions) => {
      // each member of an 'and' array is itself a conjunction, so build it and
      // integrate it into the conjunction predicate structure
      /* eslint-disable no-use-before-define */
      const innerConjunction = generateConjunction(subconditions, conjunction.offset + conjunction.params.length, generator);
      /* eslint-enable no-use-before-define */
  
      conjunction.params = conjunction.params.concat(innerConjunction.params);
      conjunction.predicates.push(`(${innerConjunction.predicates.join(' AND ')})`);
  
      return conjunction;
    }, {
      params: [],
      predicates: [],
      offset
    });
  };
  
  /**
   * Build a conjunction (logical AND).
   *
   * @param {Object} criteria - A criteria object.
   * @param {Number} offset - Offset prepared statement parameter ordinals
   * @param {Function} generator - Generator function to use to build SQL
   * predicates.
   * @return {Object} A conjunction node.
   */
  const generateConjunction = (criteria, offset, generator) => {
    return _.reduce(criteria, (conjunction, value, key) => {
      if (key === 'or') {
        const disjunction = generateDisjunction(value, conjunction.offset + conjunction.params.length, generator);
  
        conjunction.params = conjunction.params.concat(disjunction.params);
        conjunction.predicates.push(`(${disjunction.predicates.join(' OR ')})`);
  
        return conjunction;
      } else if (key === 'and') {
        const inner_conjunction = generateInnerConjunction(value, conjunction.offset + conjunction.params.length, generator);
  
        conjunction.params = conjunction.params.concat(inner_conjunction.params);
        conjunction.predicates.push(`(${inner_conjunction.predicates.join(' AND ')})`);
  
        return conjunction;
      }
  
      const condition = parseKey(key, ops);
  
      if (!!value && condition.isJSON) {
        if (_.isArray(value)) {
          condition.value = value.map(v => v.toString());
        } else {
          condition.value = value.toString();
        }
      } else {
        condition.value = value;
      }
  
      condition.offset = conjunction.offset + conjunction.params.length + 1;
      condition.params = [];
  
      const result = exports[generator](condition, key, criteria);
  
      conjunction.predicates.push(result.predicate);
  
      if (result.params) { conjunction.params = conjunction.params.concat(result.params); }
  
      return conjunction;
    }, {
      params: [],
      predicates: [],
      offset: offset || 0
    });
  };
  
  /**
   * Query conditions generator.
   *
   * @param {Object} criteria - Query criteria mapping column names (optionally
   * including operation eg 'my_field <>') to the parameter values. Predicates
   * generated from a criteria object are joined together with `and`; an `or` key
   * denotes an array of nested criteria objects, the collected predicates from
   * each of which are parenthesized and joined with `or`.
   * @param {Number} [offset=0] - Added to the token index value in the prepared
   * statement (with offset 0, parameters will start $1, $2, $3).
   * @param {String} [generator=generator] - Generator functions mutate the SQL
   * output.
   *
   * @return {Object} An object representing the query conditions. The
   * `conditions` field contains the final SQL string, the `params` field the
   * parameters for the prepared statement, and the `predicates` field the raw
   * predicate mapping.
   */
  exports = module.exports = function where (criteria, offset = 0, generator = 'tableGenerator') {
    if (_.isPlainObject(criteria) && _.isEmpty(criteria)) {
      return {
        conditions: 'TRUE',
        params: []
      };
    }
  
    if (Object.prototype.hasOwnProperty.call(criteria, 'conditions') && Object.prototype.hasOwnProperty.call(criteria, 'params')) {
      // this is a pre-built predicate coming from full-text search or
      // Readable.where
      //
      // searchDoc can override the default generator (filter in columns, then
      // text search against the body) but if there isn't a nestedGenerator
      // passed then defer to the original
      if (_.isPlainObject(criteria.params)) {
        // where() with named parameters
        return {
          conditions: criteria.conditions,
          params: criteria.params
        };
      }
  
      if (_.isPlainObject(criteria.where) && !_.isEmpty(criteria.where)) {
        const subWhere = where(criteria.where || {}, criteria.params.length, criteria.nestedGenerator || generator);
  
        return {
          conditions: `${criteria.conditions} AND ${subWhere.conditions}`,
          params: criteria.params.concat(subWhere.params)
        };
      }
  
      return {
        conditions: criteria.conditions,
        params: criteria.params
      };
    }
  
    const conjunction = generateConjunction(criteria, offset, generator);
  
    return {
      conditions: conjunction.predicates.join(' AND '),
      params: conjunction.params
    };
  };
  
  exports.tableGenerator = tableGenerator;
  exports.docGenerator = docGenerator;
  