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

import { styled, theme } from '../../stitches.config';
import { Select, SelectProps } from '../Select';

import { useValue } from '../../hooks';
import { BaseFormInputProps } from '../internal';

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 StyleListTextFaded = styled('span', {
    color: theme.colors['grey-2'],
});

type TypeaheadOption<O> =
    | {
          /** Type of the option */
          type: 'create';
      }
    | {
          /** Type of the option */
          type: 'option';

          /** Option if it exists */
          option: O;
      }
    | {
          /** Type of the option */
          type: 'value';

          /** Key to match against */
          key: string;
      };

type SingleTypeahead<O> = Omit<
    SelectProps<O, false>,
    'value' | 'defaultValue' | 'onChange'
> &
    Pick<BaseFormInputProps<string>, 'value' | 'defaultValue' | 'onChange'>;

type MultipleTypeahead<O> = Omit<
    SelectProps<O, true>,
    'value' | 'defaultValue' | 'onChange'
> &
    Pick<BaseFormInputProps<string[]>, 'value' | 'defaultValue' | 'onChange'>;

export type TypeaheadProps<O, multiple extends boolean> = multiple extends true
    ? MultipleTypeahead<O>
    : SingleTypeahead<O>;

/**
 * Typeahead component
 */
const _Typeahead = <O, multiple extends boolean>(
    props: TypeaheadProps<O, multiple>,
    ref: ForwardedRef<HTMLDivElement>,
): JSX.Element => {
    const {
        value,
        defaultValue,
        options,
        onChange = () => null,
        multiple = false,
        search = '',
        getSearch = DEFAULT_GET_SEARCH,
        getKey = DEFAULT_GET_KEY,
        getDisplay = DEFAULT_GET_DISPLAY,
        onSearch = () => null,
        ...otherProps
    } = props;

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

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

    // manage value for single-select
    const [internalValue, setInternalValue] = useValue<
        TypeaheadProps<O, multiple>['value']
    >({
        initialValue: multiple ? [] : '',
        value: value,
        defaultValue: defaultValue,
        onChange: (value) => {
            if (multiple) {
                (onChange as NonNullable<MultipleTypeahead<O>['onChange']>)(
                    value as NonNullable<MultipleTypeahead<O>['value']>,
                );
            } else {
                (onChange as NonNullable<SingleTypeahead<O>['onChange']>)(
                    value as NonNullable<SingleTypeahead<O>['value']>,
                );
            }
        },
    });

    // rendered options
    const renderedValues: TypeaheadOption<O>[] | TypeaheadOption<O> | null =
        useMemo(() => {
            if (multiple) {
                return (
                    internalValue as NonNullable<MultipleTypeahead<O>['value']>
                ).map((v) => {
                    return { type: 'value', key: v };
                });
            }

            return {
                type: 'value',
                key: internalValue as NonNullable<SingleTypeahead<O>['value']>,
            };
        }, [multiple, internalValue]);

    // render the options with the search value
    const renderedOptions = useMemo(() => {
        const updated: TypeaheadOption<O>[] = [];

        // track if we can create
        let isCreate = !!searchValue;

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

            // if there is a direct match we do not add
            const key = getKey(option);
            if (key === searchValue) {
                isCreate = false;
            }

            // only keep if the options are there
            updated.push({
                type: 'option',
                option: option,
            });
        }

        // check the selected values
        if (multiple && internalValue) {
            for (
                let valIdx = 0, valLen = internalValue.length;
                valIdx < valLen;
                valIdx++
            ) {
                const value = internalValue[valIdx];

                if (value === searchValue) {
                    isCreate = false;
                    break;
                }
            }
        } else if (!multiple) {
            if (internalValue === searchValue) {
                isCreate = false;
            }
        }

        if (isCreate) {
            updated.push({
                type: 'create',
            });
        }

        return updated;
    }, [multiple, internalValue, options, searchValue, getKey]);

    /**
     * Intercept changes and update accordingly
     *
     * value - value to change
     */
    const handleOnChange = (
        value: TypeaheadOption<O>[] | TypeaheadOption<O> | null,
    ) => {
        if (multiple) {
            if (!value) {
                setInternalValue([]);
                return;
            }

            const updated = [];

            // track if we are creating
            let isCreate = false;

            const options = value as TypeaheadOption<O>[];

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

                if (option.type === 'create') {
                    isCreate = true;
                } else if (option.type === 'option') {
                    updated.push(getKey(option.option));
                } else if (option.type === 'value') {
                    updated.push(option.key);
                }
            }

            if (isCreate && updated.indexOf(searchValue) === -1) {
                updated.push(searchValue);
            }

            setInternalValue(updated);
        } else {
            if (value === null) {
                setInternalValue('');
                return;
            }

            const option = value as TypeaheadOption<O>;

            let key = '';
            if (option.type === 'create') {
                key = searchValue;
            } else if (option.type === 'option') {
                key = getKey(option.option);
            } else if (option.type === 'value') {
                key = option.key;
            }

            setInternalValue(key);
        }
    };

    /**
     * Intercept and handle the changes
     *
     * @param search - search term
     */
    const handleOnSearch = (search: string) => {
        // set the internal search value to update the text
        setSearchValue(search);

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

    /**
     * Intercept and get the key base on the option
     *
     * @param option - option to get the key for
     */
    const handleGetKey = (option: TypeaheadOption<O>): string => {
        if (option.type === 'create') {
            return '';
        } else if (option.type === 'option') {
            return getKey(option.option);
        } else if (option.type === 'value') {
            return option.key;
        }

        return '';
    };

    /**
     * Intercept and get the display base on the option
     *
     * @param option - option to get the display for
     */
    const handleGetDisplay = (option: TypeaheadOption<O>): React.ReactNode => {
        if (option.type === 'create') {
            return (
                <StyleListTextFaded>Create {searchValue}</StyleListTextFaded>
            );
        } else if (option.type === 'option') {
            return getDisplay(option.option);
        } else if (option.type === 'value') {
            return option.key;
        }

        return '';
    };

    /**
     * Intercept and gget the display base on the option
     *
     * @param search - search string
     * @param option - option to get the display for
     */
    const handleGetSearch = (
        search: string,
        option: TypeaheadOption<O>,
    ): boolean => {
        if (option.type === 'create') {
            return true;
        } else if (option.type === 'option') {
            return getSearch(search, option.option);
        } else if (option.type === 'value') {
            return false;
        }

        return false;
    };

    if (multiple) {
        return (
            <Select<TypeaheadOption<O>, true>
                ref={ref}
                value={renderedValues as TypeaheadOption<O>[]}
                onChange={handleOnChange}
                options={renderedOptions}
                multiple={multiple}
                search={searchValue}
                onSearch={handleOnSearch}
                getKey={handleGetKey}
                getDisplay={handleGetDisplay}
                getSearch={handleGetSearch}
                {...otherProps}
            />
        );
    } else {
        return (
            <Select<TypeaheadOption<O>, false>
                ref={ref}
                value={renderedValues as TypeaheadOption<O> | null}
                onChange={handleOnChange}
                options={renderedOptions}
                multiple={multiple}
                search={searchValue}
                onSearch={handleOnSearch}
                getKey={handleGetKey}
                getDisplay={handleGetDisplay}
                getSearch={handleGetSearch}
                {...otherProps}
            />
        );
    }
};

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