import React, { createContext, forwardRef, useState, useCallback, useContext, useEffect, useMemo, useRef } from "react"
import PropTypes from "prop-types"
import {
  useFloating,
  useClick,
  useDismiss,
  useRole,
  useListItem,
  useListNavigation,
  useInteractions,
  useId,
  FloatingFocusManager,
  useTypeahead,
  flip,
  offset,
  size,
  autoUpdate,
  FloatingList,
  useMergeRefs,
} from "@floating-ui/react"
import cx from "classnames"
import { omit } from "lodash"
import Label from "../label"
import CKIcon from "../ck-icon"
import Portal from "../portal"

const DEFAULT_BTN_CLASSES = "inline-flex items-center border transition-colors delay-75 text-sm gap-2 justify-between rounded select-none pl-3 pr-2"
export const MENU_CLASSES = "bg-white p-2 shadow-lg outline-none overflow-auto border border-gray-200 rounded z-10 overflow-x-hidden"
export const MENU_OFFSET = 4
export const MAX_MENU_HEIGHT = 300
export const MIN_MENU_WIDTH = 192

export const SelectParent = forwardRef((props, ref) => {
  const { options, onChange, name, ...otherProps } = props
  const selectableOptions = options.filter(option => option.value !== undefined)
  const selected = selectableOptions.find(option => option.value === props.value)

  const handleChange = (index) => {
    if(onChange) onChange({ target: { ...selectableOptions[index], name } })
  }

  return (
    <Select {...otherProps} onChange={handleChange} ref={ref}>
      {options.map((option, index) => {
        if(option.value === undefined) return <div key={index}>{option}</div>

        return <Option key={`${option.label}-${option.value}`} selected={selected?.value === option.value} label={option.label}>{option.component || option.label}</Option>
      })}
    </Select>
  )
})

export const SelectContext = createContext({})

const excludeSeparators = child => child.props?.children?.props?.role !== "separator"

export const Select = forwardRef(({
  label,
  children,
  className,
  onChange,
  disabled,
  placeholder = "Select",
  ...props
}, forwardedRef) => {
  const labelId = useId()
  const [isOpen, setIsOpen] = useState(false)
  const [activeIndex, setActiveIndex] = useState(null)
  const childNodes = useMemo(
    () => React.Children.toArray(children).filter(excludeSeparators),
    [children]
  )

  const [selectedIndex, setSelectedIndex] = useState(() => {
    const selected = childNodes.findIndex(child => child.props.selected)
    return selected === -1 ? null : selected
  })
  const [selectedLabel, setSelectedLabel] = useState(() => {
    if(selectedIndex !== null) {
      return childNodes[selectedIndex].props.label || childNodes[selectedIndex].props.children
    }
    return null
  })

  // Update selected label when selected index changes
  useEffect(() => {
    const newIndex = childNodes.findIndex(child => child.props.selected)
    if(newIndex === -1) return
    setSelectedIndex(newIndex)
    setSelectedLabel(childNodes[newIndex].props.label || childNodes[newIndex].props.children)
  }, [childNodes])

  const elementsRef = useRef([])
  const labelsRef = useRef([])

  const { refs, floatingStyles, context } = useFloating({
    placement: "bottom-start",
    open: isOpen,
    onOpenChange: setIsOpen,
    whileElementsMounted: autoUpdate,
    middleware: [
      flip(),
      size({
        apply({ rects, elements }) {
          Object.assign(elements.floating.style, {
            minWidth: `${rects.reference.width}px`,
          })
        },
      }),
      offset(MENU_OFFSET)
    ],
  })

  const handleSelect = useCallback((index) => {
    setSelectedIndex(index)
    if(onChange) {
      onChange(index)
    }
    setIsOpen(false)
    if (index !== null) {
      setSelectedLabel(labelsRef.current[index])
    }
  }, [onChange])

  const handleTypeaheadMatch = index => {
    if (isOpen) {
      setActiveIndex(index)
    } else {
      handleSelect(index)
    }
  }

  const listNav = useListNavigation(context, {
    listRef: elementsRef,
    activeIndex,
    selectedIndex,
    onNavigate: setActiveIndex
  })
  const typeahead = useTypeahead(context, {
    listRef: labelsRef,
    activeIndex,
    selectedIndex,
    onMatch: handleTypeaheadMatch,
    findMatch: (list, query) => {
      // Overrides the default findMatch to match strings within the label and not just the start
      // It also gracefully handles the case where the label is a React element
      return list.find(label => {
        const labelString = typeof label === "string" ? label : label?.props?.children || ""
        return labelString.toLowerCase().includes(query)
      })
    }
  })
  const click = useClick(context)
  const dismiss = useDismiss(context)
  const role = useRole(context, { role: "listbox" })

  const interactions = [listNav, dismiss, role].concat(disabled ? [] : [click, typeahead])

  const {
    getReferenceProps,
    getFloatingProps,
    getItemProps
  } = useInteractions(interactions)

  const selectContext = React.useMemo(
    () => ({
      activeIndex,
      selectedIndex,
      setSelectedIndex,
      getItemProps,
      handleSelect
    }),
    [activeIndex, selectedIndex, getItemProps, handleSelect]
  )

  const onLabelClick = () => {
    refs.reference.current.focus()
  }

  const btnProps = omit(props, ["aria-label", "aria-labelledby", "error", "value"])
  const btnClassName = cx(
    DEFAULT_BTN_CLASSES,
    className,
    {
      "text-gray-600": !selectedLabel,
      "text-gray-900": selectedLabel,
      "border-blue-500": isOpen && !props.error,
      "border-gray-200 hover:border-gray-300": !isOpen && !props.error,
      "border-red-500": props.error,
      "bg-gray-50": disabled
    }
  )

  return (
    <>
      {label && (
        <Label as="div" className="mb-1 cursor-default" id={labelId} onClick={onLabelClick}>{label}</Label>
      )}
      <div
        ref={useMergeRefs([refs.setReference, forwardedRef])}
        tabIndex={0}
        {...getReferenceProps()}
        aria-labelledby={label ? labelId : props["aria-labelledby"]}
        aria-label={!label ? props["aria-label"] : null}
        aria-disabled={disabled}
        className={btnClassName}
        style={{ paddingBlock: "7px" }}
        {...btnProps}
      >
        <div>{selectedLabel ?? placeholder}</div>
        <CKIcon name="down" className={cx("duration-75 transition-transform transform text-gray-800", { "rotate-180" : isOpen })} aria-hidden="true" />
      </div>
      <SelectContext.Provider value={selectContext}>
        {isOpen && (
          <FloatingFocusManager context={context} modal={false}>
            <Portal>
              <div
                ref={refs.setFloating}
                style={{ ...floatingStyles, maxHeight: MAX_MENU_HEIGHT }}
                className={MENU_CLASSES}
                aria-label="Options"
                {...getFloatingProps()}
              >
                <FloatingList elementsRef={elementsRef} labelsRef={labelsRef}>
                  {children}
                </FloatingList>
              </div>
            </Portal>
          </FloatingFocusManager>
        )}
      </SelectContext.Provider>
    </>
  )
})

export const Option = ({ children, label, disabled, create, ...props }) => {
  const {
    activeIndex,
    selectedIndex,
    getItemProps,
    handleSelect
  } = useContext(SelectContext)

  const { ref, index } = useListItem({ label: disabled ? null : label || children })

  const isActive = activeIndex === index
  const isSelected = selectedIndex === index

  return (
    <button
      ref={ref}
      {...props}
      role="option"
      aria-selected={isSelected}
      aria-disabled={disabled}
      tabIndex={isActive && !disabled ? 0 : -1}
      className={cx("block w-full p-2 text-left text-sm focus:outline-none", props.className, {
        "relative border-t border-gray-200 pt-4 px-4 mt-2": create,
        "rounded": !create,
        "text-gray-900": !disabled,
        "bg-gray-50": isActive && !disabled && !create,
        "text-gray-500 cursor-default": disabled,
      })}
      style={create ? { left: -8, width: "calc(100% + 16px)" } : {}}
      {...getItemProps({
        onClick: () => {
          if(disabled) return
          handleSelect(index)
        }
      })}
    >
      {isSelected && <CKIcon name="check" className="float-right" aria-hidden="true" />}
      {children}
    </button>
  )
}

Select.propTypes = {
  className: PropTypes.string,
  label: PropTypes.string,
  "aria-label": PropTypes.string,
  "aria-labelledby": PropTypes.string,
  error: PropTypes.bool,
  disabled: PropTypes.bool,
  onChange: PropTypes.func,
  children: PropTypes.node,
  placeholder: PropTypes.string
}

Option.propTypes = {
  children: PropTypes.node,
  disabled: PropTypes.bool,
  selected: PropTypes.bool,
  className: PropTypes.string,
  create: PropTypes.bool,
  label: PropTypes.node,
}
