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

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

const DEFAULT_FILTER = (options, inputValue, selectedIndex) => {
  const input = inputValue.toLowerCase()
  if (!input) return options
  if (selectedIndex !== -1) return options
  return options.filter((option) => {
    const labelString = typeof option.label === "string" ? option.label : option.label?.props?.children
    
    return labelString.toLowerCase().includes(input)
  })
}

const AutoComplete = forwardRef(
  (
    {
      async,
      value,
      options: items,
      className,
      label,
      placeholder = "Select",
      filter = DEFAULT_FILTER,
      create,
      disabled,
      ...props
    },
    forwardedRef
  ) => {
    if (async && typeof items !== "function") {
      throw new Error("Async autocomplete requires options to be a function")
    }
    const [options, setOptions] = useState(async ? [] : items)
    const [loading, setLoading] = useState(false)
    const [isOpen, setIsOpen] = useState(false)
    const [inputValue, setInputValue] = useState(() => {
      const selectedObject = options.find((option) => option.value === value)
      const selectedLabel = value?.label || selectedObject?.label
      const valueString = typeof selectedLabel === "string" ? selectedLabel : selectedLabel?.props?.children

      return valueString || ""
  })
    const [activeIndex, setActiveIndex] = useState(null)
    const selectedIndex = options.findIndex(
      (option) => option.label === inputValue
    )
    const filteredOptions = filter(options, inputValue, selectedIndex)
    const labelId = useId()
    const createPrefix = typeof create === "string" ? create : "Create"
    const createCount = useRef(0)

    const elementsRef = useRef([])

    const loadOptions = React.useMemo(
      () =>
        debounce(() => {
          if (inputValue.length < 2) return
          const abortController = new AbortController()
          setLoading(true)
          items(inputValue, { signal: abortController.signal })
            .then((opts) => {
              if (opts) setOptions(opts)
            })
            .catch((error) => {
              if (error.code === DOMException.AbortError) {
                // cancelled, ignore
                return
              }
              throw error
            })
            .finally(() => {
              setLoading(false)
            })
        }, 300),
      [items, inputValue]
    )

    useEffect(() => {
      if (!async) return
      loadOptions()
    }, [async, loadOptions])

    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 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) => {
        const selectedOption = filteredOptions[index] || {
          label: inputValue,
          value: `${inputValue}${++createCount.current}`,
          created: true,
        }
        if (
          selectedOption.created &&
          !options.find((option) => option.label === selectedOption.label)
        ) {
          setOptions([...options, selectedOption])
        }
        const labelString = typeof selectedOption.label === "string" ? selectedOption.label : selectedOption.label.props.children
        setInputValue(labelString)
        if (props.onChange) {
          props.onChange({ target: { ...selectedOption, name: props.name } })
        }
        setIsOpen(false)
        refs.domReference.current?.focus()
      },
      [filteredOptions, refs.domReference, props, inputValue, options]
    )

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

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

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

        if (value) {
          setActiveIndex(0)
        }
      },
      [isOpen]
    )

    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)
        }
      },
      [activeIndex, handleSelect]
    )

    const onClick = useCallback(() => {
      if (!isOpen) refs.reference.current.select()

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

    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": disabled
    })

    return (
      <>
        {label && (
          <Label
            as="div"
            className="cursor-default"
            id={labelId}
            onClick={onLabelClick}
          >
            {label}
          </Label>
        )}
        <div className={inputClassName}>
          <input
            {...getReferenceProps({
              ref: useMergeRefs([refs.setReference, forwardedRef]),
              onChange,
              onClick,
              onKeyDown,
              value: inputValue,
              placeholder,
              "aria-autocomplete": "list",
            })}
            aria-labelledby={label ? labelId : props["aria-labelledby"]}
            aria-label={!label ? props["aria-label"] : null}
            className="border-none w-full h-full bg-transparent focus:outline-none pl-3 pr-12 placeholder-gray-600"
            style={{ minWidth: placeholder.length + 2 + "ch" }}
            disabled={disabled}
          />
          <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,
                  }}
                  className={MENU_CLASSES}
                  aria-label="Options"
                  {...getFloatingProps()}
                >
                  <FloatingList elementsRef={elementsRef}>
                    {loading && filteredOptions.length < 1 && (
                      <Option disabled className="text-center text-gray-600">
                        Loading...
                      </Option>
                    )}
                    {filteredOptions.length === 0 && !loading && (
                      <Empty
                        create={create}
                        createPrefix={createPrefix}
                        input={inputValue}
                      />
                    )}
                    {filteredOptions.map((option) => (
                      <Option key={`${option.value}-${option.label}`}>
                        {option.component || option.label}
                      </Option>
                    ))}
                    {create &&
                      inputValue.length > 1 &&
                      filteredOptions.length !== 0 &&
                      !filteredOptions.find(
                        (option) =>
                          option.label.toLowerCase() ===
                          inputValue.toLowerCase()
                      ) && (
                        <Option create>
                          {createPrefix} &ldquo;{inputValue}&rdquo;
                        </Option>
                      )}
                  </FloatingList>
                </div>
              </Portal>
            </FloatingFocusManager>
          )}
        </SelectContext.Provider>
      </>
    )
  }
)

function Empty({ create, createPrefix, input }) {
  if (!create)
    return (
      <Option disabled 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>
      {input.length > 0 && (
        <Option create>
          {createPrefix} &ldquo;{input}&rdquo;
        </Option>
      )}
    </div>
  )
}

export default AutoComplete
