import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';

import type { SelectOption, SelectProps } from './types';

import useDebouncedValue from '~/src/hooks/useDebouncedValue';
import useIsLargeScreen from '~/src/hooks/useIsLargeScreen';
import withSpacing from '~/src/lib/hocs/withSpacing';
import AnchoredDialog from '../AnchoredDialog';
import Box from '../Box';
import Clickable from '../Clickable';
import ChevronIcon from '../Icon/ChevronIcon';
import Loading from '../Loading';
import Modal from '../Modal';
import Text from '../Text';
import { BACKGROUND_COLOR, DEFAULT_OPTION_HEIGHT } from './constants';
import SelectButton from './SelectButton';
import SelectOptions from './SelectOptions';
import SelectSearchInput from './SelectSearchInput';

const Select = <T extends SelectOption>(props: SelectProps<T>) => {
  const {
    testId,

    name,
    defaultValue,
    value,
    label,
    placeholder = '',
    options,

    onChange,

    // we downstream this prop and make extra calculations there
    // so do not rely on it in this component
    optionHeight = DEFAULT_OPTION_HEIGHT,
    visibleOptionsCount = 5,
    searchFeature,

    renderButton,
    renderOption,

    isLoading = false,
    isDisabled = false,
    isRequired = false,
    isSameWidth = true,

    position: [xPosition, yPosition] = [undefined, undefined],

    ...boxProps
  } = props;

  const targetRef = useRef<HTMLDivElement>(null);
  const inputRef = useRef<HTMLInputElement>(null);

  const isLargeScreen = useIsLargeScreen();

  const [internalValue, setInternalValue] = useState(defaultValue ?? value);
  const [isOpened, setIsOpened] = useState(false);
  const [query, setQuery] = useState('');

  // debounced query helps to avoid laggy user input
  // during large options list filtration
  const debouncedQuery = useDebouncedValue(query, {
    force: !query.length,
    wait: 500,
  });

  // options map helps to get the particular option in performant way
  const optionsMap = useMemo(
    () =>
      options.reduce(
        (map, option) => {
          map[option.id] = option;
          return map;
        },
        {} as Record<T['id'], T>
      ),
    [options]
  );

  const optionsToShow = useMemo(() => {
    if (debouncedQuery.length) {
      return options.filter(({ text, caption }) => {
        const queryToMatch = debouncedQuery.trim().toLowerCase();
        const textTarget = text.trim().toLowerCase();
        const captionTarget = caption?.trim().toLowerCase();

        return [textTarget, captionTarget].some((target) =>
          target?.includes(queryToMatch)
        );
      });
    }

    return options;
  }, [options, debouncedQuery]);

  const onChangeInternal = useCallback<(id: SelectProps['value']) => void>(
    (id) => {
      const value = id === '' ? undefined : id;

      setInternalValue(value);

      onChange?.({
        value,
        prevValue: internalValue,
        setValue: setInternalValue,
      });
    },
    [internalValue, onChange]
  );

  // O(1) selected option retrieving
  const selectedOption = internalValue ? optionsMap[internalValue] : undefined;

  const renderContent = useCallback(
    ({ query, close }: { query: string; close: () => void }) => {
      if (isLoading) {
        return (
          <Box padding="1.6rem" centerContent>
            <Loading size="2rem" />
          </Box>
        );
      }

      return (
        <Box testId={`${testId || 'select'}Content`} flexColumn fullHeight>
          <Box noFlexShrink>
            {searchFeature ? (
              <SelectSearchInput
                ref={inputRef}
                testId={testId}
                value={query}
                placeholder={searchFeature.placeholder}
                close={close}
                onChange={setQuery}
                onClear={() => {
                  setQuery('');
                  inputRef.current?.focus();
                }}
              />
            ) : isLargeScreen ? null : (
              <Box
                flexRow
                alignCenter
                height="5.6rem"
                padding="0 1.6rem"
                style={{ borderBottom: '1px solid #3f3f3f' }}
              >
                <Clickable onClick={close} isInline noFlexShrink>
                  <ChevronIcon size="2.4rem" direction="left" />
                </Clickable>
                <Text
                  isBold
                  centered
                  flexGrow
                  withEllipsis
                  size="1.7rem"
                  margin="0 1rem"
                >
                  {label}
                </Text>
                <Box noFlexShrink width="2.4rem" />
              </Box>
            )}
          </Box>
          <SelectOptions
            testId={testId}
            inputRef={inputRef}
            options={optionsToShow}
            close={close}
            query={debouncedQuery}
            onChange={onChangeInternal}
            renderOption={renderOption}
            optionHeight={optionHeight}
            visibleOptionsCount={visibleOptionsCount}
            searchFeature={searchFeature}
            selectedOption={selectedOption}
          />
        </Box>
      );
    },
    [
      selectedOption,
      optionsToShow,
      debouncedQuery,
      isLargeScreen,
      optionHeight,
      isLoading,
    ]
  );

  useEffect(() => {
    // reset query after select close
    if (!isOpened) {
      setQuery('');
    }
  }, [isOpened]);

  useEffect(() => {
    // NOTE: Update select value only if value explicitly provided
    // Checking for own property allow to pass undefined as a value to erase select
    if (props.hasOwnProperty('value')) {
      setInternalValue(value);
    }
  }, [value]);

  return (
    <Box
      testId={testId}
      // REVIEW: this sizing doesn't work if you want the component to be full width
      width="fit-content"
      height="fit-content"
      {...boxProps}
    >
      <Box positionRelative zIndex={1} flexColumn fullWidth fullHeight>
        <div ref={targetRef} style={{ width: 'inherit', height: 'inherit' }}>
          {/* hidden input to pass value to native form and native form validation */}
          <input
            name={name}
            value={internalValue ?? ''}
            tabIndex={-1}
            required={isRequired}
            readOnly
            style={{
              position: 'absolute',
              zIndex: -1,
              top: 0,
              left: 0,
              opacity: '0',
              pointerEvents: 'none',
              width: '100%',
              height: '100%',
            }}
          />
          {renderButton?.({
            testId,
            isOpened,
            isLoading,
            isDisabled,
            isEmpty: !selectedOption,
            option: selectedOption,
            open: () => setIsOpened(true),
          }) ?? (
            <SelectButton<T>
              testId={testId}
              isOpened={isOpened}
              isLoading={isLoading}
              isDisabled={isDisabled}
              isEmpty={!selectedOption}
              text={selectedOption?.text ?? placeholder}
              open={() => setIsOpened(true)}
            />
          )}
        </div>
      </Box>
      {isOpened &&
        (isLargeScreen ? (
          <AnchoredDialog
            color={BACKGROUND_COLOR}
            targetElRef={targetRef}
            withTriangle={false}
            xPosition={xPosition}
            yPosition={yPosition}
            matchTargetElWidth={isSameWidth}
            style={{
              overflow: 'hidden',
              minWidth: isSameWidth ? '100%' : '20rem',
              maxWidth: isSameWidth ? '100%' : '40rem',
            }}
            onClose={() => setIsOpened(false)}
            renderContent={({ close }) => renderContent({ query, close })}
          />
        ) : (
          <Modal
            onClose={() => setIsOpened(false)}
            renderHeader={() => <></>}
            renderContent={({ close }) => renderContent({ query, close })}
            style={{
              padding: '0',
              width: '100%',
              height: '100%',
              backgroundColor: BACKGROUND_COLOR,
            }}
          />
        ))}
    </Box>
  );
};

const SelectWithSpacing = withSpacing(Select);

export default memo(SelectWithSpacing) as typeof Select;
