import { useRef } from 'react';
import isEqual from 'lodash.isequal';
import { isIncluded, isListViewGroup, isMultiValue } from './core';
import { itemMatch } from './search';
import type {
    ListViewItem,
    ListViewGroup,
    ListViewOptions,
    ListViewValue,
    ListViewApiValue,
    ListViewOptionsLoadResult,
    ListViewFlatOption,
} from '../types';

export function isSingleSelection<T extends ListViewItem>(
    optionOrGroup?: T[] | ListViewGroup<T>[]
): optionOrGroup is T[] {
    return !!optionOrGroup && !isListViewGroup(optionOrGroup[0]);
}

export function isGroupSelection<T extends ListViewItem>(
    optionOrGroup?: T[] | ListViewGroup<T>[]
): optionOrGroup is ListViewGroup<T>[] {
    return !!optionOrGroup && isListViewGroup(optionOrGroup[0]);
}

function isFlatOptions<T extends ListViewItem>(options: ListViewOptions<T>): options is T[] {
    return !options.length || !isListViewGroup(options[0]);
}

export function isGroupedOptions<T extends ListViewItem>(
    options: ListViewOptions<T>
): options is T[] {
    return !isFlatOptions(options);
}

export function getOptionsCount<T extends ListViewItem>(options: ListViewOptions<T>): number {
    return options.reduce((result, optionOrGroup) => {
        if (isListViewGroup(optionOrGroup)) return result + optionOrGroup.options.length;

        return result + 1;
    }, 0);
}

export function flattenOptions<T extends ListViewItem>(
    options: ListViewOptions<T>,
    { exclude, excludeGroupHeaders }: { excludeGroupHeaders?: boolean; exclude?: T[] } = {}
): ListViewFlatOption<T>[] {
    if (isFlatOptions(options)) {
        if (exclude) return options.filter((option) => !isIncluded(exclude, option));

        return options;
    }

    let index = 0;
    return options.flatMap((optionOrGroup) => {
        if (isListViewGroup(optionOrGroup)) {
            const groupIndex = (index++).toString();
            const filteredOptions = exclude
                ? optionOrGroup.options.filter((option) => !isIncluded(exclude, option))
                : optionOrGroup.options;

            const groupOptions =
                optionOrGroup.showSelectAll && !excludeGroupHeaders
                    ? filteredOptions.map((option) => ({ ...option, groupIndex }))
                    : filteredOptions;

            if (!groupOptions.length) return [];
            if (excludeGroupHeaders) return groupOptions;

            const selectableGroupOptions = filteredOptions.filter((o) => !o.disabled);

            const groupValues = optionOrGroup.showSelectAll
                ? {
                      groupOptions: selectableGroupOptions,
                      groupIndex,
                  }
                : {};

            return [
                {
                    label: optionOrGroup.label,
                    itemType: 'label',
                    value: '',
                    ...groupValues,
                } as T & { groupOptions?: T[]; groupIndex?: string },
                ...groupOptions,
            ];
        }

        if (exclude) return isIncluded(exclude, optionOrGroup) ? [] : optionOrGroup;

        return optionOrGroup;
    });
}

/**
 * Filters flat or grouped ListView options using a search term.
 *
 * Used internally by the ListView filtering pipeline.
 *
 * @param {ListViewOptions<T>} source - Flat options or grouped options
 * @param {string} filter - Search term used for matching
 * @param {'label'|'subtitle'|'all'} [matchBy='label'] - Fields to match against
 * @returns {ListViewOptions<T>} Filtered options
 */
export function filterOptions<T extends ListViewItem>(
    source: ListViewOptions<T>,
    filter: string,
    matchBy = 'label'
) {
    let totalCount = 0;
    let options = [];

    if (isFlatOptions(source)) {
        totalCount = source.length;
        options = source.filter((item) => itemMatch(item, filter, matchBy));
    } else {
        options = source.reduce((result, optionOrGroup) => {
            if (isListViewGroup(optionOrGroup)) {
                totalCount += optionOrGroup.options.length;

                if (optionOrGroup.filterable === false || !filter)
                    return [...result, optionOrGroup];

                const filteredGroup = optionOrGroup.options.filter((item) =>
                    itemMatch(item, filter, matchBy)
                );

                if (!filteredGroup.length) return result;

                return [...result, { ...optionOrGroup, options: filteredGroup }];
            }

            totalCount += 1;

            if (!filter || itemMatch(optionOrGroup, filter, matchBy))
                return [...result, optionOrGroup];

            return result;
        }, [] as ListViewOptions<T>);
    }

    return { totalCount, options };
}

export function defaultFilter<T extends ListViewItem>(
    options: ListViewOptions<T>,
    value?: string,
    matchBy?: string
): ListViewOptionsLoadResult<T> {
    const { totalCount, options: data } = value
        ? filterOptions(options, value, matchBy)
        : { totalCount: getOptionsCount(options), options };

    return { totalCount, data };
}

export function removeValues<T extends ListViewItem, M extends boolean>(
    source: ListViewValue<T, M>,
    values: T[],
    defaultValue: ListViewValue<T, M>
) {
    if (isMultiValue(source))
        return source.filter((option) => !isIncluded(values, option)) as ListViewValue<T, M>;

    return defaultValue;
}

export function addValues<T extends ListViewItem, M extends boolean>(
    source: ListViewValue<T, M>,
    values: T[],
    defaultValue: ListViewValue<T, M>
) {
    const trimmed = removeValues(source, values, defaultValue);

    if (isMultiValue(trimmed)) return [...trimmed, ...values] as ListViewValue<T, M>;

    return values[0] as ListViewValue<T, M>;
}

export function createListViewValue<T extends ListViewItem, M extends boolean>(
    values: ListViewApiValue<T, M>,
    options: ListViewOptions<T>
): ListViewValue<T, M> {
    const findOrCreate = (value: string) => {
        if (!options) return { value };

        let match;

        for (let i = 0; i < options.length; i++) {
            const option = options[i];

            if (isListViewGroup(option))
                match = option.options.find((item) => item.value === value);
            else if (option.value === value) match = option;

            if (match) break;
        }

        return match ?? { value };
    };

    const convert = () => {
        if (!values) return undefined;

        if (Array.isArray(values)) {
            if (values.some((value) => typeof value === 'string'))
                return values.map((value) => {
                    if (typeof value !== 'string') return value;
                    return findOrCreate(value);
                });

            return values;
        }

        if (typeof values === 'string') return findOrCreate(values);

        return values;
    };

    return convert() as ListViewValue<T, M>;
}

export function useDeepMemo<T>(value: T): T {
    const ref = useRef<T>();

    if (!ref.current || !isEqual(value, ref.current)) ref.current = value;

    return ref.current;
}
