import React, {
  FormEvent,
  forwardRef,
  HTMLAttributes,
  KeyboardEvent,
  ReactNode,
  useEffect,
  useState,
  useRef,
} from 'react'

import classNames from 'classnames'
import PropTypes from 'prop-types'

import { CMultiSelectCleaner } from './CMultiSelectCleaner'
import { CMultiSelectNativeSelect } from './CMultiSelectNativeSelect'
import { CMultiSelectOptions } from './CMultiSelectOptions'
import { CMultiSelectSearchInput } from './CMultiSelectSearchInput'
import { CMultiSelectSelectAllButton } from './CMultiSelectSelectAllButton'
import { CMultiSelectSelection } from './CMultiSelectSelection'

import { useForkedRef } from '../../utils/hooks'
export interface CMultiSelectProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
  /**
   * A string of all className you want applied to the base component.
   */
  className?: string
  /**
   * Enables selection cleaner element.
   */
  cleaner?: boolean
  /**
   * It specifies that multiple options can be selected at once.
   */
  multiple?: boolean
  /**
   * Execute a function when a user changes the selected option.
   */
  onChange?: (selected: Option[]) => void
  /**
   * List of option elements.
   */
  options: Option[]
  /**
   * Sets maxHeight of options list.
   */
  optionsMaxHeight?: number | string
  /**
   * Sets option style.
   */
  optionsStyle?: 'checkbox' | 'text'
  /**
   * Specifies a short hint that is visible in the search input.
   */
  placeholder?: string
  /**
   * Enables search input element.
   */
  search?: boolean
  /**
   * Sets the label for no results when filtering.
   */
  searchNoResultsLabel?: string | ReactNode
  /**
   * Enables select all button.
   */
  selectAll?: boolean
  /**
   * Sets the select all button label.
   */
  selectAllLabel?: string | ReactNode
  /**
   * Sets the selection style.
   */
  selectionType?: 'counter' | 'tags' | 'text'
  /**
   * Sets the counter selection label.
   */
  selectionTypeCounterText?: string
  /**
   * Toggle the visibility of multi select dropdown.
   */
  visible?: boolean
}

export interface Option {
  disabled?: boolean
  label?: string
  options?: Option[]
  order?: number
  selected?: boolean
  // TODO: change to label
  text: string
  value: number | string
}

const flattenArray = (options: Option[]): Option[] => {
  return options.reduce((acc: Option[], val: Option) => {
    return acc.concat(Array.isArray(val.options) ? flattenArray(val.options) : val)
  }, [])
}

const getSelectedOptions = (options: Option[]) => {
  return flattenArray(options).filter((option: Option) => {
    if (option.selected) {
      return option
    }
    return
  })
}

export const CMultiSelect = forwardRef<HTMLDivElement, CMultiSelectProps>(
  (
    {
      className,
      cleaner = true,
      multiple = true,
      onChange,
      options,
      optionsMaxHeight = 'auto',
      optionsStyle = 'checkbox',
      placeholder = 'Select...',
      search = true,
      searchNoResultsLabel = 'No results found',
      selectAll = true,
      selectAllLabel = 'Select all options',
      selectionType = 'tags',
      selectionTypeCounterText = 'item(s) selected',
      visible = false,
    },
    ref,
  ) => {
    const multiSelectRef = useRef<HTMLDivElement>(null)
    const forkedRef = useForkedRef(ref, multiSelectRef)

    const nativeSelectRef = useRef<HTMLSelectElement>(null)
    const searchRef = useRef<HTMLInputElement>(null)

    const [_options, setOptions] = useState<Option[]>(options)
    const [_vOptions, setVOptions] = useState<Option[]>(options)
    const [_search, setSearch] = useState('')
    const [_visible, setVisible] = useState(visible)

    const [selected, setSelected] = useState<Option[]>([])
    const [count, setCount] = useState<number>(0)

    useEffect(() => {
      const selected = _options && getSelectedOptions(_options)

      selected.sort((a: Option, b: Option) => {
        if (typeof a.order === 'undefined') {
          return -1
        }
        if (b.order && a.order > b.order) return 1
        if (b.order && a.order < b.order) return -1
        return 0
      })
      setSelected(selected)
    }, [_options, options])

    useEffect(() => {
      setVOptions(filterOptionsList(_search, _options))
    }, [_options, _search])

    useEffect(() => {
      nativeSelectRef.current &&
        nativeSelectRef.current.dispatchEvent(new Event('change', { bubbles: true }))
    }, [selected])

    useEffect(() => {
      window.addEventListener('click', handleClickOutside)
      window.addEventListener('keyup', handleKeyup)

      return () => {
        window.removeEventListener('click', handleClickOutside)
        window.removeEventListener('keyup', handleKeyup)
      }
    })

    const _className = classNames(
      'form-multi-select',
      {
        show: _visible,
        'form-multi-select-selection-tags': multiple && selectionType === 'tags',
      },
      className,
    )

    const updateOptions = (value: number | string, options: Option[] = _options): Option[] => {
      return multiple && options
        ? options &&
            options.map((option: Option) => {
              setCount(count + 1)
              return option.options
                ? { ...option, options: updateOptions(value, option.options) }
                : option.value == value // TODO: find solution
                ? { ...option, selected: !option.selected, order: count }
                : { ...option }
            })
        : options &&
            options.map((option: Option) => {
              return option.options
                ? { ...option, options: updateOptions(value, option.options) }
                : option.value == value // TODO: find solution
                ? { ...option, selected: true }
                : { ...option, selected: false }
            })
    }

    const toggleAllOptions = (
      options: Option[],
      selected: boolean,
      counter: number = count,
    ): Option[] => {
      return options.map((option: Option) => {
        !option.selected && counter++
        setCount(counter)
        if (option.options) {
          return {
            ...option,
            options: toggleAllOptions(option.options, selected, counter),
          }
        }
        return option.disabled
          ? { ...option }
          : selected && !option.selected
          ? { ...option, selected: selected, order: counter }
          : { ...option, selected: selected }
      })
    }

    const filterOptionsList = (search: string, options = _vOptions) => {
      return search.length
        ? options &&
            options.reduce((acc: Option[], val: Option) => {
              const options =
                val.options &&
                val.options.filter(
                  (element) =>
                    element.text && element.text.toLowerCase().includes(search.toLowerCase()),
                )

              if (
                (val.text && val.text.toLowerCase().includes(search.toLowerCase())) ||
                (options && options.length)
              ) {
                acc.push(Object.assign({}, val, options && options.length && { options }))
              }

              return acc
            }, [])
        : _options
    }

    const handleKeyup = (event: Event) => {
      if (multiSelectRef.current && !multiSelectRef.current.contains(event.target as HTMLElement)) {
        setVisible(false)
      }
    }
    const handleClickOutside = (event: Event) => {
      if (multiSelectRef.current && !multiSelectRef.current.contains(event.target as HTMLElement)) {
        setVisible(false)
      }
    }

    const handleSearchChange = (event: FormEvent<HTMLInputElement>) => {
      const target = event.target as HTMLInputElement
      setSearch(target.value.toLowerCase())
    }

    const handleSearchKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
      if (_search.length) return

      if (event.key === 'Backspace' || event.key === 'Delete') {
        const last = selected.filter((option: Option) => !option.disabled).pop()
        last && setSelected(selected.filter((option: Option) => option.value !== last.value))
        last && setOptions(updateOptions(last.value))
      }
    }

    const handleOptionOnClick = (option: Option) => {
      setOptions(updateOptions(option.value))
      if (!multiple) {
        setVisible(false)
        setSearch('')
        if (searchRef.current) {
          searchRef.current.value = ''
        }
      }
    }

    const handleSelectAll = () => {
      setOptions(toggleAllOptions(_options, true))
    }

    const handleDeselectAll = () => {
      setOptions(toggleAllOptions(_options, false))
    }

    return (
      <>
        <CMultiSelectNativeSelect
          multiple={multiple}
          options={_options}
          value={
            multiple
              ? selected.map((option: Option) => option.value.toString())
              : selected.map((option: Option) => option.value)[0]
          }
          onChange={() => onChange && onChange(selected)}
          ref={nativeSelectRef}
        />
        <div className={_className} ref={forkedRef}>
          <div
            onClick={() => {
              setVisible(true)
              search && searchRef.current?.focus()
            }}
          >
            <CMultiSelectSelection
              multiple={multiple}
              onRemove={(option) => handleOptionOnClick(option)}
              search={search}
              selected={selected}
              selectionType={selectionType}
              selectionTypeCounterText={selectionTypeCounterText}
            />
            {multiple && cleaner && selected.length > 0 && (
              <CMultiSelectCleaner onClick={() => handleDeselectAll()} />
            )}
            {search && (
              <CMultiSelectSearchInput
                {...(selected.length === 0 && { placeholder: placeholder })}
                {...(selected.length &&
                  selectionType === 'counter' && {
                    placeholder: `${selected.length} ${selectionTypeCounterText}`,
                  })}
                {...(selected.length &&
                  !multiple && { placeholder: selected.map((option) => option.text)[0] })}
                onChange={(event) => handleSearchChange(event)}
                onKeyDown={(event) => handleSearchKeyDown(event)}
                {...(multiple &&
                  selected.length &&
                  selectionType !== 'counter' && { size: _search.length + 2 })}
                ref={searchRef}
              />
            )}
          </div>
          <div className="form-multi-select-dropdown">
            {multiple && selectAll && (
              <CMultiSelectSelectAllButton onClick={() => handleSelectAll()}>
                {selectAllLabel}
              </CMultiSelectSelectAllButton>
            )}
            <CMultiSelectOptions
              handleOptionOnClick={(option) => handleOptionOnClick(option)}
              options={_vOptions}
              optionsMaxHeight={optionsMaxHeight}
              optionsStyle={optionsStyle}
              searchNoResultsLabel={searchNoResultsLabel}
            />
          </div>
        </div>
      </>
    )
  },
)

CMultiSelect.propTypes = {
  className: PropTypes.string,
  cleaner: PropTypes.bool,
  multiple: PropTypes.bool,
  onChange: PropTypes.func,
  options: PropTypes.array.isRequired,
  optionsMaxHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  optionsStyle: PropTypes.oneOf(['checkbox', 'text']),
  placeholder: PropTypes.string,
  search: PropTypes.bool,
  searchNoResultsLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
  selectAll: PropTypes.bool,
  selectAllLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
  selectionType: PropTypes.oneOf(['counter', 'tags', 'text']),
  selectionTypeCounterText: PropTypes.string,
  visible: PropTypes.bool,
}

CMultiSelect.displayName = 'CMultiSelect'
