import {
    forwardRef,
    ForwardedRef,
    ReactNode,
    useState,
    useMemo,
    useEffect,
    useRef,
} from 'react';

import { styled } from '../../stitches.config';

import { useValue, useIntersectionObserver } from '../../hooks';
import {
    BaseFormInputStyles,
    BaseFormInput,
    BaseFormInputProps,
    BaseList,
    BaseListOption,
    BaseListEmptyOption,
} from '../internal';

import { Checkbox } from '../Checkbox';
import { Scroll } from '../Scroll';

import { getSearch as DEFAULT_GET_SEARCH } from '@/utility';
import { getKey as DEFAULT_GET_KEY } from '@/utility';
import { getDisplay as DEFAULT_GET_DISPLAY } from '@/utility';

const StyledFormInput = styled(BaseFormInput, BaseFormInputStyles, {
    cursor: 'pointer',
    variants: {
        size: {
            sm: {
                padding: '0',
                height: '$space$32',
            },
            md: {
                padding: '0',
                height: '$space$48',
            },
            lg: {
                padding: '0',
                height: '$space$64',
            },
        },
    },
});

const StyledContainer = styled('div', {
    display: 'flex',
    flexDirection: 'column',
    height: '$space$full',
    overflow: 'hidden',
});

const StyledInput = styled('input', {
    backgroundColor: 'transparent',
    borderBottomWidth: '$default',
    outline: 'none',
    width: '$space$full',
    margin: '0 0 $1 0',
    variants: {
        valid: {
            true: {
                borderBottomColor: `$grey-4`,
                '&:focus': {
                    borderBottomColor: '$primary-1',
                },
            },
            false: {
                borderBottomColor: `$grey-4`,
                '&:focus': {
                    borderBottomColor: '$error-1',
                },
            },
        },
        size: {
            sm: {
                padding: '0 $1',
                height: '$space$6',
            },
            md: {
                padding: '$1 $2',
                height: '$space$8',
            },
            lg: {
                padding: '$2 $3',
                height: '$space$10',
            },
        },
    },
    defaultVariants: {
        size: 'md',
        valid: true,
    },
});

const StyledSelectAllContainer = styled('div', {
    flex: '0',
    padding: '0 $1',
    width: '$space$full',
});

const StyledSelectAll = styled(BaseListOption, {
    flex: '0',
    background: 'transparent',
    variants: {
        size: {
            sm: {
                minHeight: '$space$6',
            },
            md: {
                minHeight: '$space$8',
            },
            lg: {
                minHeight: '$space$10',
            },
        },
    },
    defaultVariants: {
        size: 'md',
    },
});

const StyledCheckbox = styled(Checkbox, {
    width: '$space$full',
});

const StyledCheckboxText = styled('div', {
    width: '$space$full',
    whiteSpace: 'nowrap',
    overflow: 'hidden',
    textOverflow: 'ellipsis',
});

const StyledScroll = styled(Scroll, {
    flex: '1',
});

const StyledList = styled(BaseList, {
    background: 'transparent',
    border: 'none',
    height: '$space$auto',
    zIndex: '1',
    overflow: 'auto',
    padding: '0 $1 $1 $1',
});

const StyledListOption = styled(BaseListOption, {
    background: 'transparent',
    '&[data-focused]': {
        backgroundColor: '$primary-5',
    },
    variants: {
        size: {
            sm: {
                minHeight: '$space$6',
            },
            md: {
                minHeight: '$space$8',
            },
            lg: {
                minHeight: '$space$10',
            },
        },
    },
    defaultVariants: {
        size: 'md',
    },
});

const StyledListEmptyOption = styled(BaseListEmptyOption, {
    background: 'transparent',
    variants: {
        size: {
            sm: {
                height: '$space$6',
            },
            md: {
                height: '$space$8',
            },
            lg: {
                height: '$space$10',
            },
        },
    },
    defaultVariants: {
        size: 'md',
    },
});

interface BaseChecklist<O, V> extends BaseFormInputProps<V> {
    /** Options to render in the list of the Checklist */
    options: O[];

    /** Allow multiple options to be selected */
    multiple?: boolean;

    /** Show a select all button when multiple */
    selectAll?: boolean;

    /** Placeholder to show when there is no value */
    placeholder?: string;

    /** Props to pass to the input */
    inputProps?: React.InputHTMLAttributes<HTMLInputElement>;

    /** Search term */
    search?: string;

    /** Filter the options on search */
    filter?: boolean;

    /** Callback that is triggered when search is updated */
    onSearch?: (value: string) => void;

    /** Callback that is triggered when the end of the list is reached */
    onLoad?: () => void;

    /** Callback that is triggered on search. This returns true if the option should be included. */
    getSearch?: (search: string, option: O) => boolean;

    /** Callback that is triggered to get the unique key of an option */
    getKey?: (option: O) => string;

    /** Callback that is triggered to get the display of an option */
    getDisplay?: (option: O) => ReactNode;
}

type SingleChecklist<O> = BaseChecklist<O, O | null>;

type MultipleChecklist<O> = BaseChecklist<O, O[]>;

export type ChecklistProps<O, multiple extends boolean> = multiple extends true
    ? MultipleChecklist<O>
    : SingleChecklist<O>;

const _Checklist = <O, multiple extends boolean>(
    props: ChecklistProps<O, multiple>,
    ref: ForwardedRef<HTMLDivElement>,
): JSX.Element => {
    const {
        id,
        value,
        defaultValue,
        onChange = () => null,
        options,
        multiple = false,
        selectAll = true,
        placeholder,
        inputProps,
        search = '',
        filter = true,
        onSearch = () => null,
        onLoad = () => null,
        getSearch = DEFAULT_GET_SEARCH,
        getKey = DEFAULT_GET_KEY,
        getDisplay = DEFAULT_GET_DISPLAY,
        disabled = false,
        valid,
        size,
        ...otherProps
    } = props;

    // store the input + list in a ref
    const inputRef = useRef<HTMLInputElement | null>(null);
    const listRef = useRef<HTMLUListElement | null>(null);
    const scrollRef = useRef<HTMLDivElement | null>(null);
    const lastRef = useRef<HTMLElement | null>(null);

    // manage the internal value
    const [internalValue, setInternalValue] = useValue<
        MultipleChecklist<O>['value'] | SingleChecklist<O>['value']
    >({
        initialValue: multiple ? [] : null,
        value: value,
        defaultValue: defaultValue,
        onChange: (value) => {
            if (multiple) {
                (onChange as NonNullable<MultipleChecklist<O>['onChange']>)(
                    value as NonNullable<MultipleChecklist<O>['value']>,
                );
            } else {
                (onChange as NonNullable<SingleChecklist<O>['onChange']>)(
                    value as NonNullable<SingleChecklist<O>['value']>,
                );
            }
        },
    });

    // store the highlighted information
    const [highlight, setHighlight] = useState<'search' | 'selectAll' | number>(
        'search',
    );

    // manage she searchValue
    const [searchValue, setSearchValue] = useState<string>(search);

    // update the search value whenever it changes
    useEffect(() => {
        setSearchValue(search);
    }, [search]);

    // store the selected options in an array
    const selectedOptions: O[] = useMemo(() => {
        if (multiple) {
            return internalValue as NonNullable<MultipleChecklist<O>['value']>;
        } else {
            if (internalValue === null) {
                return [];
            }

            return [internalValue as NonNullable<SingleChecklist<O>['value']>];
        }
    }, [multiple, internalValue]);

    // store the keys for the selected options
    const selectedKeys = useMemo(() => {
        // this assumes that there are no duplicate keys
        // we will replace with the first one

        const selected: {
            [key: string]: number;
        } = {};
        for (let optIdx = selectedOptions.length - 1; optIdx >= 0; optIdx--) {
            const k = getKey(selectedOptions[optIdx]);

            selected[k] = optIdx;
        }

        return selected;
    }, [selectedOptions, getKey]);

    // filtered options that are returned after search (or don't filter )
    const renderedOptions: O[] = useMemo(() => {
        if (!filter || !searchValue) {
            return options;
        }

        // make it case insensitive
        const cleaned = searchValue.toLowerCase();

        return options.filter((o) => getSearch(cleaned, o));
    }, [searchValue, filter, options, getSearch]);

    // store the boolean for the selected values
    const allSelected = useMemo(() => {
        // look at all of the filtered options and see if the key is selected, if any is missing then the option is not selected
        for (let optIdx = renderedOptions.length - 1; optIdx >= 0; optIdx--) {
            const k = getKey(renderedOptions[optIdx]);

            const selected = Object.prototype.hasOwnProperty.call(
                selectedKeys,
                k,
            );

            if (!selected) {
                return false;
            }
        }

        return true;
    }, [renderedOptions, selectedKeys, getKey]);

    // store the rendered items
    const renderedItems = useMemo(() => {
        return renderedOptions.map((o, idx) => {
            return {
                key: getKey(o),
                option: o,
                id: `${id}--option-${idx}`,
            };
        });
    }, [renderedOptions, getKey, id]);

    // infinite load
    useIntersectionObserver(lastRef, () => {
        // only load if the list is bigger than the scroll area
        if (
            !disabled &&
            listRef.current &&
            scrollRef.current &&
            listRef.current.offsetHeight > scrollRef.current.offsetHeight
        ) {
            onLoad();
        }
    });

    /**
     * Handle the newly changed option
     *
     * option - changed option
     */
    const handleChange = (option?: O | null) => {
        if (multiple) {
            if (!option) {
                return;
            }

            const key = getKey(option);

            // get the index of the key (if it exists)
            const optionIdx = Object.prototype.hasOwnProperty.call(
                selectedKeys,
                key,
            )
                ? selectedKeys[key]
                : -1;

            if (optionIdx === -1) {
                const updated = [...selectedOptions, option];

                setInternalValue(updated);
            } else {
                const updated = selectedOptions.filter((o, idx) => {
                    return idx !== optionIdx;
                });

                setInternalValue(updated);
            }
        } else {
            setInternalValue(option === undefined ? null : option);
        }
    };

    /**
     * Handle selecting all of the values
     */
    const handleSelectAll = () => {
        if (!multiple || !selectAll) {
            return;
        }

        if (allSelected) {
            // everything is selected so select nothing
            setInternalValue([]);
        } else {
            // nothing is selected so select everything
            setInternalValue([...renderedOptions]);
        }
    };

    /**
     * Scroll to the highlighted item
     *
     * item - item to highlight
     */
    const scrollToHighlight = (item: typeof highlight) => {
        if (item === 'search' || item === 'selectAll') {
            return;
        }

        // scroll to the item if it exists
        const itemEle = listRef?.current?.children[item];
        if (itemEle) {
            itemEle.scrollIntoView({
                behavior: 'auto',
                block: 'nearest',
            });
        }
    };

    return (
        <StyledFormInput
            ref={ref}
            id={id}
            focusRef={inputRef}
            valid={valid}
            size={size}
            disabled={disabled}
            role={'combobox'}
            {...otherProps}
        >
            <StyledContainer>
                <StyledInput
                    as={'input'}
                    ref={inputRef}
                    valid={valid}
                    // @ts-expect-error: There is an issue with stitches merging props, this will work.
                    size={size}
                    disabled={disabled}
                    placeholder={placeholder}
                    autoComplete={'off'}
                    onFocus={() => {
                        if (disabled) {
                            return;
                        }

                        // reset the highlight
                        setHighlight('search');
                    }}
                    onBlur={() => {
                        if (disabled) {
                            return;
                        }

                        // reset the highlight
                        setHighlight('search');
                    }}
                    onKeyDown={(event: React.KeyboardEvent) => {
                        if (disabled) {
                            return;
                        }

                        let next: typeof highlight = highlight;
                        if (event.key === 'Enter') {
                            if (highlight === 'search') {
                                //noop
                            } else if (highlight === 'selectAll') {
                                handleSelectAll();
                            } else {
                                handleChange(renderedItems[highlight].option);
                            }
                        } else if (event.key === 'Space') {
                            // prevent the default scroll
                            event.preventDefault();

                            if (highlight === 'search') {
                                //noop
                            } else if (highlight === 'selectAll') {
                                handleSelectAll();
                            } else {
                                handleChange(renderedItems[highlight].option);
                            }
                        } else if (event.key === 'Home') {
                            next = 'search';
                        } else if (event.key === 'End') {
                            if (renderedItems.length > 0) {
                                next = renderedItems.length - 1;
                            } else {
                                next = 'search';
                            }
                        } else if (event.key === 'ArrowDown') {
                            event.preventDefault();

                            if (highlight === 'search') {
                                if (selectAll && multiple) {
                                    next = 'selectAll';
                                } else if (renderedItems.length > 0) {
                                    next = 0;
                                } else {
                                    next = 'search';
                                }
                            } else if (highlight === 'selectAll') {
                                if (renderedItems.length > 0) {
                                    next = 0;
                                } else {
                                    next = 'selectAll';
                                }
                            } else {
                                next = Math.min(
                                    highlight + 1,
                                    renderedItems.length - 1,
                                );
                            }
                        } else if (event.key === 'ArrowUp') {
                            event.preventDefault();

                            if (highlight === 'search') {
                                next = 'search';
                            } else if (highlight === 'selectAll') {
                                next = 'search';
                            } else {
                                if (highlight === 0 && selectAll && multiple) {
                                    next = 'selectAll';
                                } else if (highlight === 0) {
                                    next = 'search';
                                } else {
                                    next = Math.max(0, highlight - 1);
                                }
                            }
                        }

                        // set the highlight
                        setHighlight(next);

                        // scroll to it
                        scrollToHighlight(next);
                    }}
                    onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
                        const updated = event.target.value || '';

                        // set the search value
                        setSearchValue(updated);

                        // call the callback
                        if (onSearch) {
                            onSearch(updated);
                        }

                        // reset the highlight
                        setHighlight('search');
                    }}
                    aria-labelledby={otherProps['aria-labelledby']}
                    {...inputProps}
                />
                {selectAll && multiple && (
                    <StyledSelectAllContainer>
                        <StyledSelectAll
                            as={'div'}
                            id={`${id}--selectAll`}
                            size={size}
                            data-focused={
                                highlight === 'selectAll' || undefined
                            }
                            onClick={(event: React.MouseEvent) => {
                                // don't move the focus
                                event.preventDefault();

                                if (disabled) {
                                    return;
                                }

                                // set the highlight
                                setHighlight('selectAll');

                                // select all of the items
                                handleSelectAll();
                            }}
                            onMouseOver={() => {
                                if (disabled) {
                                    return;
                                }

                                // set the highlight
                                setHighlight('selectAll');
                            }}
                            role={'option'}
                            aria-label={
                                searchValue
                                    ? 'Select all searched options in the checklist'
                                    : 'Select all options in the checklist'
                            }
                            title={
                                searchValue
                                    ? 'Select all searched options in the checklist'
                                    : 'Select all options in the checklist'
                            }
                        >
                            <StyledCheckbox
                                disabled={disabled}
                                value={allSelected}
                                aria-hidden={true}
                                inputProps={{
                                    tabIndex: -1,
                                }}
                            >
                                <StyledCheckboxText>
                                    {searchValue
                                        ? '(Select Searched)'
                                        : '(Select All)'}
                                </StyledCheckboxText>
                            </StyledCheckbox>
                        </StyledSelectAll>
                    </StyledSelectAllContainer>
                )}
                <StyledScroll ref={scrollRef} horizontal={false}>
                    <StyledList
                        as={'ul'}
                        ref={listRef}
                        onMouseLeave={() => {
                            if (disabled) {
                                return;
                            }

                            // reset the highlight
                            setHighlight('search');
                        }}
                        role="listbox"
                        aria-labelledby={otherProps['aria-labelledby']}
                        aria-multiselectable={multiple ? 'true' : 'false'}
                        aria-orientation="vertical"
                        aria-activedescendant={
                            typeof highlight === 'number'
                                ? `${id}--option-${highlight}`
                                : ''
                        }
                    >
                        {renderedItems.length > 0 && (
                            <>
                                {renderedItems.map(
                                    ({ option, key, id }, index) => {
                                        const selected =
                                            Object.prototype.hasOwnProperty.call(
                                                selectedKeys,
                                                key,
                                            );

                                        return (
                                            <StyledListOption
                                                id={id}
                                                key={key}
                                                size={size}
                                                data-focused={
                                                    highlight === index ||
                                                    undefined
                                                }
                                                onClick={(event) => {
                                                    // don't move the focus
                                                    event.preventDefault();

                                                    if (disabled) {
                                                        return;
                                                    }

                                                    // set the highlight
                                                    setHighlight(index);

                                                    // select the item if the list is not open
                                                    handleChange(option);
                                                }}
                                                onMouseOver={() => {
                                                    if (disabled) {
                                                        return;
                                                    }

                                                    // set the highlight
                                                    setHighlight(index);
                                                }}
                                                role={'option'}
                                                aria-selected={
                                                    selected ? true : false
                                                }
                                                title={key}
                                            >
                                                <StyledCheckbox
                                                    disabled={disabled}
                                                    value={selected}
                                                    aria-hidden={true}
                                                    inputProps={{
                                                        tabIndex: -1,
                                                    }}
                                                >
                                                    <StyledCheckboxText>
                                                        {getDisplay(option)}
                                                    </StyledCheckboxText>
                                                </StyledCheckbox>
                                            </StyledListOption>
                                        );
                                    },
                                )}
                                <span ref={lastRef}></span>
                            </>
                        )}

                        {renderedItems.length === 0 && (
                            <StyledListEmptyOption size={size}>
                                No Options
                            </StyledListEmptyOption>
                        )}
                    </StyledList>
                </StyledScroll>
            </StyledContainer>
        </StyledFormInput>
    );
};

export const Checklist = forwardRef(_Checklist) as <
    O,
    multiple extends boolean,
>(
    props: ChecklistProps<O, multiple> & {
        ref?: ForwardedRef<HTMLDivElement>;
    },
) => ReturnType<typeof _Checklist>;
