import React, {
  forwardRef,
  useCallback,
  useEffect,
  useMemo,
  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 Icon from "../icon"
import Portal from "../portal"
import {
  SelectContext,
  MENU_CLASSES,
  MAX_MENU_HEIGHT,
  MENU_OFFSET,
} from "./select"
import Option from "./option"
import { jsxTextContent } from "./util"

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
  const result = options.filter((option) => {
    const labelString = jsxTextContent(option.label)
    if(!labelString) return false
    
    return labelString.toLowerCase().includes(input)
  })
  return result
}

const AutoComplete = forwardRef(({
    async,
    value,
    options: items,
    onChange,
    ...props
  }, ref) => {
    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 [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 handleFilter = (filterString) => {
      setInputValue(filterString)
    }

    const handleChange = event => {
      if(event.target.created) setOptions([...options, event.target])
      if(onChange) onChange(event)
    }

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

    const debouncedLoadOptions = useMemo(() => debounce(loadOptions, 300), [loadOptions])

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

    return (
      <ComposableAutocomplete
        {...props}
        ref={ref}
        searchValue={inputValue}
        state={{ loading }}
        onFilter={handleFilter}
        onChange={handleChange}
      >
        {options.map(option => (
          <Option key={`${option.value}-${option.label}`} value={option.value} label={option.label}>
            {option.component || option.label}
          </Option>
        ))}
      </ComposableAutocomplete>
    )
})

const ComposableAutocomplete = forwardRef(({
  children,
  className,
  label,
  searchValue,
  placeholder = "Select",
  filter = DEFAULT_FILTER,
  create,
  state = {},
  disabled,
  onFilter,
  ...props
}, forwardedRef) => {
  const [isOpen, setIsOpen] = useState(false)
  const childNodes = useMemo(
    () => React.Children.toArray(children),
    [children]
  )
  const [inputValue, setInputValue] = useState(() => {
    const index = childNodes.findIndex(child => child.props.selected)
    if(index < 0) return searchValue || ""

    const selectedLabel = childNodes[index].props.label || childNodes[index].props.children
    return searchValue || selectedLabel || ""
  })
  const selectedIndex = childNodes.findIndex(child => {
    const labelString = jsxTextContent(child.props.label || child.props.children)
    return labelString === inputValue
  })
  const [activeIndex, setActiveIndex] = useState(null)
  const createPrefix = typeof create === "string" ? create : "Create"

  // Allows to set the input value from the outside
  useEffect(() => {
    if (searchValue) {
      setInputValue(searchValue)
    }
  }, [searchValue])

  const filteredOptions = filter(
    childNodes.map((child, index) => ({
      label: child.props.label || child.props.children,
      value: child.props.value,
      position: index,
    })),
    inputValue,
    selectedIndex
  )
  const labelId = useId()

  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, {
            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 createdOption = {
        label: inputValue,
        value: inputValue,
        position: childNodes.length,
        created: true,
      }
      const selectedOption = filteredOptions[index] || createdOption
      if(!selectedOption) return
      const labelString = jsxTextContent(selectedOption.label)
      setInputValue(labelString)
      if (props.onChange) {
        props.onChange({ target: { ...selectedOption, name: props.name } })
      }
      setIsOpen(false)
      refs.domReference.current?.focus()
    },
    [filteredOptions, refs.domReference, props, inputValue, childNodes]
  )

  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)
      const newFilteredOptions = filter(
        childNodes.map((child, index) => ({
          label: child.props.label || child.props.children,
          value: child.props.value,
          position: index,
        })),
        value,
        selectedIndex
      )

      if (value) {
        setActiveIndex(0)
      }
      if(onFilter) {
        onFilter(value, newFilteredOptions)
      }
    },
    [isOpen, onFilter, childNodes, filter, selectedIndex]
  )

  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}
        />
        <Icon
          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}>
                  {filteredOptions.length === 0 && !state.loading && (
                    <Option disabled className="text-center text-gray-600">
                      No result found
                    </Option>
                  )}
                  {filteredOptions.length === 0 && state.loading && (
                    <Option disabled className="text-center text-gray-600">
                      Loading...
                    </Option>
                  )}
                  {filteredOptions.length > 0 && React.Children.map(children, (child, index) => {
                    const isFilteredOut = !filteredOptions.find((option) => option.position === index)
                    return isFilteredOut ? null : child
                  })}
                  {create &&
                    inputValue.length > 1 &&
                    !filteredOptions.find(
                      (option) =>
                        option.label.toLowerCase() ===
                        inputValue.toLowerCase()
                    ) && (
                      <Option create>
                        {createPrefix} &ldquo;{inputValue}&rdquo;
                      </Option>
                    )}
                </FloatingList>
              </div>
            </Portal>
          </FloatingFocusManager>
        )}
      </SelectContext.Provider>
    </>
  )
})

export { AutoComplete, ComposableAutocomplete }