/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2015 - present Instructure, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

import { Component, SyntheticEvent } from 'react'

import keycode from 'keycode'

import { isActiveElement } from '@instructure/ui-dom-utils'
import { createChainedFunction } from '@instructure/ui-utils'
import { logError as error } from '@instructure/console'
import type { SelectableProps } from './props'
import { allowedProps } from './props'

import { withDeterministicId } from '@instructure/ui-react-utils'

/**
---
category: components/utilities
tags: autocomplete, typeahead, combobox, dropdown, search
---
**/
@withDeterministicId()
class Selectable extends Component<SelectableProps> {
  static allowedProps = allowedProps

  static defaultProps = {
    isShowingOptions: false
  }

  _id = this.props.id || this.props.deterministicId!()
  _listId = `${this._id}-list`
  _descriptionId = `${this._id}-description`
  private _trigger: Element | null = null

  isSelectedOption = (id: string) => {
    const { selectedOptionId } = this.props

    if (Array.isArray(selectedOptionId)) {
      return selectedOptionId.indexOf(id) > -1
    }
    return selectedOptionId === id
  }

  handleOpenClose = (event: React.KeyboardEvent | React.MouseEvent) => {
    const { isShowingOptions, onRequestShowOptions, onRequestHideOptions } =
      this.props

    event.preventDefault()

    if (isShowingOptions) {
      onRequestHideOptions?.(event)
    } else {
      if (!isActiveElement(this._trigger)) {
        ;(this._trigger as HTMLElement)!.focus()
      }
      onRequestShowOptions?.(event)
    }
  }

  handleKeyDown = (event: React.KeyboardEvent) => {
    const {
      isShowingOptions,
      highlightedOptionId,
      onRequestHighlightOption,
      onRequestHighlightFirstOption,
      onRequestHighlightLastOption,
      onRequestSelectOption
    } = this.props

    const key = keycode.names[event.keyCode]

    switch (key) {
      case 'space':
        if (!isShowingOptions) {
          // if options hidden, show them
          this.handleOpenClose(event)
        }
        break
      case 'enter':
        if (highlightedOptionId) {
          // select highlighted option
          event.preventDefault()
          onRequestSelectOption?.(event, { id: highlightedOptionId })
        }
        break
      case 'down':
        event.preventDefault()
        if (isShowingOptions) {
          // if options showing, change highlight
          onRequestHighlightOption?.(event, { direction: 1 })
        } else {
          // otherwise, show options
          this.handleOpenClose(event)
        }
        break
      case 'up':
        event.preventDefault()
        if (isShowingOptions) {
          // if options showing, change highlight
          onRequestHighlightOption?.(event, { direction: -1 })
        } else {
          // otherwise, show options
          this.handleOpenClose(event)
        }
        break
      case 'home':
        if (isShowingOptions) {
          // if options showing, highlight first option
          event.preventDefault()
          onRequestHighlightFirstOption?.(event)
        }
        break
      case 'end':
        if (isShowingOptions) {
          // if options showing, highlight last option
          event.preventDefault()
          onRequestHighlightLastOption?.(event)
        }
        break
    }
  }

  handleKeyUp = (event: React.KeyboardEvent) => {
    const { isShowingOptions } = this.props
    const key = keycode.names[event.keyCode]

    if (key === 'esc') {
      if (isShowingOptions) {
        // if options showing, hide them
        this.handleOpenClose(event)
      }
    }
  }

  render() {
    const {
      isShowingOptions,
      highlightedOptionId,
      onRequestHighlightOption,
      onRequestSelectOption,
      children,
      render = children
    } = this.props

    if (typeof render === 'function') {
      return render({
        getRootProps: ({ onMouseDown, ...rest } = {}) => {
          return {
            onMouseDown: createChainedFunction((event: React.MouseEvent) => {
              // if we call preventDefault, label can't be selected and copied, so we only call it when the options are shown
              if (event.target !== this._trigger && isShowingOptions) {
                event.preventDefault() // prevent trigger from losing focus
              }
            }, onMouseDown),
            ...rest
          }
        },

        getLabelProps: (props) => {
          return {
            htmlFor: this._id,
            ...props
          }
        },

        getTriggerProps: ({
          ref,
          onKeyDown,
          onKeyUp,
          onClick,
          ...rest
        } = {}) => {
          return {
            id: this._id,
            ref: createChainedFunction(
              ref,
              (el: Element | null) => (this._trigger = el)
            )!,
            'aria-haspopup': 'listbox',
            'aria-expanded': isShowingOptions,
            'aria-controls': isShowingOptions ? this._listId : undefined,
            'aria-describedby': this._descriptionId,
            'aria-activedescendant': isShowingOptions
              ? highlightedOptionId
              : undefined,
            onKeyDown: createChainedFunction(this.handleKeyDown, onKeyDown),
            onKeyUp: createChainedFunction(this.handleKeyUp, onKeyUp),
            onClick: createChainedFunction(this.handleOpenClose, onClick),
            ...rest
          }
        },

        getInputProps: ({ readOnly, ...rest } = {}) => {
          return {
            role: 'combobox',
            'aria-autocomplete': readOnly ? 'none' : 'both',
            autoComplete: 'off',
            readOnly,
            ...rest
          }
        },

        getListProps: ({ onMouseDown, onClick, ...rest } = {}) => {
          return {
            id: this._listId,
            role: 'listbox',
            onMouseDown: createChainedFunction((event: React.MouseEvent) => {
              event.preventDefault() // prevent trigger from losing focus
            }, onMouseDown),
            onClick: createChainedFunction((event: SyntheticEvent) => {
              // prevent synthetic event from firing on the document
              // this event could inadvertently close a parent dialog
              event.stopPropagation()
              event.nativeEvent.stopImmediatePropagation()
            }, onClick),
            ...rest
          }
        },

        getOptionProps: ({ id, onMouseOver, onClick, ...rest } = {}) => {
          error(
            Boolean(id),
            `[Selectable] Must provide id for each option via \`getOptionProps\`.`
          )
          return {
            id,
            role: 'option',
            'aria-selected': this.isSelectedOption(id!) ? 'true' : 'false',
            onClick: createChainedFunction((event) => {
              onRequestSelectOption?.(event, { id })
            }, onClick),
            onMouseOver: createChainedFunction((event) => {
              onRequestHighlightOption?.(event, { id })
            }, onMouseOver),
            ...rest
          }
        },

        getDisabledOptionProps: (props) => {
          return {
            'aria-disabled': 'true',
            ...props
          }
        },

        getDescriptionProps: (props) => {
          return {
            id: this._descriptionId,
            ...props
          }
        }
      })
    } else {
      return null
    }
  }
}

export default Selectable
export { Selectable }
