import {Fragment, useCallback, useContext, useMemo, useRef} from 'react';
import {AriaGridListOptions, useGridList} from '@react-aria/gridlist';
import {mergeProps} from '@react-aria/utils';
import {ListState} from '@react-stately/list';

import {t} from 'sentry/locale';
import domId from 'sentry/utils/domId';
import {FormSize} from 'sentry/utils/theme';

import {SelectContext} from '../control';
import {SelectFilterContext} from '../list';
import {ListLabel, ListSeparator, ListWrap, SizeLimitMessage} from '../styles';
import {SelectSection} from '../types';

import {GridListOption} from './option';
import {GridListSection} from './section';

interface GridListProps
  extends React.HTMLAttributes<HTMLUListElement>,
    Omit<
      AriaGridListOptions<any>,
      'disabledKeys' | 'selectedKeys' | 'defaultSelectedKeys' | 'onSelectionChange'
    > {
  /**
   * Keyboard event handler, to be attached to the list (`ul`) element, to seamlessly
   * move focus from one composite list to another when an arrow key is pressed. Returns
   * a boolean indicating whether the keyboard event was intercepted. If yes, then no
   * further callback function should be run.
   */
  keyDownHandler: (e: React.KeyboardEvent<HTMLUListElement>) => boolean;
  /**
   * Object containing the selection state and focus position, needed for
   * `useGridList()`.
   */
  listState: ListState<any>;
  /**
   * Text label to be rendered as heading on top of grid list.
   */
  label?: React.ReactNode;
  /**
   * To be called when the user toggle-selects a whole section (applicable when sections
   * have `showToggleAllButton` set to true.) Note: this will be called in addition to
   * and before `onChange`.
   */
  onSectionToggle?: (
    section: SelectSection<React.Key>,
    type: 'select' | 'unselect'
  ) => void;
  size?: FormSize;
  /**
   * Message to be displayed when some options are hidden due to `sizeLimit`.
   */
  sizeLimitMessage?: string;
}

/**
 * A grid list with accessibile behaviors & attributes.
 * https://react-spectrum.adobe.com/react-aria/useGridList.html
 *
 * Unlike list boxes, grid lists are two-dimensional. Users can press Arrow Up/Down to
 * move between rows (options), and Arrow Left/Right to move between "columns". This is
 * useful when the select options have smaller, interactive elements (buttons/links)
 * inside. Grid lists allow users to focus on those child elements (using the Arrow
 * Left/Right keys) and interact with them, which isn't possible with list boxes.
 */
function GridList({
  listState,
  size = 'md',
  label,
  onSectionToggle,
  sizeLimitMessage,
  keyDownHandler,
  ...props
}: GridListProps) {
  const ref = useRef<HTMLUListElement>(null);
  const labelId = domId('grid-label-');
  const {gridProps} = useGridList(
    {...props, 'aria-labelledby': label ? labelId : props['aria-labelledby']},
    listState,
    ref
  );

  const onKeyDown = useCallback<React.KeyboardEventHandler<HTMLUListElement>>(
    e => {
      const continueCallback = keyDownHandler?.(e);
      // Prevent grid list from clearing value on Escape key press
      continueCallback && e.key !== 'Escape' && gridProps.onKeyDown?.(e);
    },
    [keyDownHandler, gridProps]
  );

  const {overlayIsOpen, search} = useContext(SelectContext);
  const hiddenOptions = useContext(SelectFilterContext);
  const listItems = useMemo(
    () =>
      [...listState.collection].filter(node => {
        if (node.type === 'section') {
          return ![...node.childNodes].every(child =>
            hiddenOptions.has(child.props.value)
          );
        }

        return !hiddenOptions.has(node.props.value);
      }),
    [listState.collection, hiddenOptions]
  );

  return (
    <Fragment>
      {listItems.length !== 0 && <ListSeparator role="separator" />}
      {listItems.length !== 0 && label && <ListLabel id={labelId}>{label}</ListLabel>}
      <ListWrap {...mergeProps(gridProps, props)} onKeyDown={onKeyDown} ref={ref}>
        {overlayIsOpen &&
          listItems.map(item => {
            if (item.type === 'section') {
              return (
                <GridListSection
                  key={item.key}
                  node={item}
                  listState={listState}
                  onToggle={onSectionToggle}
                  size={size}
                />
              );
            }

            return (
              <GridListOption
                key={item.key}
                node={item}
                listState={listState}
                size={size}
              />
            );
          })}

        {!search && hiddenOptions.size > 0 && (
          <SizeLimitMessage>
            {sizeLimitMessage ?? t('Use search to find more options…')}
          </SizeLimitMessage>
        )}
      </ListWrap>
    </Fragment>
  );
}

export {GridList};
