text: /*
   * Copyright (c) 2016-present Invertase Limited & Contributors
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this library 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.
   *
   */
  
  import { isNumber } from '@react-native-firebase/app/lib/common';
  import { buildNativeArray, generateNativeData } from './utils/serialize';
  
  const OPERATORS = {
    '==': 'EQUAL',
    '>': 'GREATER_THAN',
    '>=': 'GREATER_THAN_OR_EQUAL',
    '<': 'LESS_THAN',
    '<=': 'LESS_THAN_OR_EQUAL',
    'array-contains': 'ARRAY_CONTAINS',
    'array-contains-any': 'ARRAY_CONTAINS_ANY',
    'in': 'IN',
  };
  
  const INEQUALITY = {
    LESS_THAN: true,
    LESS_THAN_OR_EQUAL: true,
    GREATER_THAN: true,
    GREATER_THAN_OR_EQUAL: true,
  };
  
  const DIRECTIONS = {
    asc: 'ASCENDING',
    desc: 'DESCENDING',
  };
  
  export default class FirestoreQueryModifiers {
    constructor() {
      this._limit = undefined;
      this._filters = [];
      this._orders = [];
      this._type = 'collection';
      // Cursors
      this._startAt = undefined;
      this._startAfter = undefined;
      this._endAt = undefined;
      this._endBefore = undefined;
    }
  
    _copy() {
      const newInstance = new FirestoreQueryModifiers();
      newInstance._limit = this._limit;
      newInstance._filters = [...this._filters];
      newInstance._orders = [...this._orders];
      newInstance._type = this._type;
      newInstance._startAt = this._startAt;
      newInstance._startAfter = this._startAfter;
      newInstance._endAt = this._endAt;
      newInstance._endBefore = this._endBefore;
      return newInstance;
    }
  
    get filters() {
      return this._filters;
    }
  
    get orders() {
      return this._orders;
    }
  
    get options() {
      const options = {};
  
      if (this._limit) {
        options.limit = this._limit;
      }
      if (this._startAt) {
        options.startAt = this._startAt;
      }
      if (this._startAfter) {
        options.startAfter = this._startAfter;
      }
      if (this._endAt) {
        options.endAt = this._endAt;
      }
      if (this._endBefore) {
        options.endBefore = this._endBefore;
      }
  
      return options;
    }
  
    get type() {
      return this._type;
    }
  
    setFieldsCursor(cursor, fields) {
      this[`_${cursor}`] = buildNativeArray(fields);
      return this;
    }
  
    /**
     * Options
     */
  
    hasStart() {
      return !!(this._startAt || this._startAfter);
    }
  
    hasEnd() {
      return !!(this._endAt || this._endBefore);
    }
  
    /**
     * Collection Group
     */
  
    asCollectionGroup() {
      this._type = 'collectionGroup';
      return this;
    }
  
    /**
     * Limit
     */
  
    isValidLimit(limit) {
      return !isNumber(limit) || Math.floor(limit) !== limit || limit <= 0;
    }
  
    limit(limit) {
      this._limit = limit;
      return this;
    }
  
    /**
     * Filters
     */
  
    isValidOperator(operator) {
      return !!OPERATORS[operator];
    }
  
    isEqualOperator(operator) {
      return OPERATORS[operator] === 'EQUAL';
    }
  
    isInOperator(operator) {
      return OPERATORS[operator] === 'IN' || OPERATORS[operator] === 'ARRAY_CONTAINS_ANY';
    }
  
    where(fieldPath, opStr, value) {
      const filter = {
        fieldPath: fieldPath._toPath(),
        operator: OPERATORS[opStr],
        value: generateNativeData(value),
      };
  
      this._filters = this._filters.concat(filter);
      return this;
    }
  
    validateWhere() {
      let hasInequality;
  
      for (let i = 0; i < this._filters.length; i++) {
        const filter = this._filters[i];
        // Skip if no inequality
        if (!INEQUALITY[filter.operator]) {
          continue;
        }
  
        // Set the first inequality
        if (!hasInequality) {
          hasInequality = filter;
          continue;
        }
  
        // Check the set value is the same as the new one
        if (INEQUALITY[filter.operator] && hasInequality) {
          if (hasInequality.fieldPath !== filter.fieldPath) {
            throw new Error(
              `Invalid query. All where filters with an inequality (<, <=, >, or >=) must be on the same field. But you have inequality filters on '${
                hasInequality.fieldPath
              }' and '${filter.fieldPath}'`,
            );
          }
        }
      }
  
      let hasArrayContains;
      let hasArrayContainsAny;
      let hasIn;
  
      for (let i = 0; i < this._filters.length; i++) {
        const filter = this._filters[i];
  
        if (filter.operator === OPERATORS['array-contains']) {
          if (hasArrayContains) {
            throw new Error('Invalid query. Queries only support a single array-contains filter.');
          }
          hasArrayContains = true;
        }
  
        if (filter.operator === OPERATORS['array-contains-any']) {
          if (hasArrayContainsAny) {
            throw new Error(
              "Invalid query. You cannot use more than one 'array-contains-any' filter.",
            );
          }
  
          if (hasIn) {
            throw new Error(
              "Invalid query. You cannot use 'array-contains-any' filters with 'in' filters.",
            );
          }
  
          hasArrayContainsAny = true;
        }
  
        if (filter.operator === OPERATORS.in) {
          if (hasIn) {
            throw new Error("Invalid query. You cannot use more than one 'in' filter.");
          }
  
          if (hasArrayContainsAny) {
            throw new Error(
              "Invalid query. You cannot use 'in' filters with 'array-contains-any' filters.",
            );
          }
  
          hasIn = true;
        }
      }
    }
  
    /**
     * Orders
     */
  
    isValidDirection(directionStr) {
      return !!DIRECTIONS[directionStr.toLowerCase()];
    }
  
    orderBy(fieldPath, directionStr) {
      const order = {
        fieldPath: fieldPath._toPath(),
        direction: directionStr ? DIRECTIONS[directionStr.toLowerCase()] : DIRECTIONS.asc,
      };
  
      this._orders = this._orders.concat(order);
      return this;
    }
  
    validateOrderBy() {
      // Ensure order hasn't been called on the same field
      if (this._orders.length > 1) {
        const orders = this._orders.map($ => $.fieldPath);
        const set = new Set(orders);
  
        if (set.size !== orders.length) {
          throw new Error('Invalid query. Order by clause cannot contain duplicate fields.');
        }
      }
  
      // Skip if no where filters or already validated
      if (this._filters.length === 0 || this._orders.length > 1) {
        return;
      }
  
      // Ensure the first order field path is equal to the inequality filter field path
      for (let i = 0; i < this._filters.length; i++) {
        const filter = this._filters[i];
        // Inequality filter
        if (INEQUALITY[filter.operator]) {
          if (filter.fieldPath !== this._orders[0].fieldPath) {
            throw new Error(
              `Invalid query. You have a where filter with an inequality (<, <=, >, or >=) on field '${
                filter.fieldPath
              }' and so you must also use '${
                filter.fieldPath
              }' as your first Query.orderBy(), but your first Query.orderBy() is on field '${
                this._orders[0].fieldPath
              }' instead`,
            );
          }
        }
      }
    }
  }
  