/*
 * Copyright 2020 Adobe. All rights reserved.
 * This file is licensed to you under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License. You may obtain a copy
 * of the License at http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under
 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
 * OF ANY KIND, either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */

import {AriaButtonProps} from '@react-types/button';
import {AriaListBoxOptions} from '@react-aria/listbox';
import {AriaSelectProps, SelectionMode} from '@react-types/select';
import {chain, filterDOMProps, mergeProps, nodeContains, useId} from '@react-aria/utils';
import {DOMAttributes, KeyboardDelegate, RefObject, ValidationResult} from '@react-types/shared';
import {FocusEvent, useMemo} from 'react';
import {HiddenSelectProps} from './HiddenSelect';
import {ListKeyboardDelegate, useTypeSelect} from '@react-aria/selection';
import {SelectState} from '@react-stately/select';
import {setInteractionModality} from '@react-aria/interactions';
import {useCollator} from '@react-aria/i18n';
import {useField} from '@react-aria/label';
import {useMenuTrigger} from '@react-aria/menu';

export interface AriaSelectOptions<T, M extends SelectionMode = 'single'> extends Omit<AriaSelectProps<T, M>, 'children'> {
  /**
   * An optional keyboard delegate implementation for type to select,
   * to override the default.
   */
  keyboardDelegate?: KeyboardDelegate
}

export interface SelectAria<T, M extends SelectionMode = 'single'> extends ValidationResult {
  /** Props for the label element. */
  labelProps: DOMAttributes,

  /** Props for the popup trigger element. */
  triggerProps: AriaButtonProps,

  /** Props for the element representing the selected value. */
  valueProps: DOMAttributes,

  /** Props for the popup. */
  menuProps: AriaListBoxOptions<T>,

  /** Props for the select's description element, if any. */
  descriptionProps: DOMAttributes,

  /** Props for the select's error message element, if any. */
  errorMessageProps: DOMAttributes,

  /** Props for the hidden select element. */
  hiddenSelectProps: HiddenSelectProps<T, M>
}

interface SelectData {
  isDisabled?: boolean,
  isRequired?: boolean,
  name?: string,
  form?: string,
  validationBehavior?: 'aria' | 'native'
}

export const selectData: WeakMap<SelectState<any, any>, SelectData> = new WeakMap<SelectState<any>, SelectData>();

/**
 * Provides the behavior and accessibility implementation for a select component.
 * A select displays a collapsible list of options and allows a user to select one of them.
 * @param props - Props for the select.
 * @param state - State for the select, as returned by `useListState`.
 */
export function useSelect<T, M extends SelectionMode = 'single'>(props: AriaSelectOptions<T, M>, state: SelectState<T, M>, ref: RefObject<HTMLElement | null>): SelectAria<T, M> {
  let {
    keyboardDelegate,
    isDisabled,
    isRequired,
    name,
    form,
    validationBehavior = 'aria'
  } = props;

  // By default, a KeyboardDelegate is provided which uses the DOM to query layout information (e.g. for page up/page down).
  // When virtualized, the layout object will be passed in as a prop and override this.
  let collator = useCollator({usage: 'search', sensitivity: 'base'});
  let delegate = useMemo(() => keyboardDelegate || new ListKeyboardDelegate(state.collection, state.disabledKeys, ref, collator), [keyboardDelegate, state.collection, state.disabledKeys, collator, ref]);

  let {menuTriggerProps, menuProps} = useMenuTrigger<T>(
    {
      isDisabled,
      type: 'listbox'
    },
    state,
    ref
  );

  let onKeyDown = (e: KeyboardEvent) => {
    if (state.selectionManager.selectionMode === 'multiple') {
      return;
    }
    
    switch (e.key) {
      case 'ArrowLeft': {
        // prevent scrolling containers
        e.preventDefault();

        let key = state.selectedKey != null ? delegate.getKeyAbove?.(state.selectedKey) : delegate.getFirstKey?.();
        if (key) {
          state.setSelectedKey(key);
        }
        break;
      }
      case 'ArrowRight': {
        // prevent scrolling containers
        e.preventDefault();

        let key = state.selectedKey != null ? delegate.getKeyBelow?.(state.selectedKey) : delegate.getFirstKey?.();
        if (key) {
          state.setSelectedKey(key);
        }
        break;
      }
    }
  };

  let {typeSelectProps} = useTypeSelect({
    keyboardDelegate: delegate,
    selectionManager: state.selectionManager,
    onTypeSelect(key) {
      state.setSelectedKey(key);
    }
  });

  let {isInvalid, validationErrors, validationDetails} = state.displayValidation;
  let {labelProps, fieldProps, descriptionProps, errorMessageProps} = useField({
    ...props,
    labelElementType: 'span',
    isInvalid,
    errorMessage: props.errorMessage || validationErrors
  });

  typeSelectProps.onKeyDown = typeSelectProps.onKeyDownCapture;
  delete typeSelectProps.onKeyDownCapture;
  if (state.selectionManager.selectionMode === 'multiple') {
    typeSelectProps = {};
  }

  let domProps = filterDOMProps(props, {labelable: true});
  let triggerProps = mergeProps(typeSelectProps, menuTriggerProps, fieldProps);

  let valueId = useId();

  selectData.set(state, {
    isDisabled,
    isRequired,
    name,
    form,
    validationBehavior
  });

  return {
    labelProps: {
      ...labelProps,
      onClick: () => {
        if (!props.isDisabled) {
          ref.current?.focus();

          // Show the focus ring so the user knows where focus went
          setInteractionModality('keyboard');
        }
      }
    },
    triggerProps: mergeProps(domProps, {
      ...triggerProps,
      isDisabled,
      onKeyDown: chain(triggerProps.onKeyDown, onKeyDown, props.onKeyDown),
      onKeyUp: props.onKeyUp,
      'aria-labelledby': [
        valueId,
        triggerProps['aria-labelledby'],
        triggerProps['aria-label'] && !triggerProps['aria-labelledby'] ? triggerProps.id : null
      ].filter(Boolean).join(' '),
      onFocus(e: FocusEvent) {
        if (state.isFocused) {
          return;
        }

        if (props.onFocus) {
          props.onFocus(e);
        }

        if (props.onFocusChange) {
          props.onFocusChange(true);
        }

        state.setFocused(true);
      },
      onBlur(e: FocusEvent) {
        if (state.isOpen) {
          return;
        }

        if (props.onBlur) {
          props.onBlur(e);
        }

        if (props.onFocusChange) {
          props.onFocusChange(false);
        }

        state.setFocused(false);
      }
    }),
    valueProps: {
      id: valueId
    },
    menuProps: {
      ...menuProps,
      autoFocus: state.focusStrategy || true,
      shouldSelectOnPressUp: true,
      shouldFocusOnHover: true,
      disallowEmptySelection: true,
      linkBehavior: 'selection',
      onBlur: (e) => {
        if (nodeContains(e.currentTarget, e.relatedTarget as Node)) {
          return;
        }

        if (props.onBlur) {
          props.onBlur(e);
        }

        if (props.onFocusChange) {
          props.onFocusChange(false);
        }

        state.setFocused(false);
      },
      'aria-labelledby': [
        fieldProps['aria-labelledby'],
        triggerProps['aria-label'] && !fieldProps['aria-labelledby'] ? triggerProps.id : null
      ].filter(Boolean).join(' ')
    },
    descriptionProps,
    errorMessageProps,
    isInvalid,
    validationErrors,
    validationDetails,
    hiddenSelectProps: {
      isDisabled,
      name,
      label: props.label,
      state,
      triggerRef: ref,
      form
    }
  };
}
