"use strict";
var __extends = (this && this.__extends) || (function () {
    var extendStatics = function (d, b) {
        extendStatics = Object.setPrototypeOf ||
            ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
            function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
        return extendStatics(d, b);
    };
    return function (d, b) {
        extendStatics(d, b);
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
var utils_1 = require("../../utils");
var tiny_emitter_1 = __importDefault(require("tiny-emitter"));
var lodash_1 = __importDefault(require("lodash"));
var EventBus_1 = __importDefault(require("../utils/EventBus"));
var constants_1 = require("../../utils/constants");
var regexp_1 = require("../utils/regexp");
var Store = /** @class */ (function (_super) {
    __extends(Store, _super);
    /**
     * Example configurations:
     * ```json
     * {
     *     "fields": [],
     *     "fields": {},
     *     "stateName": ""
     * }
     * ```
     *
     * @param {object} params
     */
    function Store(params) {
        var _this = _super.call(this) || this;
        _this.params = {};
        _this.fields = [];
        _this.convertField = function (field) {
            var type = field.type || 'string';
            if (field.isPrimaryKey) {
                _this.primaryKey = field.id;
            }
            var result = {
                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) {
                result.field = field.field;
            }
            return result;
        };
        _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;
            }
            _this.textFilterPattern.lastIndex = 0;
            var clearText = getClearText(text);
            return _this.textFilterPattern.test(clearText);
        };
        var debounceValue = _this.useBackend()
            ? _this.getBackendDebounceValue()
            : _this.getClientDebounceValue();
        _this.debouncedInternalLoad = lodash_1.default.debounce(_this.internalLoad, debounceValue);
        return _this;
    }
    Store.prototype.clone = function () {
        var copiedStore = lodash_1.default.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
     */
    Store.prototype.register = function (type) {
        var _this = this;
        this.storeType = type;
        this.onStoreReset = function (storeType, details) {
            if (!storeType || type === storeType) {
                _this.reload();
                if (details && details.removedIds) {
                    _this.emit('deleteItems', details.removedIds);
                }
            }
        };
        EventBus_1.default.on('resetStore', this.onStoreReset);
    };
    /**
     * Unwires store from EventBus and cancels all pending activities
     */
    Store.prototype.destructor = function () {
        if (this.onStoreReset) {
            EventBus_1.default.off('resetStore', this.onStoreReset);
        }
        this.debouncedInternalLoad.cancel();
        this.endLoading();
    };
    /**
     * Returns a field descriptor by ID
     * @param fieldId
     */
    Store.prototype.getFieldById = function (fieldId) {
        return this.fields.filter(function (field) { return field.id === fieldId; })[0];
    };
    /**
     * Resets page back to zero
     * TODO Should emit change event?
     */
    Store.prototype.resetCurrentPage = function () {
        this.params.currentPage = 0;
    };
    /**
     * @deprecated
     * @param filter
     */
    Store.prototype.setInitialFieldsFilter = function (filter) {
        this.params.customFieldsFilter = filter;
    };
    /**
     * Use {@link data/Store.setTextFilter} instead
     * @deprecated
     * @param searchString
     */
    Store.prototype.setInitialTextFilter = function (searchString) {
        this.setTextFilter(searchString);
    };
    /**
     * TODO Should emit change event?
     * @param {string} searchString
     */
    Store.prototype.setTextFilter = function (searchString) {
        this.params.filterByText = searchString;
        this.textFilterPattern = Store.searchStringToRegExp(searchString);
        this.textHighlightPattern = Store.getHighlightRegExp(searchString);
    };
    /**
     * @deprecated
     * @param {string} fieldId
     * @param {string} direction
     */
    Store.prototype.setInitialSorting = function (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
     */
    Store.prototype.setInitialPagination = function (currentPage, pageSize) {
        if (pageSize === void 0) { pageSize = null; }
        this.params.currentPage = currentPage;
        this.params.pageSize = pageSize;
    };
    Store.prototype.setItems = function (items, skipConversion) {
        this.startLoading();
        var data = skipConversion ? items : this.convert(items);
        this.pool = Promise.resolve(data);
        this.calculate(data);
    };
    Store.prototype.getItemsCollection = function (converter) {
        var _this = this;
        var items = {};
        var data = this.useBackend() ? this.getData() : this.getRawData();
        data.forEach(function (item) {
            items[item[_this.primaryKey]] = converter(lodash_1.default.cloneDeep(item));
        });
        return items;
    };
    // FIXME add more exact types
    Store.prototype.extendFormState = function (items, converter) {
        return lodash_1.default.merge(this.getItemsCollection(converter), items);
    };
    /**
     * This method will be used if useBackend = true
     * @return {Promise.<Array>}
     */
    Store.prototype.loadWithParams = function () {
        return Promise.resolve({ data: [], filteredCount: 0, totalCount: 0 });
    };
    /**
     * This method will be used if useBackend = false
     * @return {Promise.<Array>}
     */
    Store.prototype.loadAll = function () {
        return Promise.resolve([]);
    };
    Store.prototype.load = function () {
        this.startLoading();
        this.debouncedInternalLoad();
    };
    /**
     * TODO Add _ before name
     * Emits before load event
     * @protected
     */
    Store.prototype.startLoading = function () {
        if (!this.awaiting) {
            this.awaiting = true;
            EventBus_1.default.emit(constants_1.eventBeforeStoreReload, this.storeType);
        }
    };
    /**
     * TODO Add _ before name
     * Emits after load event
     * @protected
     */
    Store.prototype.endLoading = function () {
        this.loaded = true;
        if (this.awaiting) {
            this.awaiting = false;
            EventBus_1.default.emit(constants_1.eventAfterStoreReload, this.storeType);
        }
    };
    /**
     * TODO Add _ before name
     * Actual load function that calls backend/frontend loaders and manages calculations
     * @protected
     * @param forced
     */
    Store.prototype.internalLoad = function (forced) {
        var _this = this;
        if (this.useBackend()) {
            this.loadWithParams()
                .then(function (_a) {
                var data = _a.data, filteredCount = _a.filteredCount, totalCount = _a.totalCount, lastPage = _a.lastPage;
                _this.data = _this.convert(data);
                _this.filteredCount = filteredCount;
                _this.totalCount = totalCount;
                // todo move to more common way?
                if (lastPage !== undefined) {
                    _this.lastPage = lastPage;
                }
                _this.notifyChanges();
            })
                .catch(function (error) {
                _this.calculate([]);
                throw error;
            });
        }
        else {
            if (forced || !this.pool) {
                this.pool = this.loadAll().then(function (data) {
                    return _this.convert(data);
                });
            }
            this.pool
                .then(function (data) {
                _this.calculate(data);
            })
                .catch(function (error) {
                // eslint-disable-next-line
                console.error(error.stack || error);
                _this.calculate([]);
            });
        }
    };
    Store.prototype.reload = function () {
        this.internalLoad(true);
    };
    Store.prototype.convert = function (data) {
        var _this = this;
        return data.map(function (item) {
            var result = {};
            _this.fields.forEach(function (field) {
                result[field.id] = field.getter(item);
            });
            return result;
        });
    };
    Store.prototype.setFilteringFields = function (fields) {
        var restfields = [];
        for (var _i = 1; _i < arguments.length; _i++) {
            restfields[_i - 1] = arguments[_i];
        }
        if (typeof fields === 'string') {
            fields = [].slice.call(arguments, 0);
        }
        this.filteringFields = fields.map(this.getFieldById, this);
    };
    Store.getDefaultComparator = function (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 (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;
        }
        a = a.toLowerCase();
        b = b.toLowerCase();
        if (a < b) {
            return -1;
        }
        if (a > b) {
            return 1;
        }
        return 0;
    };
    Store.compareNumbers = function (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 (value, callback) {
        return callback(value);
    };
    Store.getDefaultTextFilterHandler = function (type) {
        switch (type) {
            case 'enum':
                return function (fieldValue, callback) {
                    return Store.defaultFilter(fieldValue.description, callback);
                };
            default:
                return Store.defaultFilter;
        }
    };
    Store.getDefaultFieldFilterHandler = function (type) {
        switch (type) {
            case 'enum':
                return function (fieldValue, callback) { return Store.defaultFilter(fieldValue.value, callback); };
            default:
                return Store.defaultFilter;
        }
    };
    Store.getFilterByAction = function (action, filterValue) {
        switch (action) {
            case 'in':
                return function (value) {
                    return lodash_1.default.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: ...
    Store.prototype.useBackend = function () {
        return false;
    };
    Store.prototype.getClientDebounceValue = function () {
        return 150;
    };
    Store.prototype.getBackendDebounceValue = function () {
        return 500;
    };
    Store.prototype.setFields = function (fields) {
        var _this = 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 === _this.primaryKey; })) {
            // eslint-disable-next-line
            console.error('Store "%s" cannot find primary key "%s" in fields definition', this.constructor.name, this.primaryKey); //TODO Throw?
        }
    };
    Store.prototype.setFieldsFromArray = function (fields) {
        if (!fields.length) {
            throw new Error('Store: empty fields declaration');
        }
        this.fields = fields.map(this.convertField);
    };
    Store.prototype.addField = function (field) {
        this.fields.push(this.convertField(field));
    };
    Store.prototype.setFieldsFromObject = function (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
    Store.prototype.setCurrentPage = function (index) {
        this.params.currentPage = index;
        this.load();
    };
    Store.prototype.setPageSize = function (size) {
        this.writeState({ pageSize: size });
        this.params.pageSize = size;
        this.resetCurrentPage();
        this.load();
    };
    Store.prototype.writeState = function (attrs) {
        if (this.stateName) {
            var state = this.readState();
            state = state ? lodash_1.default.merge(state, attrs) : attrs; // merge with existed
            utils_1.writeCookie(this.stateName, JSON.stringify(state), '/');
        }
    };
    Store.prototype.readState = function () {
        var state;
        if (this.stateName) {
            var cookieValues = utils_1.readCookie(this.stateName);
            state = JSON.parse(cookieValues);
        }
        return state;
    };
    Store.prototype.sortBy = function (fieldId, direction) {
        if (!direction) {
            direction = 'ASC';
        }
        this.params.sorting = {
            fieldId: fieldId,
            direction: direction
        };
        this.writeState({
            sortBy: fieldId,
            sortDir: direction
        });
        this.resetCurrentPage();
        this.load();
    };
    Store.prototype.filterByText = function (searchString) {
        this.setTextFilter(searchString);
        this.resetCurrentPage();
        this.load();
    };
    Store.prototype.filterByFields = function (fields) {
        this.params.customFieldsFilter = fields;
        this.resetCurrentPage();
        this.load();
    };
    Store.prototype.filterByField = function (fieldName) {
        var filter = [];
        for (var _i = 1; _i < arguments.length; _i++) {
            filter[_i - 1] = arguments[_i];
        }
        var currentFilterClean = this.getCurrentFilter(fieldName);
        var fieldFilter = [];
        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 lodash_1.default.extend({ field: fieldName }, item); });
            }
        }
        this.filterByFields(currentFilterClean.concat(fieldFilter));
    };
    Store.prototype.resetFieldFilter = function (fieldName) {
        this.filterByFields(this.getCurrentFilter(fieldName));
    };
    Store.prototype.getCurrentFilter = function (excludedFieldName) {
        return this.params.customFieldsFilter.filter(function (item) { return item.field !== excludedFieldName; });
    };
    Store.prototype.hasFilters = function () {
        return !!(this.params.filterByText ||
            (this.params.customFieldsFilter && this.params.customFieldsFilter.length));
    };
    Store.prototype.getFiltered = function (list) {
        var _this = this;
        // Filter by fields
        if (this.params.customFieldsFilter.length) {
            this.params.customFieldsFilter.forEach(function (filter) {
                list = list.filter(function (item) {
                    var field = _this.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 _this.filteringFields.some(function (field) {
                    return field.textFilterHandler(item[field.id], _this.textFilterCallback);
                }); //TODO Add full item
            });
        }
        return list;
    };
    Store.prototype.getSorted = function (list) {
        var _a = this.params.sorting, fieldId = _a.fieldId, direction = _a.direction;
        var comparator = this.getFieldById(fieldId).comparator;
        return mergeSort(list, function (a, b) {
            return comparator(a[fieldId], b[fieldId], direction) * (direction === 'ASC' ? 1 : -1);
        });
    };
    Store.prototype.getSliced = function (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);
    };
    Store.prototype.validateCurrentPage = function (list) {
        var maxPageIndex = Math.max(0, Math.ceil(list.length / this.params.pageSize) - 1);
        this.params.currentPage = Math.min(this.params.currentPage, maxPageIndex);
    };
    Store.prototype.calculate = function (data) {
        var hasRawDataChanges = !lodash_1.default.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);
    };
    Store.prototype.notifyChanges = function (hasRawDataChanges) {
        if (hasRawDataChanges === void 0) { hasRawDataChanges = false; }
        this.endLoading();
        this.emit('change', this.getData(), hasRawDataChanges);
    };
    Store.prototype.onChange = function (handler, context) {
        if (context === void 0) { context = null; }
        this.on('change', handler, context);
    };
    Store.prototype.onDeleteItems = function (handler, context) {
        this.on('deleteItems', handler, context);
    };
    Store.prototype.getTotalCount = function () {
        return this.totalCount;
    };
    Store.prototype.getIsLastPage = function () {
        return !!this.lastPage;
    };
    Store.prototype.getFilteredCount = function () {
        return this.filteredCount;
    };
    Store.prototype.getData = function () {
        return this.data;
    };
    Store.prototype.getRawData = function () {
        if (this.useBackend()) {
            throw new Error('Method getRawData is unavailable for backend store; use getData instead of getRawData');
        }
        return this.rawData;
    };
    Store.prototype.getPrimaryKeys = function () {
        var _this = this;
        return this.data.map(function (item) { return item[_this.primaryKey]; });
    };
    Store.prototype.getCount = function () {
        return this.data.length;
    };
    Store.prototype.getCurrentPage = function () {
        return this.params.currentPage;
    };
    Store.prototype.getPageSize = function () {
        return this.params.pageSize;
    };
    Store.prototype.getSorting = function () {
        return this.params.sorting;
    };
    Store.prototype.getSortingFieldId = function () {
        return this.params.sorting.fieldId;
    };
    Store.prototype.getSortingDirection = function () {
        return this.params.sorting.direction;
    };
    Store.prototype.isAwaiting = function () {
        return !!this.awaiting;
    };
    Store.prototype.isLoaded = function () {
        return !!this.loaded;
    };
    Store.searchStringToRegExp = regexp_1.searchStringToRegExp;
    Store.getHighlightRegExp = regexp_1.getHighlightRegExp;
    return Store;
}(tiny_emitter_1.default));
exports.Store = Store;
// We need this for stable sorting, because default Array.prototype.sort provides unstable sort results in Chrome
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(constants_1.htmlTagRegEx, '');
}
exports.default = Store; // for compatibility
//# sourceMappingURL=Store.js.map