import React, { forwardRef, useCallback, useEffect, useContext, useRef, useState } from "react"
import {
  autoUpdate,
  size,
  flip,
  offset,
  useDismiss,
  useFloating,
  useId,
  useInteractions,
  useListItem,
  useListNavigation,
  useMergeRefs,
  useRole,
  FloatingFocusManager,
  FloatingList,
} from "@floating-ui/react"
import cx from "classnames"
import Label from "../label"
import CKIcon from "../ck-icon"
import Portal from "../portal"
import Checkbox from "../checkbox"
import { SelectContext, MENU_CLASSES, MAX_MENU_HEIGHT, MIN_MENU_WIDTH, MENU_OFFSET } from "../select-menu/select-menu"

const DEFAULT_INPUT_CLASSES = "relative inline-flex items-center border transition-colors delay-75 text-sm gap-2 rounded pr-8"

const filteredMatch = (options, value) => options.findIndex(option => option.label.toLowerCase().includes(value.toLowerCase()))

const MultiSelectInput = forwardRef(({
  options: items,
  label,
  value,
  placeholder = "Select",
  create,
  className,
  ...props
}, forwardedRef) => {
  const [isOpen, setIsOpen] = useState(false)
  const [inputValue, setInputValue] = useState("")
  const [options, setOptions] = useState(items)
  const [selected, setSelected] = useState(options.filter(option => value?.includes(option.value)) || [])
  const [activeIndex, setActiveIndex] = useState(null)
  const labelId = useId()
  const createPrefix = typeof create === "string" ? create : "Create"
  const createCount = useRef(0)
  const filteredOptions = options.filter(option => option.label.toLowerCase().includes(inputValue.toLowerCase()))

  // Update selected options when value prop changes
  useEffect(() => {
    setSelected(options.filter(option => value?.includes(option.value)) || [])
  }, [value, options])

  useEffect(() => {
    setOptions(items)
  }, [items])

  const elementsRef = 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, {
            width: `${rects.reference.width}px`,
          })
        }
      }),
      offset(MENU_OFFSET)
    ]
  })

  const role = useRole(context, { role: "listbox" })
  const dismiss = useDismiss(context)
  const listNav = useListNavigation(context, {
    listRef: elementsRef,
    activeIndex,
    onNavigate: setActiveIndex,
    virtual: true,
    loop: true
  })

  const {
    getReferenceProps,
    getFloatingProps,
    getItemProps
  } = useInteractions([role, dismiss, listNav])

  const handleSelect = useCallback(index => {
    if(index === -1) return
    let selections
    const newOption = filteredOptions[index] || { label: inputValue, value: `${inputValue}${++createCount.current}`, created: true }
    if(selected.find(option => option.value === newOption.value)) {
      selections = selected.filter(option => option.value !== newOption.value)
    } else {
      // Add in the same order as the options
      const selectedOptions = [...selected, newOption]
      if(!newOption.created) {
        selections = options.filter(option => selectedOptions.find(opt => opt.value === option.value))
      } else {
        selections = selectedOptions
        if(!options.find(option => option.value === newOption.value)) setOptions([...options, { label: newOption.label, value: newOption.value }])
      }
    }
    setSelected(selections)
    if(props.onChange) {
      props.onChange({ target: { name: props.name, value: selections } })
    }
    setInputValue("")
    const input = refs.reference.current.querySelector("input")
    input.focus()
  }, [options, filteredOptions, refs.reference, selected, props, inputValue])

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

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

  const onChange = event => {
    const value = event.target.value
    setInputValue(value)
    if(!isOpen) setIsOpen(true)

    const filtered = options.filter(option => option.label.toLowerCase().includes(value.toLowerCase()))
    const match = filteredMatch(filtered, value)
    const fallback = options.length
    const indexPosition = match > -1 ? match : fallback
    setActiveIndex(indexPosition)
  }

  const onKeyDown = useCallback(event => {
    if (
      event.key === "Enter" &&
      activeIndex != null
    ) {
      /*
        We are using a virtual navigation to keep focus on the input (see useListNavigation hook above)
        With a virtual navigation, interactions like Enter will not trigger selection/click/onchange.
        Hence, we need to call handleSelect below to trigger onChange callback during the input keydown event.
      */
      handleSelect(activeIndex)
    } else if (event.key === "Backspace" && !inputValue) {
      if(props.onChange) {
        props.onChange({ target: { name: props.name, value: selected.slice(0, -1) } })
      }
      setSelected(selected.slice(0, -1))
    }
  }, [activeIndex, handleSelect, inputValue, selected, props])

  const onClick = useCallback(() => {
    if(props.disabled) return
    const input = refs.reference.current.querySelector("input")
    if(input && !isOpen) input.select()

    setIsOpen(!isOpen)
  }, [isOpen, refs.reference, props.disabled])

  const inputClassName = cx(
    DEFAULT_INPUT_CLASSES,
    className,
    {
      "text-gray-600": !inputValue,
      "text-gray-900": inputValue,
      "border-blue-500": isOpen && !props.error,
      "border-gray-200 hover:border-gray-300": !isOpen && !props.error,
      "border-red-500": props.error,
      "bg-gray-50": props.disabled,
    }
  )

  const removePill = (pill) => {
    if(props.disabled) return
    const newSelections = selected.filter(option => option.value !== pill.value)
    if(props.onChange) {
      props.onChange({ target: { name: props.name, value: newSelections } })
    }
    setSelected(newSelections)
  }

  return (
    <>
      {label && (
        <Label as="div" className="mb-1 cursor-default" id={labelId} onClick={onLabelClick}>{label}</Label>
      )}
      <div
        className={inputClassName}
        style={{ paddingBlock: 7 }}
        aria-labelledby={label ? labelId : props["aria-labelledby"]}
        aria-label={!label ? props["aria-label"] : "select multiple options"}
        aria-disabled={props.disabled || false}
        {...getReferenceProps({
          ref: useMergeRefs([refs.setReference, forwardedRef]),
          onClick
        })}
      >
        <div className="flex flex-wrap gap-1 items-center pl-2 w-full" role="list">
          {selected.map((pill) => {
            const option = options.find(option => option.value === pill.value)
            return (
              <div key={`${option.label}-${option.value}`} className={cx("inline-flex gap-1 items-center pl-2 pr-1 py-1 bg-gray-100 rounded-full h-6 whitespace-no-wrap max-w-full", { "opacity-75": props.disabled })} role="listitem">
                <span className="text-gray-800 text-sm leading-relaxed max-w-full truncate">{option.label}</span>
                <button
                  onClick={() => removePill(pill)}
                  aria-label="Remove pill"
                  className={cx("grid place-content-center border-none bg-gray-200 hover:bg-gray-300 w-4 h-4 rounded-full text-gray-700", { "pointer-events-none": props.disabled })}>
                  <CKIcon name="close-mini" size="sm" aria-hidden="true" />
                </button>
              </div>
            )
          })}
          <input
            className="border-none h-6 bg-transparent focus:outline-none placeholder-gray-600"
            style={{ maxWidth: placeholder.length + 2 + "ch" }}
            placeholder={selected.length ? "" : placeholder}
            value={inputValue}
            onKeyDown={onKeyDown}
            onChange={onChange}
            aria-autocomplete="list"
            disabled={props.disabled || false}
          />
        </div>
        <CKIcon name="down" className={cx("absolute right-0 mr-2 w-6 duration-75 transition-transform pointer-events-none transform text-gray-800", { "rotate-180" : isOpen })} aria-hidden="true" />
      </div>
      <SelectContext.Provider value={selectContext}>
        {isOpen && (
          <FloatingFocusManager
            context={context}
            initialFocus={-1}
            modal={false}
          >
            <Portal>
              <div
                ref={refs.setFloating}
                style={{ ...floatingStyles, maxHeight: MAX_MENU_HEIGHT, minWidth: MIN_MENU_WIDTH }}
                className={MENU_CLASSES}
                aria-label="Options"
                {...getFloatingProps()}
              >
                <FloatingList elementsRef={elementsRef}>
                  {filteredOptions.length === 0 && (
                    <Empty
                      create={create}
                      createPrefix={createPrefix}
                      input={inputValue}
                    />
                  )}
                  {filteredOptions.map((option) => (
                    <Option
                      key={`${option.value}-${option.label}`}
                      value={option.value}
                      disabled={option.disabled || false}
                    >
                      {option.label}
                    </Option>
                  ))}
                  {create && inputValue.length > 0 && filteredOptions.length > 0 && !filteredOptions.find(opt => opt.label.toLowerCase() === inputValue.toLowerCase()) && (
                    <Option create>
                      {createPrefix} &ldquo;{inputValue}&rdquo;
                    </Option>
                  )}
                </FloatingList>
              </div>
            </Portal>
          </FloatingFocusManager>
        )}
      </SelectContext.Provider>
    </>
  )
})

export default MultiSelectInput

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

  const { ref, index } = useListItem({ label: disabled ? null : label || children })
  const checked = Boolean(selected.find(option => option.value === props.value))

  const isActive = activeIndex === index
  const id = `${props.value}-${index}`

  const handleChange = () => {
    if(disabled) return
    handleSelect(index)
  }

  if(create) return (
    <button
      ref={ref}
      {...props}
      role="option"
      aria-selected={checked}
      aria-disabled={disabled}
      tabIndex={isActive && !disabled ? 0 : -1}
      className={cx("flex gap-2 items-center w-full p-2 text-left text-sm focus:outline-none relative border-t border-gray-200 pt-4 px-4 mt-2", props.className, {
        "text-gray-900": !disabled,
        "text-gray-500 cursor-default": disabled,
      })}
      style={{ left: -8, width: "calc(100% + 16px)" }}
      {...getItemProps()}
      onClick={handleChange}
    >
      {children}
    </button>
  )

  return (
    <label
      ref={ref}
      {...props}
      role="option"
      aria-selected={checked}
      aria-disabled={disabled}
      tabIndex={isActive && !disabled ? 0 : -1}
      htmlFor={id}
      className={cx("flex gap-2 items-center w-full p-2 text-left text-sm focus:outline-none rounded", props.className, {
        "rounded": !create,
        "text-gray-900": !disabled,
        "bg-gray-50": isActive && !disabled,
        "text-gray-500 cursor-default": disabled,
      })}
      {...getItemProps()}
    >
      {!empty && <Checkbox checked={checked} id={id} onChange={handleChange} className="flex-shrink-0" />}
      {children}
    </label>
  )
}

function Empty({ create, createPrefix, input }) {
  if (!create) return (
    <Option disabled empty className="text-center text-gray-600">
      No result found
    </Option>
  )
  return (
    <div>
      <div className="text-center text-sm text-gray-600 p-2 pointer-events-none">
        No result found
      </div>
      <Option create>
        {createPrefix} &ldquo;{input}&rdquo;
      </Option>
    </div>
  )
}
