import { queryStatus, } from '../model';
import { action, computed, createAtom, makeObservable, observable } from 'mobx';
import flatMap from 'lodash/flatMap';
export class QueryResultState {
    get pages() {
        this._pagesAtom.reportObserved();
        return this._pages.processed;
    }
    get _keyedItemsMap() {
        const { keyedItems } = this;
        const map = new Map();
        for (const keyedItem of keyedItems) {
            map.set(keyedItem.key, keyedItem);
        }
        return map;
    }
    constructor(params) {
        this._pagesAtom = createAtom('QueryResultState.pages');
        this._pages = { processed: [], unprocessed: [] };
        this._totals = { processed: 0, unprocessed: 0 };
        this._supportTotal = false;
        this.available = null;
        this.status = {
            ...queryStatus.idle,
            error: null,
            isFetching: false,
        };
        this.setNewPages = action((pages) => {
            this._pages = pages;
            this._pagesAtom.reportChanged();
        });
        this.itemKey = params.itemKey;
        this.originQuery = params.originQuery;
        this.fetchTotal = params.fetchTotal;
        makeObservable(this, {
            items: computed({
                keepAlive: true,
            }),
            keyedItems: computed({
                keepAlive: true,
            }),
            _keyedItemsMap: computed,
            pages: computed,
            size: computed,
            itemsKeys: computed,
            fetchedAll: computed,
            isFetching: computed,
            isEmptyAndNotFetching: computed,
            asComputed: computed,
            total: computed,
            hasAvailableItems: computed,
            _totals: observable.ref,
            _supportTotal: observable.ref,
            setTotals: action,
            available: observable.ref,
            status: observable.ref,
            originQuery: observable.ref,
            changeItems: action,
            totalStatus: computed,
            slice: action,
        });
    }
    init() {
        const disposers = [this.fetchTotal?.init()];
        return () => {
            disposers.forEach((disposer) => disposer?.());
        };
    }
    get errorStatus() {
        const { status } = this;
        return status.isError ? status : null;
    }
    get keyedItems() {
        this._pagesAtom.reportObserved();
        const { pages, itemKey } = this;
        let currentIndex = 0;
        return flatMap(pages, (page, pageIndex) => {
            return page.map((item, indexWithinPage) => {
                const key = itemKey(item);
                return {
                    key,
                    id: key,
                    item,
                    index: currentIndex++,
                    pageIndex,
                    indexWithinPage,
                };
            });
        });
    }
    get itemsKeys() {
        return this.keyedItems.map(({ key }) => key);
    }
    get isEmptyAndNotFetching() {
        const { status, keyedItems } = this;
        return status.isSuccess && !status.isFetching && !keyedItems.length;
    }
    get isEmpty() {
        const { keyedItems } = this;
        return keyedItems.length === 0;
    }
    get items() {
        this._pagesAtom.reportObserved();
        const { pages } = this;
        return flatMap(pages);
    }
    get total() {
        return this.fetchTotal ? this.fetchTotal.total : this._totals.processed;
    }
    get totalStatus() {
        return this.fetchTotal ? this.fetchTotal.status : this.status.status;
    }
    get supportTotal() {
        return !!this.fetchTotal || this._supportTotal;
    }
    get fetchedAll() {
        const { status, items, total } = this;
        return !status.isLoading && total <= items.length;
    }
    get isFetching() {
        return this.status.isFetching;
    }
    get(key) {
        this._pagesAtom.reportObserved();
        return this._keyedItemsMap.get(key);
    }
    has(key) {
        return this._keyedItemsMap.has(key);
    }
    createRollbackFn(updates) {
        return () => {
            const { itemKey, _keyedItemsMap, pages } = this;
            for (const { newItem, prevItem } of updates) {
                const key = itemKey(newItem);
                const keyedItem = _keyedItemsMap.get(key);
                // the current item is still the one that was recently updated
                if (keyedItem != null) {
                    const { pageIndex, indexWithinPage } = keyedItem;
                    const page = pages[pageIndex];
                    if (page[indexWithinPage] === newItem) {
                        this.changeItems([prevItem]);
                        return true;
                    }
                }
            }
            return false;
        };
    }
    changeItems(newItems) {
        const { itemKey, _pages, _pagesAtom, keyedItems } = this;
        const updates = [];
        for (const newItem of newItems) {
            const key = itemKey(newItem);
            const keyedItem = keyedItems.find((item) => item.key === key);
            if (keyedItem != null) {
                const { pageIndex, indexWithinPage } = keyedItem;
                const processedPage = _pages.processed[pageIndex];
                // this mutates react-query cache since `page` is the same reference of the cached data
                // this behavior is intentional because the strategy is to assume the server will be update correctly in the next invalidation
                // until then, we simulate locally (cache) what we expect the server state eventually to be
                const prevItem = processedPage[indexWithinPage];
                processedPage[indexWithinPage] = newItem;
                _pages.unprocessed.forEach((page) => {
                    const itemIndex = page.findIndex((item) => itemKey(item) === key);
                    if (itemIndex !== -1) {
                        page[itemIndex] = newItem;
                    }
                });
                updates.push({ prevItem, newItem });
            }
        }
        if (updates.length) {
            _pagesAtom.reportChanged();
            return {
                rollback: this.createRollbackFn(updates),
            };
        }
        return undefined;
    }
    addItems(newItems) {
        const { _pages, _pagesAtom } = this;
        _pages.processed = [newItems, ..._pages.processed];
        _pages.unprocessed = [newItems, ..._pages.unprocessed];
        _pagesAtom.reportChanged();
        return {
            rollback: () => {
                _pages.processed = _pages.processed.slice(1);
                _pages.unprocessed = _pages.unprocessed.slice(1);
                _pagesAtom.reportChanged();
            },
        };
    }
    clear() {
        this.setNewPages({ processed: [], unprocessed: [] });
    }
    addPages(morePages) {
        const { _pages } = this;
        this.setNewPages({
            unprocessed: [..._pages.unprocessed, ...morePages.unprocessed],
            processed: [..._pages.processed, ...morePages.processed],
        });
    }
    setTotals(totals) {
        this._totals = totals;
    }
    setSupportTotal(supportTotal) {
        this._supportTotal = supportTotal;
    }
    setPages(pages) {
        this.setNewPages(pages);
    }
    slice(start, end) {
        const { _pages } = this;
        this.setNewPages({
            unprocessed: _pages.unprocessed.slice(start, end),
            processed: _pages.processed.slice(start, end),
        });
    }
    get hasAvailableItems() {
        const { originQuery: { hasNonPersistentActiveFilters }, available, total, size, } = this;
        // if `available` info is missing, must assume that there are available items if non persistent filters are active
        return available != null
            ? available > 0
            : hasNonPersistentActiveFilters || total > 0 || size > 0;
    }
    get asComputed() {
        const { total, available } = this;
        return {
            total,
            available: available ?? 0,
        };
    }
    get size() {
        return this.items.length;
    }
}
//# sourceMappingURL=QueryResultState.js.map