import React, { forwardRef, HTMLAttributes, JSX, ReactNode } from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'

import { CElementCover } from '../element-cover'
import { CVirtualScroller } from '../virtual-scroller'

import { getNextSibling, getPreviousSibling } from '../../utils'
import type { Option, OptionsGroup } from './types'

export interface CMultiSelectOptionsProps extends HTMLAttributes<HTMLDivElement> {
  loading?: boolean
  onGroupOnClick?: (group: OptionsGroup) => void
  onOptionOnClick?: (option: Option) => void
  options: (Option | OptionsGroup)[]
  optionsMaxHeight?: number | string
  optionsStyle?: 'checkbox' | 'text'
  optionsTemplate?: (option: Option) => ReactNode
  optionsGroupsSelectable?: boolean
  optionsGroupsStyle?: 'checkbox' | 'text'
  optionsGroupsTemplate?: (option: OptionsGroup) => ReactNode
  searchNoResultsLabel?: string | ReactNode
  selected: Option[]
  virtualScroller?: boolean
  visibleItems?: number
}

export const CMultiSelectOptions = forwardRef<HTMLDivElement, CMultiSelectOptionsProps>(
  (
    {
      loading,
      onGroupOnClick,
      onKeyDown,
      onOptionOnClick,
      options,
      optionsMaxHeight,
      optionsStyle,
      optionsTemplate,
      optionsGroupsSelectable,
      optionsGroupsStyle,
      optionsGroupsTemplate,
      searchNoResultsLabel,
      selected,
      virtualScroller,
      visibleItems = 10,
    },
    ref
  ) => {
    const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>, option: Option) => {
      if (event.code === 'Space' || event.key === 'Enter') {
        event.preventDefault()
        onOptionOnClick?.(option)
      }

      if (event.key === 'Down' || event.key === 'ArrowDown') {
        event.preventDefault()
        const target = event.target as HTMLElement
        const next = getNextSibling(target, '.form-multi-select-option')

        next && (next as HTMLElement).focus()
      }

      if (event.key === 'Up' || event.key === 'ArrowUp') {
        event.preventDefault()
        const target = event.target as HTMLElement
        const prev = getPreviousSibling(target, '.form-multi-select-option')

        prev && (prev as HTMLElement).focus()
      }
    }

    const handleGroupKeyDown = (
      event: React.KeyboardEvent<HTMLDivElement>,
      group: OptionsGroup
    ) => {
      if (event.code === 'Space' || event.key === 'Enter') {
        event.preventDefault()
        onGroupOnClick?.(group)
        return
      }

      if (event.key === 'Down' || event.key === 'ArrowDown') {
        event.preventDefault()
        const next = getNextSibling(event.target as HTMLElement, '.form-multi-select-option')

        next && (next as HTMLElement).focus()
      }

      if (event.key === 'Up' || event.key === 'ArrowUp') {
        event.preventDefault()
        const prev = getPreviousSibling(event.target as HTMLElement, '.form-multi-select-option')

        prev && (prev as HTMLElement).focus()
      }
    }

    const getGroupCheckboxState = (group: OptionsGroup) => {
      const groupOptions = (group.options ?? []).filter((option) => !option.disabled)
      const selectedCount = groupOptions.filter((option) =>
        selected.some((_option) => _option.value === option.value)
      ).length

      if (groupOptions.length > 0 && selectedCount >= groupOptions.length) {
        return 'all'
      }

      return selectedCount === 0 ? 'none' : 'indeterminate'
    }

    const createOptions = (options: (Option | OptionsGroup)[]): JSX.Element | JSX.Element[] =>
      options.length > 0 ? (
        options.map((option: Option | OptionsGroup, index: number) =>
          'value' in option ? (
            <div
              className={classNames('form-multi-select-option', {
                'form-multi-select-option-with-checkbox': optionsStyle === 'checkbox',
                'form-multi-selected': selected.some((_option) => _option.value === option.value),
                disabled: option.disabled,
              })}
              key={index}
              onClick={() => onOptionOnClick && onOptionOnClick(option as Option)}
              onKeyDown={(event) => handleKeyDown(event, option as Option)}
              tabIndex={0}
              role="option"
              aria-selected={selected.some((_option) => _option.value === option.value)}
              aria-disabled={Boolean(option.disabled)}
            >
              {optionsTemplate ? optionsTemplate(option as Option) : option.label}
            </div>
          ) : (
            <div
              className={classNames('form-multi-select-optgroup-label', {
                'form-multi-select-optgroup-label-with-checkbox':
                  optionsGroupsSelectable && optionsGroupsStyle === 'checkbox',
                'form-multi-selected':
                  optionsGroupsSelectable &&
                  optionsGroupsStyle === 'checkbox' &&
                  getGroupCheckboxState(option as OptionsGroup) === 'all',
                'form-multi-select-indeterminate':
                  optionsGroupsSelectable &&
                  optionsGroupsStyle === 'checkbox' &&
                  getGroupCheckboxState(option as OptionsGroup) === 'indeterminate',
              })}
              key={index}
              {...(optionsGroupsSelectable &&
                optionsGroupsStyle === 'checkbox' && {
                  onClick: () => onGroupOnClick?.(option as OptionsGroup),
                  onKeyDown: (event: React.KeyboardEvent<HTMLDivElement>) =>
                    handleGroupKeyDown(event, option as OptionsGroup),
                  tabIndex: 0,
                  role: 'button',
                })}
            >
              {optionsGroupsTemplate ? optionsGroupsTemplate(option as OptionsGroup) : option.label}
            </div>
          )
        )
      ) : (
        <div className="form-multi-select-options-empty" role="status">
          {searchNoResultsLabel}
        </div>
      )

    return (
      <>
        {virtualScroller ? (
          <CVirtualScroller
            className="form-multi-select-options"
            onKeyDown={onKeyDown}
            visibleItems={visibleItems}
            ref={ref}
          >
            {createOptions(options)}
          </CVirtualScroller>
        ) : (
          <div
            className="form-multi-select-options"
            onKeyDown={onKeyDown}
            {...(optionsMaxHeight !== 'auto' && {
              style: { maxHeight: optionsMaxHeight, overflow: 'scroll' },
            })}
            ref={ref}
          >
            {createOptions(options)}
          </div>
        )}
        {loading && <CElementCover />}
      </>
    )
  }
)

CMultiSelectOptions.propTypes = {
  loading: PropTypes.bool,
  onGroupOnClick: PropTypes.func,
  onOptionOnClick: PropTypes.func,
  options: PropTypes.array.isRequired,
  optionsMaxHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  optionsStyle: PropTypes.oneOf(['checkbox', 'text']),
  optionsTemplate: PropTypes.func,
  optionsGroupsSelectable: PropTypes.bool,
  optionsGroupsStyle: PropTypes.oneOf(['checkbox', 'text']),
  optionsGroupsTemplate: PropTypes.func,
  searchNoResultsLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
  virtualScroller: PropTypes.bool,
  visibleItems: PropTypes.number,
}

CMultiSelectOptions.displayName = 'CMultiSelectOptions'
