/**
 * @module data/Store
 */
'use strict';

import _inheritsLoose from "@babel/runtime/helpers/inheritsLoose";
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import { readCookie, writeCookie } from '../utils/cookie';
import TinyEmitter from 'tiny-emitter';
import _ from 'lodash';
import EventBus from '../utils/EventBus';
import { htmlTagRegEx, eventBeforeStoreReload, eventAfterStoreReload } from '../../utils/constants';
import { getHighlightRegExp, searchStringToRegExp } from '../utils/regexp';
/**
 * @alias data/Store:Store
 */

var Store =
/*#__PURE__*/
function (_TinyEmitter) {
  _inheritsLoose(Store, _TinyEmitter);

  /**
   * Example configurations:
   * ```json
   * {
   *     "fields": [],
   *     "fields": {},
   *     "stateName": ""
   * }
   * ```
   *
   * @param {object} params
   */
  function Store(params) {
    var _this;

    _this = _TinyEmitter.call(this) || this;
    _this.primaryKey = 'id';
    _this.storeType = params.storeType;

    _this.setFields(params.fields);

    _this.loaded = false;
    _this.data = [];
    _this.rawData = [];
    _this.totalCount = 0;
    _this.filteredCount = 0;
    _this.filteringFields = _this.fields.filter(function (field) {
      return !!field.textFilterHandler;
    });
    _this.params = {};

    _this.setInitialTextFilter('');

    _this.setInitialFieldsFilter([]); // a property to be used to keep pagination and sorting information


    _this.stateName = params.stateName ? 'state_' + params.stateName : null;

    var stateProps = _this.readState();

    var initialPageSize = undefined;

    if (stateProps && stateProps.pageSize) {
      initialPageSize = stateProps.pageSize;
    }

    _this.setInitialSorting(_this.fields[0].id, 'ASC');

    _this.setInitialPagination(0, initialPageSize);

    _this.textFilterCallback = function (text) {
      if (_this.textFilterPattern === null) {
        return true;
      } else {
        _this.textFilterPattern.lastIndex = 0;
        var clearText = getClearText(text);
        return _this.textFilterPattern.test(clearText);
      }
    };

    var debounceValue = _this.useBackend() ? _this.getBackendDebounceValue() : _this.getClientDebounceValue();
    _this.debouncedInternalLoad = _.debounce(_this.internalLoad, debounceValue);
    return _this;
  }

  var _proto = Store.prototype;

  _proto.clone = function clone() {
    var copiedStore = _.cloneDeep(this);

    copiedStore.e = null; // drop event listeners from copy

    return copiedStore;
  }
  /**
   * Registers the store in Event Bus. This allows remote control over certain store types.
   * @param type
   */
  ;

  _proto.register = function register(type) {
    var _this2 = this;

    this.storeType = type;

    this.onStoreReset = function (storeType, details) {
      if (!storeType || type === storeType) {
        _this2.reload();

        if (details && details.removedIds) {
          _this2.emit('deleteItems', details.removedIds);
        }
      }
    };

    EventBus.on('resetStore', this.onStoreReset);
  }
  /**
   * Unwires store from EventBus and cancels all pending activities
   */
  ;

  _proto.destructor = function destructor() {
    if (this.onStoreReset) {
      EventBus.off('resetStore', this.onStoreReset);
    }

    this.debouncedInternalLoad.cancel();
    this.endLoading();
  }
  /**
   * Returns a field descriptor by ID
   * @param fieldId
   */
  ;

  _proto.getFieldById = function getFieldById(fieldId) {
    return this.fields.filter(function (field) {
      return field.id === fieldId;
    })[0];
  }
  /**
   * Resets page back to zero
   * TODO Should emit change event?
   */
  ;

  _proto.resetCurrentPage = function resetCurrentPage() {
    this.params.currentPage = 0;
  }
  /**
   * @deprecated
   * @param filter
   */
  ;

  _proto.setInitialFieldsFilter = function setInitialFieldsFilter(filter) {
    this.params.customFieldsFilter = filter;
  }
  /**
   * Use {@link data/Store.setTextFilter} instead
   * @deprecated
   * @param searchString
   */
  ;

  _proto.setInitialTextFilter = function setInitialTextFilter(searchString) {
    this.setTextFilter(searchString);
  }
  /**
   * TODO Should emit change event?
   * @param {string} searchString
   */
  ;

  _proto.setTextFilter = function setTextFilter(searchString) {
    this.params.filterByText = searchString;
    this.textFilterPattern = Store.searchStringToRegExp(searchString);
    this.textHighlightPattern = Store.getHighlightRegExp(searchString);
  }
  /**
   * @deprecated
   * @param {string} fieldId
   * @param {string} direction
   */
  ;

  _proto.setInitialSorting = function setInitialSorting(fieldId, direction) {
    if (direction === void 0) {
      direction = 'ASC';
    }

    var state = this.readState(); // use values from state

    if (state) {
      if (state.sortBy && this.getFieldById(state.sortBy)) {
        fieldId = state.sortBy;
      }

      if (state.sortDir) {
        direction = state.sortDir;
      }
    }

    this.params.sorting = {
      fieldId: fieldId,
      direction: direction
    };
  }
  /**
   * @deprecated
   * @param currentPage
   * @param pageSize
   */
  ;

  _proto.setInitialPagination = function setInitialPagination(currentPage, pageSize) {
    if (pageSize === void 0) {
      pageSize = null;
    }

    this.params.currentPage = currentPage;
    this.params.pageSize = pageSize;
  };

  _proto.setItems = function setItems(items, skipConversion) {
    this.startLoading();
    var data = skipConversion ? items : this.convert(items);
    this.pool = Promise.resolve(data);
    this.calculate(data);
  };

  _proto.getItemsCollection = function getItemsCollection(converter) {
    var _this3 = this;

    var items = {};
    var data = this.useBackend() ? this.getData() : this.getRawData();
    data.forEach(function (item) {
      items[item[_this3.primaryKey]] = converter(_.cloneDeep(item));
    });
    return items;
  };

  _proto.extendFormState = function extendFormState(items, converter) {
    return _.merge(this.getItemsCollection(converter), items);
  }
  /**
   * This method will be used if useBackend = true
   * @return {Promise.<Array>}
   */
  ;

  _proto.loadWithParams = function loadWithParams() {
    return Promise.resolve({
      data: [],
      filteredCount: 0,
      totalCount: 0
    });
  }
  /**
   * This method will be used if useBackend = false
   * @return {Promise.<Array>}
   */
  ;

  _proto.loadAll = function loadAll() {
    return Promise.resolve([]);
  };

  _proto.load = function load() {
    this.startLoading();
    this.debouncedInternalLoad();
  }
  /**
   * TODO Add _ before name
   * Emits before load event
   * @protected
   */
  ;

  _proto.startLoading = function startLoading() {
    if (!this.awaiting) {
      this.awaiting = true;
      EventBus.emit(eventBeforeStoreReload, this.storeType);
    }
  }
  /**
   * TODO Add _ before name
   * Emits after load event
   * @protected
   */
  ;

  _proto.endLoading = function endLoading() {
    this.loaded = true;

    if (this.awaiting) {
      this.awaiting = false;
      EventBus.emit(eventAfterStoreReload, this.storeType);
    }
  }
  /**
   * TODO Add _ before name
   * Actual load function that calls backend/frontend loaders and manages calculations
   * @protected
   * @param forced
   */
  ;

  _proto.internalLoad = function internalLoad(forced) {
    var _this4 = this;

    if (this.useBackend()) {
      this.loadWithParams().then(function (_ref) {
        var data = _ref.data,
            filteredCount = _ref.filteredCount,
            totalCount = _ref.totalCount,
            lastPage = _ref.lastPage;
        _this4.data = _this4.convert(data);
        _this4.filteredCount = filteredCount;
        _this4.totalCount = totalCount; // todo move to more common way?

        if (lastPage !== undefined) {
          _this4.lastPage = lastPage;
        }

        _this4.notifyChanges();
      }).catch(function (error) {
        _this4.calculate([]);

        throw error;
      });
    } else {
      if (forced || !this.pool) {
        this.pool = this.loadAll().then(function (data) {
          return _this4.convert(data);
        });
      }

      this.pool.then(function (data) {
        _this4.calculate(data);
      }).catch(function (error) {
        // eslint-disable-next-line
        console.error(error.stack || error);

        _this4.calculate([]);
      });
    }
  };

  _proto.reload = function reload() {
    this.internalLoad(true);
  };

  _proto.convert = function convert(data) {
    var _this5 = this;

    return data.map(function (item) {
      var result = {};

      _this5.fields.forEach(function (field) {
        result[field.id] = field.getter(item);
      });

      return result;
    });
  };

  _proto.setFilteringFields = function setFilteringFields(fields) {
    if (typeof fields === 'string') {
      fields = [].slice.call(arguments, 0);
    }

    this.filteringFields = fields.map(this.getFieldById, this);
  };

  Store.getDefaultComparator = function getDefaultComparator(type) {
    switch (type) {
      case String:
      case 'string':
        return Store.compareStrings;

      case Number:
      case 'number':
        return Store.compareNumbers;

      case 'enum':
        return function (a, b) {
          return a.value - b.value;
        };
      //case 'date': ...

      default:
        return function (a, b) {
          return a - b;
        };
    }
  };

  Store.compareStrings = function compareStrings(a, b, direction) {
    if (!a && !b) {
      return 0;
    }

    if (!a && direction === 'ASC' || !b && direction === 'DESC') {
      return 1;
    }

    if (!b && direction === 'ASC' || !a && direction === 'DESC') {
      return -1;
    }

    return a.localeCompare(b, {}, {
      numeric: true,
      sensitivity: 'base'
    });
  };

  Store.compareNumbers = function compareNumbers(a, b, direction) {
    var aIsValid = typeof a === 'number' && !isNaN(a);
    var bIsValid = typeof b === 'number' && !isNaN(b);

    if (!aIsValid && !bIsValid) {
      return 0;
    }

    if (!aIsValid && direction === 'ASC' || !bIsValid && direction === 'DESC') {
      return 1;
    }

    if (!bIsValid && direction === 'ASC' || !aIsValid && direction === 'DESC') {
      return -1;
    }

    if (a < b) {
      return -1;
    }

    if (a > b) {
      return 1;
    }

    return 0;
  };

  Store.defaultFilter = function defaultFilter(value, callback) {
    return callback(value);
  };

  Store.getDefaultTextFilterHandler = function getDefaultTextFilterHandler(type) {
    switch (type) {
      case 'enum':
        return function (fieldValue, callback) {
          return Store.defaultFilter(fieldValue.description, callback);
        };

      default:
        return Store.defaultFilter;
    }
  };

  Store.getDefaultFieldFilterHandler = function getDefaultFieldFilterHandler(type) {
    switch (type) {
      case 'enum':
        return function (fieldValue, callback) {
          return Store.defaultFilter(fieldValue.value, callback);
        };

      default:
        return Store.defaultFilter;
    }
  };

  Store.getFilterByAction = function getFilterByAction(action, filterValue) {
    switch (action) {
      case 'in':
        return function (value) {
          return _.intersection(Array.isArray(value) ? value : [value], Array.isArray(filterValue) ? filterValue : [filterValue]).length > 0;
        };

      default:
        // eslint-disable-next-line
        return function (value) {
          return value == filterValue;
        };
    }
  } // TODO: ...
  ;

  _proto.useBackend = function useBackend() {
    return false;
  };

  _proto.getClientDebounceValue = function getClientDebounceValue() {
    return 150;
  };

  _proto.getBackendDebounceValue = function getBackendDebounceValue() {
    return 500;
  };

  _proto.setFields = function setFields(fields) {
    var _this6 = this;

    if (fields instanceof Array) {
      this.setFieldsFromArray(fields);
    } else if (fields instanceof Object) {
      this.setFieldsFromObject(fields);
    } else {
      throw new TypeError('Store: argument "fields" should be Object or Array');
    }

    if (!this.fields.some(function (field) {
      return field.id === _this6.primaryKey;
    })) {
      // eslint-disable-next-line
      console.error('Store "%s" cannot find primary key "%s" in fields definition', this.constructor.name, this.primaryKey); //TODO Throw?
    }
  };

  _proto.setFieldsFromArray = function setFieldsFromArray(fields) {
    var _this7 = this;

    if (!fields.length) {
      throw new Error('Store: empty fields declaration');
    }

    this.fields = fields.map(function (field) {
      var type = field.type || 'string';

      if (field.isPrimaryKey) {
        _this7.primaryKey = field.id;
      }

      var res = {
        id: field.id,
        type: type,
        getter: field.getter || function (item) {
          return item[field.id];
        },
        comparator: field.comparator || Store.getDefaultComparator(type),
        textFilterHandler: field.textFilterHandler || Store.getDefaultTextFilterHandler(type),
        fieldFilterHandler: field.fieldFilterHandler || Store.getDefaultFieldFilterHandler(type)
      };

      if (field.field) {
        res.field = field.field;
      }

      return res;
    });
  };

  _proto.setFieldsFromObject = function setFieldsFromObject(fields) {
    this.setFieldsFromArray(Object.keys(fields).map(function (id) {
      var field = {
        id: id
      };

      if (typeof fields[id] === 'object') {
        field.type = getType(fields[id].type);
        field.isPrimaryKey = fields[id].primaryKey || fields[id].isPrimaryKey;
        field.getter = fields[id].getter;
        field.comparator = fields[id].comparator;
        field.textFilterHandler = fields[id].textFilterHandler;
        field.fieldFilterHandler = fields[id].fieldFilterHandler;

        if (fields[id].field) {
          field.field = fields[id].field;
        }
      } else {
        field.type = getType(fields[id]);
      }

      return field;
    }));
  } // TODO rename to setCurrentPageAndReload
  ;

  _proto.setCurrentPage = function setCurrentPage(index) {
    this.params.currentPage = index;
    this.load();
  };

  _proto.setPageSize = function setPageSize(size) {
    this.writeState({
      pageSize: size
    });
    this.params.pageSize = size;
    this.resetCurrentPage();
    this.load();
  } // @private
  ;

  _proto.writeState = function writeState(attrs) {
    if (this.stateName) {
      var state = this.readState();
      state = state ? _.merge(state, attrs) : attrs; // merge with existed

      writeCookie(this.stateName, JSON.stringify(state), '/');
    }
  } // @private
  ;

  _proto.readState = function readState() {
    var state;

    if (this.stateName) {
      var cookieValues = readCookie(this.stateName);
      state = JSON.parse(cookieValues);
    }

    return state;
  };

  _proto.sortBy = function sortBy(fieldId, direction) {
    if (!direction) {
      direction = 'ASC';
    }

    this.params.sorting = {
      fieldId: fieldId,
      direction: direction
    };
    this.writeState({
      sortBy: fieldId,
      sortDir: direction
    });
    this.resetCurrentPage();
    this.load();
  };

  _proto.filterByText = function filterByText(searchString) {
    this.setTextFilter(searchString);
    this.resetCurrentPage();
    this.load();
  };

  _proto.filterByFields = function filterByFields(fields) {
    this.params.customFieldsFilter = fields;
    this.resetCurrentPage();
    this.load();
  };

  _proto.filterByField = function filterByField(fieldName) {
    var currentFilterClean = this.getCurrentFilter(fieldName);
    var fieldFilter = [];

    for (var _len = arguments.length, filter = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
      filter[_key - 1] = arguments[_key];
    }

    if (filter.length === 2) {
      var action = filter[0],
          value = filter[1];
      fieldFilter.push({
        field: fieldName,
        action: action,
        value: value
      });
    } else {
      var _value = filter[0];

      if (typeof _value !== 'object') {
        fieldFilter.push({
          field: fieldName,
          value: _value
        });
      } else {
        fieldFilter = fieldFilter.concat(_value);
        fieldFilter = fieldFilter.map(function (item) {
          return _.extend({
            field: fieldName
          }, item);
        });
      }
    }

    this.filterByFields(currentFilterClean.concat(fieldFilter));
  };

  _proto.resetFieldFilter = function resetFieldFilter(fieldName) {
    this.filterByFields(this.getCurrentFilter(fieldName));
  };

  _proto.getCurrentFilter = function getCurrentFilter(excludedFieldName) {
    return this.params.customFieldsFilter.filter(function (item) {
      return item.field !== excludedFieldName;
    });
  };

  _proto.hasFilters = function hasFilters() {
    return !!(this.params.filterByText || this.params.customFieldsFilter && this.params.customFieldsFilter.length);
  };

  _proto.getFiltered = function getFiltered(list) {
    var _this8 = this;

    // Filter by fields
    if (this.params.customFieldsFilter.length) {
      this.params.customFieldsFilter.forEach(function (filter) {
        list = list.filter(function (item) {
          var field = _this8.getFieldById(filter.field);

          var fieldValue = item[filter.field];
          var operator = Store.getFilterByAction(filter.action, filter.value);
          var isPassed = field.fieldFilterHandler(fieldValue, operator);
          return filter.inverted ? !isPassed : isPassed;
        });
      });
    } // Filter by text


    if (this.params.filterByText.length && this.filteringFields.length) {
      list = list.filter(function (item) {
        return _this8.filteringFields.some(function (field) {
          return field.textFilterHandler(item[field.id], _this8.textFilterCallback);
        }); //TODO Add full item
      });
    }

    return list;
  };

  _proto.getSorted = function getSorted(list) {
    var _this$params$sorting = this.params.sorting,
        fieldId = _this$params$sorting.fieldId,
        direction = _this$params$sorting.direction;
    var comparator = this.getFieldById(fieldId).comparator;
    return mergeSort(list, function (a, b) {
      return comparator(a[fieldId], b[fieldId], direction) * (direction === 'ASC' ? 1 : -1);
    });
  };

  _proto.getSliced = function getSliced(list) {
    this.validateCurrentPage(list);
    var from = this.params.currentPage * this.params.pageSize || 0;
    var to = this.params.pageSize === null ? undefined : from + this.params.pageSize;
    return list.slice(from, to);
  };

  _proto.validateCurrentPage = function validateCurrentPage(list) {
    var maxPageIndex = Math.max(0, Math.ceil(list.length / this.params.pageSize) - 1);
    this.params.currentPage = Math.min(this.params.currentPage, maxPageIndex);
  };

  _proto.calculate = function calculate(data) {
    var hasRawDataChanges = !_.isEqual(this.rawData, data);
    this.totalCount = data.length;
    this.rawData = data;
    var list = this.getFiltered(data);
    this.filteredCount = list.length;
    list = this.getSorted(list);
    list = this.getSliced(list);
    this.data = list;
    this.notifyChanges(hasRawDataChanges);
  };

  _proto.notifyChanges = function notifyChanges(hasRawDataChanges) {
    this.endLoading();
    this.emit('change', this.getData(), hasRawDataChanges);
  };

  _proto.onChange = function onChange(handler, context) {
    this.on('change', handler, context);
  };

  _proto.onDeleteItems = function onDeleteItems(handler, context) {
    this.on('deleteItems', handler, context);
  };

  _proto.getTotalCount = function getTotalCount() {
    return this.totalCount;
  };

  _proto.getIsLastPage = function getIsLastPage() {
    return !!this.lastPage;
  };

  _proto.getFilteredCount = function getFilteredCount() {
    return this.filteredCount;
  };

  _proto.getData = function getData() {
    return this.data;
  };

  _proto.getRawData = function getRawData() {
    if (this.useBackend()) {
      throw new Error('Method getRawData is unavailable for backend store; use getData instead of getRawData');
    }

    return this.rawData;
  };

  _proto.getPrimaryKeys = function getPrimaryKeys() {
    var _this9 = this;

    return this.data.map(function (item) {
      return item[_this9.primaryKey];
    });
  };

  _proto.getCount = function getCount() {
    return this.data.length;
  };

  _proto.getCurrentPage = function getCurrentPage() {
    return this.params.currentPage;
  };

  _proto.getPageSize = function getPageSize() {
    return this.params.pageSize;
  };

  _proto.getSorting = function getSorting() {
    return this.params.sorting;
  };

  _proto.getSortingFieldId = function getSortingFieldId() {
    return this.params.sorting.fieldId;
  };

  _proto.getSortingDirection = function getSortingDirection() {
    return this.params.sorting.direction;
  };

  _proto.isAwaiting = function isAwaiting() {
    return !!this.awaiting;
  };

  _proto.isLoaded = function isLoaded() {
    return !!this.loaded;
  };

  return Store;
}(TinyEmitter); // We need this for stable sorting, because default Array.prototype.sort provides unstable sort results in Chrome


_defineProperty(Store, "searchStringToRegExp", searchStringToRegExp);

_defineProperty(Store, "getHighlightRegExp", getHighlightRegExp);

export { Store as default };

var mergeSort = function () {
  function mergeSortInner(arr, compare) {
    var length = arr.length;
    var middle = Math.floor(length / 2); // Default "compare" method has been removed since we do not have any method calls without second argument.

    if (length < 2) {
      return arr;
    }

    return merge(mergeSortInner(arr.slice(0, middle), compare), mergeSortInner(arr.slice(middle, length), compare), compare);
  }

  function merge(left, right, compare) {
    var result = [];

    while (left.length > 0 || right.length > 0) {
      if (left.length > 0 && right.length > 0) {
        if (compare(left[0], right[0]) <= 0) {
          result.push(left[0]);
          left = left.slice(1);
        } else {
          result.push(right[0]);
          right = right.slice(1);
        }
      } else if (left.length > 0) {
        result.push(left[0]);
        left = left.slice(1);
      } else if (right.length > 0) {
        result.push(right[0]);
        right = right.slice(1);
      }
    }

    return result;
  }

  return mergeSortInner;
}();

function getType(type) {
  switch (type) {
    case 'number':
    case Number:
      return 'number';

    case 'string':
    case String:
      return 'string';

    case 'boolean':
    case Boolean:
      return 'boolean';

    default:
      return 'custom';
  }
}

function getClearText(text) {
  return String(text).replace(htmlTagRegEx, '');
}