import {
  FocusEventHandler,
  KeyboardEventHandler,
  MouseEventHandler,
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';

import { IconGlyph } from '../../../../components/Icon/constants';
import { TextAutoTooltip } from '../../../../components/Text/TextAutoTooltip';
import { useTranslation } from '../../../../core/hooks/useTranslation';
import { useTestIdAttribute } from '../../../../hooks/useTestIdAttribute';
import { FullPropsOptions, TestIdProps } from '../../../../types';
import { assertEmptyObject } from '../../../../utils/assertEmptyObject';
import { makeTestId } from '../../../../utils/makeTestId';
import { SkeletonOption } from '../../constants';
import { SelectOption } from '../../types';

import { MultiSelectItem } from './MultiSelectItem/MultiSelectItem';
import {
  StyledClearButton,
  StyledDropdownRightContentContainer,
  StyledMultipleItemsList,
  StyledPlaceholder,
  StyledSkeletonContainer,
  StyledTriggerContainer,
  TunedDropdownIcon,
  TunedRemoveAllIcon,
  TunedScrollable,
  TunedSkeleton,
  TunedTriggerButton,
} from './styled';

export type SelectFieldTriggerProps<Option extends SelectOption> = {
  disabled?: boolean;
  cleanable?: boolean;
  /** Text that will be shown in button, if `children` is empty */
  placeholder: string | undefined;
} & TestIdProps &
  FullPropsOptions<
    'multiple',
    {
      selectedValues: Option['value'][];
      onChange: (values: Option['value'][]) => void;
    },
    {
      selectedValue: Option['value'] | undefined | null;
      onChange: (values: Option['value'] | null) => void;
    }
  > &
  FullPropsOptions<
    'async',
    {
      selectedOptions: Option[];
      optionsPrevPage: Option[];
      optionsCurrentPage: Option[];
      optionsNextPage: Option[];
    },
    {
      options: Option[];
    }
  >;

/**
 * Button that should open {@link SelectField}.
 *
 * Should be used only in {@link SelectFieldProps.trigger}.
 */
export function SelectFieldTrigger<Option extends SelectOption>(props: SelectFieldTriggerProps<Option>) {
  const {
    multiple,
    async,
    options,
    selectedOptions,
    selectedValue,
    selectedValues,
    optionsPrevPage,
    optionsCurrentPage,
    optionsNextPage,
    onChange,
    placeholder,
    testId,
    cleanable,
    disabled,
    ...rest
  } = props;
  assertEmptyObject(rest);

  const { t } = useTranslation();
  const testIdAttribute = useTestIdAttribute();

  const [currentActiveCrossButtonIndex, setCurrentActiveCrossButtonIndex] = useState(-1);

  // We need to know all options that was loaded in Menu to show label when user select this
  // option from Menu
  const [allLoadedOptionsMap, setAllLoadedOptionsMap] = useState({} as { [value: string]: Option });
  const addOptionsToAllLoadedOptions = useCallback((newOptions: Option[]) => {
    setAllLoadedOptionsMap((current) => {
      const result = { ...current };
      newOptions.forEach((option) => {
        result[option.value] = option;
      });
      return result;
    });
  }, []);
  useEffect(() => {
    if (async) {
      addOptionsToAllLoadedOptions(optionsPrevPage);
    }
  }, [addOptionsToAllLoadedOptions, async, optionsPrevPage]);
  useEffect(() => {
    if (async) {
      addOptionsToAllLoadedOptions(optionsCurrentPage);
    }
  }, [addOptionsToAllLoadedOptions, async, optionsCurrentPage]);
  useEffect(() => {
    if (async) {
      addOptionsToAllLoadedOptions(optionsNextPage);
    }
  }, [addOptionsToAllLoadedOptions, async, optionsNextPage]);

  // All options that loaded by selectedOptions props or by Menu pagination
  const allLoadedOptions = useMemo(() => {
    if (!async) {
      return [];
    }

    return [...selectedOptions, ...Object.values(allLoadedOptionsMap)];
  }, [allLoadedOptionsMap, async, selectedOptions]);

  const usedSelectedOption = useMemo(() => {
    if (multiple) {
      return undefined;
    }

    if (!async) {
      const result = options.find((option) => option.value === selectedValue);
      if (!result) {
        return null;
      }

      return result;
    } else {
      const result = allLoadedOptions.find((option) => option.value === selectedValue);
      if (!result) {
        return SkeletonOption;
      }

      return result;
    }
  }, [multiple, async, options, selectedValue, allLoadedOptions]);

  const usedSelectedOptions = useMemo(() => {
    if (!multiple) {
      return [];
    }

    if (!async) {
      return selectedValues.map((value) => {
        const result = options.find((option) => option.value === value);
        if (!result) {
          throw new Error(`Not found option with value ${value}`);
        }

        return result;
      });
    } else {
      return selectedValues.map((value) => {
        const result = allLoadedOptions.find((option) => option.value === value);
        if (!result) {
          return SkeletonOption;
        }

        return result;
      });
    }
  }, [multiple, async, selectedValues, options, allLoadedOptions]);

  // If user remove the last option by keyboard move focus to previous
  // remove option button
  useEffect(() => {
    if (currentActiveCrossButtonIndex >= usedSelectedOptions.length) {
      setCurrentActiveCrossButtonIndex(usedSelectedOptions.length - 1);
    }
  }, [currentActiveCrossButtonIndex, usedSelectedOptions.length]);

  const handleRemoveMultiSelectItemClick = useCallback(
    (option: Option) => () => {
      /* istanbul ignore next */
      if (!multiple) {
        return;
      }

      const newValue = selectedValues.filter((value) => value !== option.value);
      onChange(newValue);
    },
    [multiple, onChange, selectedValues],
  );

  // Move to the prev or next remove item button when user press Arrow Left
  // or Arrow Right
  const handleKeyDown: KeyboardEventHandler<HTMLButtonElement> = useCallback(
    (event) => {
      setCurrentActiveCrossButtonIndex((v) => {
        const totalCount = usedSelectedOptions.length;
        switch (event.key) {
          case 'ArrowRight':
            return v === totalCount - 1 ? 0 : v + 1;
          case 'ArrowLeft':
            return v === 0 ? totalCount - 1 : v - 1;
          /* istanbul ignore next */
          default:
          // no-default
        }

        return v;
      });
    },
    [usedSelectedOptions.length],
  );

  const optionLabelContent = useCallback(
    (option: Option | null | undefined | typeof SkeletonOption, index?: number) => {
      if (option === SkeletonOption) {
        return (
          <StyledSkeletonContainer
            key={`skeleton-${index}`}
            $index={index}
            as="li"
            {...{ [testIdAttribute]: makeTestId(testId, 'skeleton', index?.toString() as string) }}
          >
            <TunedSkeleton />
          </StyledSkeletonContainer>
        );
      }

      if (option) {
        return multiple ? (
          <MultiSelectItem
            key={option.value}
            autofocusRemoveButton={index === currentActiveCrossButtonIndex}
            disabled={disabled}
            onRemoveClick={handleRemoveMultiSelectItemClick(option)}
            testId={makeTestId(testId, 'item', index?.toString() as string)}
          >
            {option.label}
          </MultiSelectItem>
        ) : (
          <TextAutoTooltip
            key={option.value}
            testId={makeTestId(testId, 'value', index?.toString() as string)}
          >
            {option.label}
          </TextAutoTooltip>
        );
      }

      return <StyledPlaceholder>{placeholder ?? t('ui.select.select')}</StyledPlaceholder>;
    },
    [
      currentActiveCrossButtonIndex,
      disabled,
      handleRemoveMultiSelectItemClick,
      multiple,
      placeholder,
      t,
      testId,
      testIdAttribute,
    ],
  );

  const content = useMemo(() => {
    if (!multiple) {
      return optionLabelContent(usedSelectedOption);
    } else {
      return usedSelectedOptions.length === 0 ? (
        optionLabelContent(undefined)
      ) : (
        <StyledMultipleItemsList {...{ [testIdAttribute]: makeTestId(testId, 'list') }}>
          {usedSelectedOptions.map(optionLabelContent)}
        </StyledMultipleItemsList>
      );
    }
  }, [multiple, optionLabelContent, testId, testIdAttribute, usedSelectedOption, usedSelectedOptions]);

  const clearButtonVisible = (cleanable && !!usedSelectedOption) || (multiple && selectedValues.length > 0);

  // Pay attention that this handler should be connected to onMouseUp event
  // because TriggerButton uses it event as onClick replacement.
  const handleCleanClick: MouseEventHandler = useCallback(
    (event) => {
      event.stopPropagation();

      if (multiple) {
        onChange([]);
      } else {
        onChange(null);
      }
    },
    [multiple, onChange],
  );

  const triggerButtonRef = useRef<HTMLButtonElement>(null);

  const handleBlur: FocusEventHandler = useCallback((event) => {
    // If focus move inside some element inside trigger button do not reset
    // active remove button focus
    if (
      triggerButtonRef.current === event.relatedTarget ||
      !triggerButtonRef.current?.contains(event.relatedTarget)
    ) {
      setCurrentActiveCrossButtonIndex(-1);
    }
  }, []);

  return (
    <StyledTriggerContainer {...{ [testIdAttribute]: testId }}>
      <TunedTriggerButton
        ref={triggerButtonRef}
        onBlur={handleBlur}
        onKeyDown={handleKeyDown}
        testId={makeTestId(testId, 'button')}
      >
        <TunedScrollable $clearVisible={clearButtonVisible} $disabled={disabled}>
          {content}
          {!disabled && (
            <StyledDropdownRightContentContainer $clearVisible={clearButtonVisible}>
              {clearButtonVisible && (
                <StyledClearButton
                  aria-label={cleanable ? t('ui.select.clear') : t('ui.select.removeAllItems')}
                  onMouseUp={handleCleanClick}
                  role="button"
                  {...{
                    [testIdAttribute]: makeTestId(testId, cleanable ? 'clear-button' : 'remove-all-button'),
                  }}
                >
                  <TunedRemoveAllIcon glyph={IconGlyph.CloseCircle} />
                </StyledClearButton>
              )}
              <TunedDropdownIcon glyph={IconGlyph.ChevronDown} testId={makeTestId(testId, 'dropdown-icon')} />
            </StyledDropdownRightContentContainer>
          )}
        </TunedScrollable>
      </TunedTriggerButton>
    </StyledTriggerContainer>
  );
}
