import { Chance } from 'chance';
import orderBy from 'lodash/orderBy';
import shuffle from 'lodash/shuffle';
import { EventEmitter } from 'events';
import { hashQueryKey } from 'react-query/core';
export class InMemoryBackend {
    constructor(params) {
        this.events = new EventEmitter();
        this.chance = new Chance();
        this.fetchData = async (query) => {
            const fetchDataStartPromises = this.events
                .listeners('fetchDataStart')
                .map((listener) => listener(query));
            if (fetchDataStartPromises.length) {
                await Promise.all(fetchDataStartPromises);
            }
            await this.simulateAsyncDelay();
            return this.fetchDataSync(query);
        };
        this.fetchTotal = async (query) => {
            const fetchTotalStartPromises = this.events
                .listeners('fetchTotalStart')
                .map((listener) => listener(query));
            if (fetchTotalStartPromises.length) {
                await Promise.all(fetchTotalStartPromises);
            }
            await this.simulateAsyncDelay();
            return this.fetchDataSync({ ...query, limit: 1 }).total ?? 0;
        };
        this.deleteMany = async (items, params) => {
            await this.simulateAsyncDelay(params);
            items.forEach((item) => this.map.delete(this.itemKey(item)));
        };
        this.update = async (item) => {
            await this.simulateAsyncDelay();
            this.map.set(this.itemKey(item), item);
        };
        this.categoriesOrdinal = new Map();
        this.elementAt = (index) => this.values[index];
        this.randomItem = (predicate = (_) => true) => {
            const index = Math.floor(Math.random() * this.available);
            return {
                index,
                item: shuffle(this.values).find(predicate),
            };
        };
        this.enableTotal = params.enableTotal ?? true;
        this.createOne = params.createOne;
        this.itemKey = params.itemKey;
        this.paginationMode = params.paginationMode;
        this.predicate = params.predicate ?? (() => () => true);
        this.orderBy = params.orderBy;
        this.projection = params.projection;
        this.initialTotal = params.total;
        this.delay = params.delay ?? { min: 0, max: 5 };
        this.excludeOrdinalFilters = params.excludeOrdinalFilters ?? [];
        this.map = new Map(this.newList(params.total).map((item) => [this.itemKey(item), item]));
    }
    async simulateAsyncDelay({ delay, throwError } = {}) {
        const { chance } = this;
        await new Promise((resolve) => setTimeout(resolve, chance.integer(delay ?? this.delay))); // simulate async
        await throwError?.();
    }
    newList(length) {
        return new Array(length)
            .fill(null)
            .map((_, index) => this.createOne(index));
    }
    fetchDataSync(query) {
        const { itemKey, predicate, orderBy: getOrderBy, paginationMode } = this;
        const ordinal = this._getOrdinal(query.filters ?? {});
        const allItems = this.values;
        const orders = getOrderBy?.(query);
        let itemsAfterOrdering;
        if (orders?.length) {
            itemsAfterOrdering = orderBy(allItems, orders.map(({ fieldName }) => fieldName), orders.map(({ order }) => order));
        }
        else if (ordinal.size) {
            itemsAfterOrdering = orderBy(allItems, (item) => ordinal.get(itemKey(item)), ['asc']);
        }
        else {
            itemsAfterOrdering = allItems;
        }
        const itemsAfterFiltering = itemsAfterOrdering.filter(predicate(query));
        let offset;
        if (paginationMode === 'cursor') {
            offset = query.cursor
                ? itemsAfterFiltering.findIndex((value) => itemKey(value) === query.cursor)
                : 0;
        }
        else {
            offset = query.cursor
                ? itemsAfterFiltering.findIndex((value) => itemKey(value) === query.cursor)
                : query.offset ?? 0;
        }
        const itemsAfterSlicing = itemsAfterFiltering.slice(offset, offset + query.limit);
        const cursorItem = itemsAfterFiltering[offset + query.limit];
        const itemsAfterFieldsProjection = this.projection
            ? itemsAfterSlicing.map(this.projection(query))
            : itemsAfterSlicing;
        return {
            items: itemsAfterFieldsProjection,
            ...(this.enableTotal
                ? { total: itemsAfterFiltering.length, available: allItems.length }
                : {}),
            cursor: cursorItem && itemKey(cursorItem),
        };
    }
    async deleteAll(params = {}) {
        const { filters = {} } = params;
        await this.simulateAsyncDelay(params);
        const { predicate, itemKey } = this;
        const isItemDeleted = predicate({
            filters,
            limit: 1,
        });
        for (const item of this.values) {
            if (isItemDeleted(item)) {
                this.map.delete(itemKey(item));
            }
        }
        this.events.emit('deleteAll');
    }
    clear() {
        this.map.clear();
    }
    async updateMany(items, params = {}) {
        await this.simulateAsyncDelay(params);
        for (const item of items) {
            this.map.set(this.itemKey(item), item);
        }
        this.events.emit('updatedMany');
    }
    _getOrdinal(filters) {
        const { categoriesOrdinal, excludeOrdinalFilters } = this;
        const category = hashQueryKey([
            {
                filters: Object.fromEntries(Object.entries(filters).filter(([key]) => !excludeOrdinalFilters.includes(key))),
            },
        ]);
        const ordinal = categoriesOrdinal.get(category) ?? new Map();
        if (!categoriesOrdinal.has(category)) {
            categoriesOrdinal.set(category, ordinal);
        }
        return ordinal;
    }
    async move(events, params = {}) {
        const { itemKey } = this;
        const moveStartPromises = this.events
            .listeners('moveStart')
            .map((listener) => listener(events));
        if (moveStartPromises.length) {
            await Promise.all(moveStartPromises);
        }
        await this.simulateAsyncDelay(params);
        for (const { from, after, filters } of events) {
            const ordinal = this._getOrdinal(filters);
            const array = ordinal.size
                ? orderBy(this.values.slice(), (item) => ordinal.get(itemKey(item)), [
                    'asc',
                ])
                : this.values.slice();
            const fromKey = itemKey(from.item);
            const toKey = after != null ? itemKey(after.item) : null;
            const fromIndex = array.findIndex((item) => fromKey === itemKey(item));
            if (fromIndex !== -1) {
                array.splice(fromIndex, 1);
            }
            if (toKey != null) {
                const toIndex = array.findIndex((item) => toKey === itemKey(item));
                if (toIndex !== -1) {
                    array.splice(toIndex + 1, 0, from.item);
                }
            }
            else {
                array.splice(0, 0, from.item);
            }
            ordinal.clear();
            for (let i = 0, len = array.length; i < len; i++) {
                ordinal.set(itemKey(array[i]), i);
            }
        }
    }
    async updateAll(patch, params = {}) {
        const { filters = {} } = params;
        await this.simulateAsyncDelay(params);
        const { predicate } = this;
        const itemsAfterFiltering = this.values.filter(predicate({
            filters,
            limit: 1,
        }));
        const patchedItems = itemsAfterFiltering.map((value) => ({
            ...value,
            ...patch(value),
        }));
        for (const item of patchedItems) {
            this.map.set(this.itemKey(item), item);
        }
        this.events.emit('updateAll');
    }
    async unshift(newItems) {
        await this.simulateAsyncDelay();
        const map = new Map();
        for (const item of newItems) {
            map.set(this.itemKey(item), item);
        }
        for (const [key, value] of Array.from(this.map.entries())) {
            map.set(key, value);
        }
        this.map = map;
    }
    async push(newItems) {
        await this.simulateAsyncDelay();
        for (const item of newItems) {
            this.map.set(this.itemKey(item), item);
        }
    }
    getRandomItems(count, { limit, page }) {
        const { chance, values } = this;
        const offset = (page - 1) * limit;
        return chance.pickset(values.slice(offset, offset + limit), count);
    }
    get values() {
        return Array.from(this.map.values());
    }
    get available() {
        return this.map.size;
    }
    get(key) {
        if (key == null) {
            return undefined;
        }
        return this.map.get(key);
    }
}
//# sourceMappingURL=InMemoryBackend.js.map