Show:
import { isNone } from '@ember/utils';
import { warn } from '@ember/debug';
import BaseAdapter from './base-adapter';
import {
  SimplePredicate,
  ComplexPredicate,
  StringPredicate,
  DetailPredicate,
  DatePredicate,
  GeographyPredicate,
  GeometryPredicate,
  TruePredicate,
  FalsePredicate
} from './predicate';
import { ConstParam, AttributeParam } from './parameter';
import FilterOperator from './filter-operator';
import Condition from './condition';
import Information from '../utils/information';

/**
 * Class of query language adapter that translates query object into JS function which
 * filters native JS array of objects by specified logic.
 *
 * ```js
 * const data = [
 *   { Name: 'A', Surname: 'X', Age: 10 },
 *   { Name: 'B', Surname: 'Y', Age: 11 },
 *   { Name: 'B', Surname: 'Z', Age: 12 }
 * ];
 *
 * let adapter = new JSAdapter(moment);
 * let builder = new QueryBuilder(store, 'AnyUnknownModel').where('Name', FilterOperator.Eq, 'B');
 * let filter = adapter.buildFunc(builder.build());
 *
 * let result = filter(data); // Y and Z
 * ```
 *
 * All filters uses short circuit logic ([wiki](https://en.wikipedia.org/wiki/Short-circuit_evaluation)).
 *
 * @module ember-flexberry-data
 * @class JsAdapter
 * @extends Query.BaseAdapter
 */
export default class JSAdapter extends BaseAdapter {
  /**
    @param {Moment} moment Moment service.
    @class IndexedDBAdapter
    @constructor
  */
  constructor(moment) {
    super();

    if (moment) {
      this._moment = moment;
    }
  }

  /**
   * Builds JS function for filtering JS array of objects by specified logic from query.
   *
   * @method buildFunc
   * @param query Query language instance.
   * @returns {Function} Function for filtering JS array of objects.
   */
  buildFunc(query) {
    let filter = query.predicate ? this.buildFilter(query.predicate) : (data) => data;
    let order = this.buildOrder(query);
    let projection = this.buildProjection(query);
    let topSkip = this.buildTopSkip(query);
    return function (data) {
      return projection(topSkip(order(filter(data))));
    };
  }

  /**
   * Builds function for windowing array of objects using data from the query.
   *
   * @param {QueryObject} query Query instance.
   * @returns {Function}
   */
  buildTopSkip(query) {
    if (!query.top && !query.skip) {
      return data => data;
    }

    return data => {
      let r = [];
      for (let i = 0; i < data.length; i++) {
        if (i < query.skip) {
          continue;
        }

        r.push(data[i]);

        if (r.length >= query.top) {
          break;
        }
      }

      return r;
    };
  }

  /**
   * Builds function for ordering array of objects using data from the query.
   *
   * @param {QueryObject} query Query instance.
   * @returns {Function}
   */
  buildOrder(query) {
    if (!query.order) {
      return data => data;
    }

    let desc = (a, b, p) => {
      let av = this.getValue(a, p);
      let bv = this.getValue(b, p);
      if (av > bv) {
        return -1;
      } else if (av < bv) {
        return 1;
      } else {
        return 0;
      }
    };

    let asc = (a, b, p) => {
      let av = this.getValue(a, p);
      let bv = this.getValue(b, p);
      if (av < bv) {
        return -1;
      } else if (av > bv) {
        return 1;
      } else {
        return 0;
      }
    };

    return function (data) {
      return data.sort((a, b) => {
        for (let i = 0; i < query.order.length; i++) {
          let p = query.order.attribute(i);
          let r = p.direction === 'desc' ? desc(a, b, p.name) : asc(a, b, p.name); // TODO: desc / asc constants
          if (r === 0) {
            continue;
          }

          return r;
        }

        return 0;
      });
    };
  }

  /**
   * Builds function for selecting subset of properties from array of objects using data from the query.
   *
   * @param {QueryObject} query Query instance.
   * @returns {Function}
   */
  buildProjection(query) {
    let select = query.select;
    let expand = query.expand;
    let expandKeys = Object.keys(expand);
    if (!select || select.length === 0) {
      return data => data;
    }

    return function (data) {
      let dataMap = data.map(item => {
        let r = {};

        let applySelect = function (r, item, select, exclude) {
          if (!item) {
            return;
          }

          let length = select.length;
          for (let i = 0; i < length; i++) {
            let key = select[i];
            if (exclude.indexOf(key) === -1) {
              r[key] = item[key];
            }
          }
        };

        applySelect(r, item, select, expandKeys);

        let processExpand = function(r, item, expand, expandKeys) {
          if (!expand) {
            return;
          }

          let length = expandKeys.length;
          for (let i = 0; i < length; i++) {
            let expandKey = expandKeys[i];

            let expandItem = expand[expandKey];
            let expandItemSelect = expandItem.select;
            let expandItemExpand = expandItem.expand;
            let expandItemExpandKeys = Object.keys(expandItemExpand);

            if (expandItem.relationship.type === 'belongsTo') {
              let itemValue = item[expandKey];
              if (itemValue) {
                // Try to include attr that stores type of relationship for polymorphic master in offline mode.
                // It makes sense only when buildProjection was called from indexeddb-adapter.
                // Otherwise given object always will not contain attr with polymorphic relationship type name.
                if (expandItem.relationship.polymorphic) {
                  let polymorphicMasterTypeKey = '_' + expandKey + '_type';
                  if (item.hasOwnProperty(polymorphicMasterTypeKey)) {
                    r[polymorphicMasterTypeKey] = item[polymorphicMasterTypeKey];
                  }
                }

                r[expandKey] = {};
                applySelect(r[expandKey], itemValue, expandItemSelect, expandItemExpandKeys);
                processExpand(r[expandKey], itemValue, expandItemExpand, expandItemExpandKeys);
              } else {
                r[expandKey] = null;
              }
            } else {
              r[expandKey] = [];
              let detailsCount = isNone(item[expandKey]) ? 0 : item[expandKey].length;
              for (let j = 0; j < detailsCount; j++) {
                let itemValue = item[expandKey][j];
                if (itemValue) {
                  r[expandKey].push({});
                  applySelect(r[expandKey][j], itemValue, expandItemSelect, expandItemExpandKeys);
                  processExpand(r[expandKey][j], itemValue, expandItemExpand, expandItemExpandKeys);
                }
              }
            }
          }
        };

        processExpand(r, item, expand, expandKeys);
        return r;
      });
      return dataMap;
    };
  }

  /**
    Builds function for filtering array of objects using predicate.

    @param {Query.BasePredicate} predicate Predicate for filtering array of objects.
    @param {Object} [options] Object with options for transfer `getAttributeFilterFunction` function.
    @return {Function}
  */
  buildFilter(predicate, options) {
    let b1 = predicate instanceof SimplePredicate;
    let b2 = predicate instanceof StringPredicate;
    let b3 = predicate instanceof DetailPredicate;
    let b4 = predicate instanceof DatePredicate;
    let b5 = predicate instanceof GeographyPredicate;
    let b6 = predicate instanceof GeometryPredicate;
    let b7 = predicate instanceof TruePredicate;
    let b8 = predicate instanceof FalsePredicate;
    if (b1 || b2 || b3 || b4 || b7 || b8) {
      let filterFunction = this.getAttributeFilterFunction(predicate, options);
      return this.getFilterFunctionAnd([filterFunction]);
    }

    if (b5) {
      warn('GeographyPredicate is not supported in js-adapter',
      false,
      { id: 'ember-flexberry-data-debug.js-adapter.geography-predicate-is-not-supported' });
      return function (data) {
        return data;
      };
    }

    if (b6) {
      warn('GeometryPredicate is not supported in js-adapter',
      false,
      { id: 'ember-flexberry-data-debug.js-adapter.geometry-predicate-is-not-supported' });
      return function (data) {
        return data;
      };
    }

    if (predicate instanceof ComplexPredicate) {
      let filterFunctions = predicate.predicates.map(predicate => this.getAttributeFilterFunction(predicate, options));
      switch (predicate.condition) {
        case Condition.And:
          return this.getFilterFunctionAnd(filterFunctions);

        case Condition.Or:
          return this.getFilterFunctionOr(filterFunctions);

        default:
          throw new Error(`Unsupported condition '${predicate.condition}'.`);
      }
    }

    throw new Error(`Unsupported predicate '${predicate}'`);
  }

  /**
    Returns function for checkign single object using predicate.

    @param {Query.BasePredicate} predicate Predicate for an attribute.
    @param {Object} [options] Object with options.
    @param {Object} [options.booleanAsString] If this option set as `true` and type of `predicate.value` equals boolean, convert value to string.
    @returns {Function} Function for checkign single object.
  */
  getAttributeFilterFunction(predicate, options) {
    let _this = this;
    if (!predicate) {
      return (i) => i;
    }

    if (predicate instanceof SimplePredicate || predicate instanceof DatePredicate) {
      // predicate.attributePath - attribute or AttributeParam or ConstParam.
      // predicate.value - const or AttributeParam or ConstParam.

      return (i) => {
        let firstOperand = predicate.attributePath;
        let secondOperand = predicate.value;
        let isFirstAttribute = !(firstOperand instanceof ConstParam);
        let isSecondAttribute = secondOperand instanceof AttributeParam;

        let processAttributeFunction = function (attributeOperand, predicate) {
          let realPath = attributeOperand instanceof AttributeParam
                          ? attributeOperand.attributePath
                          : attributeOperand;
          let valueFromHash = _this.getValue(i, realPath);
          let momentFromHash;
          if (predicate instanceof DatePredicate) {
            momentFromHash = _this._moment.moment(valueFromHash);
          }

          // TODO: add support of variant without id.
          let masterValue;
          let isMasterPath = realPath.indexOf('.', 1) !== -1;
          if (isMasterPath) {
            let masterPath = realPath.slice(0, realPath.lastIndexOf('.'));
            masterValue = _this.getValue(i, masterPath);
          }
          
          return {
            value: valueFromHash,
            momentValue: momentFromHash,
            isMasterPath: isMasterPath,
            masterValue: masterValue
          };
        };

        let processValueFunction = function (constOperand, predicate) {
          let realValue = constOperand instanceof ConstParam
                            ? constOperand.constValue
                            : constOperand;
          let momentValue;
          if (options && options.booleanAsString && typeof realValue === 'boolean') {
            realValue = `${realValue}`;
          }

          if (predicate instanceof DatePredicate) {
            momentValue = _this._moment.moment(realValue);
          }

          return {
            value: realValue,
            momentValue: momentValue
          };
        };

        let firstValue = isFirstAttribute
                          ? processAttributeFunction(firstOperand, predicate)
                          : processValueFunction(firstOperand, predicate);
        
        let secondValue = isSecondAttribute
                          ? processAttributeFunction(secondOperand, predicate)
                          : processValueFunction(secondOperand, predicate);
        
        let datesIsValid = (predicate instanceof DatePredicate) 
                            && firstValue.momentValue.isValid() && secondValue.momentValue.isValid();

        let realFirstArgument = datesIsValid ? firstValue.momentValue : firstValue.value;
        let realSecondArgument = secondValue.value;

        let resultPredicate = null;

        switch (predicate.operator) {
          case FilterOperator.Eq:
            resultPredicate = datesIsValid 
                    ? (predicate.timeless ? realFirstArgument.isSame(realSecondArgument, 'day') : realFirstArgument.isSame(realSecondArgument))
                    : realFirstArgument === realSecondArgument;
            break;
          case FilterOperator.Neq:
            resultPredicate = datesIsValid 
                    ? (predicate.timeless ? !realFirstArgument.isSame(realSecondArgument, 'day') : !realFirstArgument.isSame(realSecondArgument))
                    : realFirstArgument !== realSecondArgument;
            break;
          case FilterOperator.Le:
            resultPredicate = datesIsValid 
                    ? (predicate.timeless ? realFirstArgument.isBefore(realSecondArgument, 'day') : realFirstArgument.isBefore(realSecondArgument))
                    : realFirstArgument < realSecondArgument;
            break;
          case FilterOperator.Leq:
            resultPredicate = datesIsValid 
                    ? (predicate.timeless ? realFirstArgument.isSameOrBefore(realSecondArgument, 'day') : realFirstArgument.isSameOrBefore(realSecondArgument))
                    : realFirstArgument <= realSecondArgument;
            break;
          case FilterOperator.Ge:
            resultPredicate = datesIsValid 
                    ? (predicate.timeless ? realFirstArgument.isAfter(realSecondArgument, 'day') : realFirstArgument.isAfter(realSecondArgument))
                    : realFirstArgument > realSecondArgument;
            break;
          case FilterOperator.Geq:
            resultPredicate = datesIsValid 
                    ? (predicate.timeless ? realFirstArgument.isSameOrAfter(realSecondArgument, 'day') : realFirstArgument.isSameOrAfter(realSecondArgument))
                    : realFirstArgument >= realSecondArgument;
            break;
          default:
            throw new Error(`Unsupported filter operator '${predicate.operator}'.`);
        }

        if (isFirstAttribute && isSecondAttribute) {
          resultPredicate = (!firstValue.isMasterPath || firstValue.masterValue) 
                            && (!secondValue.isMasterPath || secondValue.masterValue)
                            && resultPredicate;
        }

        return resultPredicate;
      };
    }

    if (predicate instanceof StringPredicate) {
      return (i) => (_this.getValue(i, predicate.attributePath) || '').toLowerCase().indexOf(predicate.containsValue.toLowerCase()) > -1;
    }

    if (predicate instanceof TruePredicate) {
      return () => true;
    }

    if (predicate instanceof FalsePredicate) {
      return () => false;
    }

    if (predicate instanceof DetailPredicate) {
      let detailFilter = _this.buildFilter(predicate.predicate, options);
      if (predicate.isAll) {
        return function (i) {
          let detail = _this.getValue(i, predicate.detailPath);
          if (!detail) {
            return false;
          }

          let result = detailFilter(detail);
          return result.length === detail.length;
        };
      } else if (predicate.isAny) {
        return function (i) {
          let detail = _this.getValue(i, predicate.detailPath);
          if (!detail) {
            return false;
          }

          let result = detailFilter(detail);
          return result.length > 0;
        };
      } else {
        throw new Error(`Unsupported detail operation.`);
      }
    }

    if (predicate instanceof ComplexPredicate) {
      let filterFunctions = predicate.predicates.map(predicate => _this.getAttributeFilterFunction(predicate, options));
      switch (predicate.condition) {
        case Condition.And:
          return function (i) {
            let check = true;
            for (let funcIndex = 0; funcIndex < filterFunctions.length; funcIndex++) {
              check &= filterFunctions[funcIndex](i);
              if (!check) {
                break;
              }
            }

            return check;
          };

        case Condition.Or:
          return function (i) {
            let check = false;
            for (let funcIndex = 0; funcIndex < filterFunctions.length; funcIndex++) {
              check |= filterFunctions[funcIndex](i);
              if (check) {
                break;
              }
            }

            return check;
          };

        default:
          throw new Error(`Unsupported condition '${predicate.condition}'.`);
      }
    }

    throw new Error(`Unsupported predicate '${predicate}'.`);
  }

  /**
   Loads value from object by specified attribute path.

  @param {Object} item Object for load value.
  @param {String} attributePath The path to the attribute.
  @returns {*|undefined} Value of attribute or `undefined`.
  */
  getValue(item, attributePath) {
    let attributes = Information.parseAttributePath(attributePath);
    let search = item;
    for (let i = 0; i < attributes.length; i++) {
      search = search[attributes[i]];
      if (!search) {
        // Don't return constant null / undefined - we need to distinguish them.
        return search;
      }

      if (typeof search === 'object' && !(search instanceof Array) && !attributes[i + 1]) {
        // TODO: In fact, it can not only be `id`.
        return search.id;
      }
    }

    return search;
  }

  /**
   * Returns complex filter function for `and` condition.
   * Result function filters array of objects and returns those, for which
   * all attribute filter functions returned `true`.
   * Result function uses short circuit logic ([wiki](https://en.wikipedia.org/wiki/Short-circuit_evaluation)).
   *
   * @param {Function[]} filterFunctions Array of attribute filter functions.
   * @returns {Function} Complex filter function for `or` condition.
   */
  getFilterFunctionAnd(filterFunctions) {
    return function (data) {
      let result = [];
      for (let itemIndex = 0; itemIndex < data.length; itemIndex++) {
        let check = true;
        for (let funcIndex = 0; funcIndex < filterFunctions.length; funcIndex++) {
          check &= filterFunctions[funcIndex](data[itemIndex]);
          if (!check) {
            break;
          }
        }

        if (check) {
          result.push(data[itemIndex]);
        }
      }

      return result;
    };
  }

  /**
   * Returns complex filter function for `or` condition.
   * Result function filters array of objects and returns those, for which
   * at least one attribute filter function returned `true`.
   * Result function uses short circuit logic ([wiki](https://en.wikipedia.org/wiki/Short-circuit_evaluation)).
   *
   * @param {Function[]} filterFunctions Array of attribute filter functions.
   * @returns {Function} Complex filter function for `or` condition.
   */
  getFilterFunctionOr(filterFunctions) {
    return function (data) {
      let result = [];
      for (let itemIndex = 0; itemIndex < data.length; itemIndex++) {
        let check = false;
        for (let funcIndex = 0; funcIndex < filterFunctions.length; funcIndex++) {
          check |= filterFunctions[funcIndex](data[itemIndex]);
          if (check) {
            break;
          }
        }

        if (check) {
          result.push(data[itemIndex]);
        }
      }

      return result;
    };
  }
}