import { useCallback, useEffect, useRef, useState } from 'react';
import get from 'lodash/get.js';
import isEqual from 'lodash/isEqual.js';

import { removeEmpty } from '../../util';
import { Exporter, FilterPayload, RaRecord, SortPayload } from '../../types';
import { useResourceContext } from '../../core';
import usePaginationState from '../usePaginationState';
import useSortState from '../useSortState';
import { useRecordSelection } from './useRecordSelection';
import { GetDataOptions, ListControllerResult } from './useListController';
import { flattenObject } from '../../dataProvider/fetch';
import { defaultExporter } from '../../export';

const refetch = () => {
    throw new Error(
        'refetch is not available for a ListContext built from useList based on local data'
    );
};

/**
 * Handle filtering, sorting and pagination on local data.
 *
 * Returns the data and callbacks expected by <ListContext>.
 *
 * @example
 * const data = [
 *     { id: 1, name: 'Arnold' },
 *     { id: 2, name: 'Sylvester' },
 *     { id: 3, name: 'Jean-Claude' },
 * ]
 *
 * const MyComponent = () => {
 *     const listContext = useList({ data });
 *     return (
 *         <ListContextProvider value={listContext}>
 *             <Datagrid>
 *                 <TextField source="id" />
 *                 <TextField source="name" />
 *             </Datagrid>
 *         </ListContextProvider>
 *     );
 * };
 *
 * @param {UseListOptions} props
 * @param {RaRecord[]} props.data An array of records
 * @param {Boolean} props.isFetching: Optional. A boolean indicating whether the data is being loaded
 * @param {Boolean} props.isLoading: Optional. A boolean indicating whether the data has been loaded at least once
 * @param {Error | String} props.error: Optional. The error if any occurred while loading the data
 * @param {Object} props.filter: Optional. An object containing the filters applied on the data
 * @param {Number} props.page: Optional. The initial page index
 * @param {Number} props.perPage: Optional. The initial page size
 * @param {SortPayload} props.sort: Optional. The initial sort (field and order)
 * @param {filterCallback} prop.filterCallback Optional. A function that allows you to make a custom filter
 */
export const useList = <RecordType extends RaRecord = any, ErrorType = Error>(
    props: UseListOptions<RecordType, ErrorType>
): UseListValue<RecordType, ErrorType> => {
    const {
        data,
        error,
        filter = defaultFilter,
        isFetching = false,
        isLoading = false,
        isPaused = false,
        isPending = false,
        isPlaceholderData = false,
        page: initialPage = 1,
        perPage: initialPerPage = 1000,
        sort: initialSort,
        filterCallback = defaultFilterCallback,
        exporter = defaultExporter,
    } = props;
    const resource = useResourceContext(props);

    const [finalItems, setFinalItems] = useState<{
        data?: RecordType[];
        total?: number;
    }>(() => ({
        data,
        total: data ? data.length : undefined,
    }));

    // pagination logic
    const { page, setPage, perPage, setPerPage } = usePaginationState({
        page: initialPage,
        perPage: initialPerPage,
    });

    // sort logic
    const { sort, setSort: setSortState } = useSortState(initialSort);
    const setSort = useCallback(
        (sort: SortPayload) => {
            setSortState(sort);
            setPage(1);
        },
        [setPage, setSortState]
    );

    // selection logic
    const [selectedIds, selectionModifiers] = useRecordSelection(
        resource
            ? {
                  resource,
              }
            : { disableSyncWithStore: true }
    );

    const onUnselectItems = useCallback(
        (fromAllStoreKeys?: boolean) => {
            return selectionModifiers.unselect(selectedIds, fromAllStoreKeys);
        },
        [selectedIds, selectionModifiers]
    );

    // filter logic
    const filterRef = useRef(filter);
    const [displayedFilters, setDisplayedFilters] = useState<{
        [key: string]: boolean;
    }>({});
    const [filterValues, setFilterValues] = useState<{
        [key: string]: any;
    }>(filter);
    const hideFilter = useCallback(
        (filterName: string) => {
            setDisplayedFilters(previousState => {
                const { [filterName]: _, ...newState } = previousState;
                return newState;
            });
            setFilterValues(previousState => {
                const { [filterName]: _, ...newState } = previousState;
                return newState;
            });
        },
        [setDisplayedFilters, setFilterValues]
    );
    const showFilter = useCallback(
        (filterName: string, defaultValue: any) => {
            setDisplayedFilters(previousState => ({
                ...previousState,
                [filterName]: true,
            }));
            setFilterValues(previousState =>
                removeEmpty({
                    ...previousState,
                    [filterName]: defaultValue,
                })
            );
        },
        [setDisplayedFilters, setFilterValues]
    );
    const setFilters = useCallback(
        (filters, displayedFilters = undefined) => {
            setFilterValues(removeEmpty(filters));
            if (displayedFilters) {
                setDisplayedFilters(displayedFilters);
            }
            setPage(1);
        },
        [setDisplayedFilters, setFilterValues, setPage]
    );

    // handle filter prop change
    useEffect(() => {
        if (!isEqual(filter, filterRef.current)) {
            filterRef.current = filter;
            setFilterValues(filter);
        }
    }, [filter]);

    const applyFilterAndSort = useCallback(
        (records: RecordType[]) => {
            let tempData = records;
            if (filterValues) {
                const flattenFilterValues = flattenObject(filterValues);
                tempData = records
                    .filter(record =>
                        Object.entries(flattenFilterValues).every(
                            ([filterName, filterValue]) => {
                                const recordValue = get(record, filterName);
                                const result = Array.isArray(recordValue)
                                    ? Array.isArray(filterValue)
                                        ? recordValue.some(item =>
                                              filterValue.includes(item)
                                          )
                                        : recordValue.includes(filterValue)
                                    : Array.isArray(filterValue)
                                      ? filterValue.includes(recordValue)
                                      : filterName === 'q' // special full-text filter
                                        ? Object.keys(record).some(
                                              key =>
                                                  typeof record[key] ===
                                                      'string' &&
                                                  record[key]
                                                      .toLowerCase()
                                                      .includes(
                                                          (
                                                              filterValue as string
                                                          ).toLowerCase()
                                                      )
                                          )
                                        : filterValue == recordValue; // eslint-disable-line eqeqeq
                                return result;
                            }
                        )
                    )
                    .filter(filterCallback);
            }
            if (sort.field) {
                tempData = tempData.sort((a, b) => {
                    if (get(a, sort.field) > get(b, sort.field)) {
                        return sort.order === 'ASC' ? 1 : -1;
                    }
                    if (get(a, sort.field) < get(b, sort.field)) {
                        return sort.order === 'ASC' ? -1 : 1;
                    }
                    return 0;
                });
            }

            return tempData;
        },
        [filterValues, filterCallback, sort.field, sort.order]
    );

    // We do all the data processing (filtering, sorting, paginating) client-side
    useEffect(
        () => {
            if (isPending || !data) return;
            const filteredAndSorted = applyFilterAndSort(data);
            const filteredLength = filteredAndSorted.length;
            const paginatedData = filteredAndSorted.slice(
                (page - 1) * perPage,
                page * perPage
            );

            setFinalItems({
                data: paginatedData,
                total: filteredLength,
            });
        }, // eslint-disable-next-line react-hooks/exhaustive-deps
        [
            // eslint-disable-next-line react-hooks/exhaustive-deps
            JSON.stringify(data),
            applyFilterAndSort,
            isPending,
            page,
            perPage,
            setFinalItems,
        ]
    );

    const onSelectAll = useCallback(() => {
        const allIds = data?.map(({ id }) => id) || [];
        selectionModifiers.select(allIds);
    }, [data, selectionModifiers]);

    const getData = useCallback(
        async ({ maxResults }: GetDataOptions = {}) => {
            if (isPending || !data) {
                return [];
            }
            const filteredAndSorted = applyFilterAndSort(data);
            if (maxResults != null) {
                return filteredAndSorted.slice(0, maxResults);
            }
            return filteredAndSorted;
        },
        [applyFilterAndSort, data, isPending]
    );

    return {
        sort,
        data: isPending ? undefined : finalItems?.data ?? [],
        defaultTitle: '',
        error: error ?? null,
        displayedFilters,
        exporter,
        filterValues,
        hasNextPage:
            finalItems?.total == null
                ? false
                : page * perPage < finalItems.total,
        hasPreviousPage: page > 1,
        hideFilter,
        isFetching,
        isLoading,
        isPaused,
        isPending,
        isPlaceholderData,
        onSelect: selectionModifiers.select,
        onSelectAll,
        onToggleItem: selectionModifiers.toggle,
        onUnselectItems,
        page,
        perPage,
        resource: '',
        refetch,
        selectedIds,
        setFilters,
        setPage,
        setPerPage,
        setSort,
        showFilter,
        total: finalItems?.total,
        getData,
    } as UseListValue<RecordType, ErrorType>;
};

export interface UseListOptions<
    RecordType extends RaRecord = any,
    ErrorType = Error,
> {
    data?: RecordType[];
    error?: ErrorType | null;
    filter?: FilterPayload;
    isFetching?: boolean;
    isLoading?: boolean;
    isPaused?: boolean;
    isPending?: boolean;
    isPlaceholderData?: boolean;
    page?: number;
    perPage?: number;
    sort?: SortPayload;
    resource?: string;
    filterCallback?: (record: RecordType) => boolean;
    exporter?: Exporter<RecordType> | false;
}

export type UseListValue<
    RecordType extends RaRecord = any,
    ErrorType = Error,
> = ListControllerResult<RecordType, ErrorType>;

const defaultFilter = {};
const defaultFilterCallback = (record: RaRecord) => Boolean(record);
