<script>
  import { MENU_BUFFER } from '../constants'
  import { watchSize, setupResizeAndScrollEventListeners } from '../utils'
  import Option from './Option'
  import Tip from './Tip'

  const directionMap = {
    top: 'top',
    bottom: 'bottom',
    above: 'top',
    below: 'bottom',
  }

  export default {
    name: 'vue-treeselect--menu',
    inject: [ 'instance' ],

    computed: {
      menuStyle() {
        const { instance } = this

        return {
          maxHeight: instance.maxHeight + 'px',
        }
      },

      menuContainerStyle() {
        const { instance } = this

        return {
          zIndex: instance.appendToBody ? null : instance.zIndex,
        }
      },
    },

    watch: {
      'instance.menu.isOpen'(newValue) {
        if (newValue) {
          // In case `openMenu()` is just called and the menu is not rendered yet.
          this.$nextTick(this.onMenuOpen)
        } else {
          this.onMenuClose()
        }
      },
    },

    created() {
      this.menuSizeWatcher = null
      this.menuResizeAndScrollEventListeners = null
    },

    mounted() {
      const { instance } = this

      if (instance.menu.isOpen) this.$nextTick(this.onMenuOpen)
    },

    destroyed() {
      this.onMenuClose()
    },

    methods: {
      renderMenu() {
        const { instance } = this

        if (!instance.menu.isOpen) return null

        return (
          <div ref="menu" class="vue-treeselect__menu" onMousedown={instance.handleMouseDown} style={this.menuStyle}>
            {this.renderBeforeList()}
            {instance.async
              ? this.renderAsyncSearchMenuInner()
              : instance.localSearch.active
                ? this.renderLocalSearchMenuInner()
                : this.renderNormalMenuInner()}
            {this.renderAfterList()}
          </div>
        )
      },

      renderBeforeList() {
        const { instance } = this
        const beforeListRenderer = instance.$scopedSlots['before-list']

        return beforeListRenderer
          ? beforeListRenderer()
          : null
      },

      renderAfterList() {
        const { instance } = this
        const afterListRenderer = instance.$scopedSlots['after-list']

        return afterListRenderer
          ? afterListRenderer()
          : null
      },

      renderNormalMenuInner() {
        const { instance } = this

        if (instance.rootOptionsStates.isLoading) {
          return this.renderLoadingOptionsTip()
        } else if (instance.rootOptionsStates.loadingError) {
          return this.renderLoadingRootOptionsErrorTip()
        } else if (instance.rootOptionsStates.isLoaded && instance.forest.normalizedOptions.length === 0) {
          return this.renderNoAvailableOptionsTip()
        } else {
          return this.renderOptionList()
        }
      },

      renderLocalSearchMenuInner() {
        const { instance } = this

        if (instance.rootOptionsStates.isLoading) {
          return this.renderLoadingOptionsTip()
        } else if (instance.rootOptionsStates.loadingError) {
          return this.renderLoadingRootOptionsErrorTip()
        } else if (instance.rootOptionsStates.isLoaded && instance.forest.normalizedOptions.length === 0) {
          return this.renderNoAvailableOptionsTip()
        } else if (instance.localSearch.noResults) {
          return this.renderNoResultsTip()
        } else {
          return this.renderOptionList()
        }
      },

      renderAsyncSearchMenuInner() {
        const { instance } = this
        const entry = instance.getRemoteSearchEntry()
        const shouldShowSearchPromptTip = instance.trigger.searchQuery === '' && !instance.defaultOptions
        const shouldShowNoResultsTip = shouldShowSearchPromptTip
          ? false
          : entry.isLoaded && entry.options.length === 0

        if (shouldShowSearchPromptTip) {
          return this.renderSearchPromptTip()
        } else if (entry.isLoading) {
          return this.renderLoadingOptionsTip()
        } else if (entry.loadingError) {
          return this.renderAsyncSearchLoadingErrorTip()
        } else if (shouldShowNoResultsTip) {
          return this.renderNoResultsTip()
        } else {
          return this.renderOptionList()
        }
      },

      renderOptionList() {
        const { instance } = this

        return (
          <div class="vue-treeselect__list">
            {instance.forest.normalizedOptions.map(rootNode => (
              <Option node={rootNode} key={rootNode.id} />
            ))}
          </div>
        )
      },

      renderSearchPromptTip() {
        const { instance } = this

        return (
          <Tip type="search-prompt" icon="warning">{ instance.searchPromptText }</Tip>
        )
      },

      renderLoadingOptionsTip() {
        const { instance } = this

        return (
          <Tip type="loading" icon="loader">{ instance.loadingText }</Tip>
        )
      },

      renderLoadingRootOptionsErrorTip() {
        const { instance } = this

        return (
          <Tip type="error" icon="error">
            { instance.rootOptionsStates.loadingError }
            <a class="vue-treeselect__retry" onClick={instance.loadRootOptions} title={instance.retryTitle}>
              { instance.retryText }
            </a>
          </Tip>
        )
      },

      renderAsyncSearchLoadingErrorTip() {
        const { instance } = this
        const entry = instance.getRemoteSearchEntry()

        // TODO: retryTitle?

        return (
          <Tip type="error" icon="error">
            { entry.loadingError }
            <a class="vue-treeselect__retry" onClick={instance.handleRemoteSearch} title={instance.retryTitle}>
              { instance.retryText }
            </a>
          </Tip>
        )
      },

      renderNoAvailableOptionsTip() {
        const { instance } = this

        return (
          <Tip type="no-options" icon="warning">{ instance.noOptionsText }</Tip>
        )
      },

      renderNoResultsTip() {
        const { instance } = this

        return (
          <Tip type="no-results" icon="warning">{ instance.noResultsText }</Tip>
        )
      },

      onMenuOpen() {
        this.adjustMenuOpenDirection()
        this.setupMenuSizeWatcher()
        this.setupMenuResizeAndScrollEventListeners()
      },

      onMenuClose() {
        this.removeMenuSizeWatcher()
        this.removeMenuResizeAndScrollEventListeners()
      },

      adjustMenuOpenDirection() {
        const { instance } = this
        if (!instance.menu.isOpen) return

        const $menu = instance.getMenu()
        const $control = instance.getControl()
        const menuRect = $menu.getBoundingClientRect()
        const controlRect = $control.getBoundingClientRect()
        const menuHeight = menuRect.height
        const viewportHeight = window.innerHeight
        const spaceAbove = controlRect.top
        const spaceBelow = window.innerHeight - controlRect.bottom
        const isControlInViewport = (
          (controlRect.top >= 0 && controlRect.top <= viewportHeight) ||
          (controlRect.top < 0 && controlRect.bottom > 0)
        )
        const hasEnoughSpaceBelow = spaceBelow > menuHeight + MENU_BUFFER
        const hasEnoughSpaceAbove = spaceAbove > menuHeight + MENU_BUFFER

        if (!isControlInViewport) {
          instance.closeMenu()
        } else if (instance.openDirection !== 'auto') {
          instance.menu.placement = directionMap[instance.openDirection]
        } else if (hasEnoughSpaceBelow || !hasEnoughSpaceAbove) {
          instance.menu.placement = 'bottom'
        } else {
          instance.menu.placement = 'top'
        }
      },

      setupMenuSizeWatcher() {
        const { instance } = this
        const $menu = instance.getMenu()

        // istanbul ignore next
        if (this.menuSizeWatcher) return

        this.menuSizeWatcher = {
          remove: watchSize($menu, this.adjustMenuOpenDirection),
        }
      },

      setupMenuResizeAndScrollEventListeners() {
        const { instance } = this
        const $control = instance.getControl()

        // istanbul ignore next
        if (this.menuResizeAndScrollEventListeners) return

        this.menuResizeAndScrollEventListeners = {
          remove: setupResizeAndScrollEventListeners($control, this.adjustMenuOpenDirection),
        }
      },

      removeMenuSizeWatcher() {
        if (!this.menuSizeWatcher) return

        this.menuSizeWatcher.remove()
        this.menuSizeWatcher = null
      },

      removeMenuResizeAndScrollEventListeners() {
        if (!this.menuResizeAndScrollEventListeners) return

        this.menuResizeAndScrollEventListeners.remove()
        this.menuResizeAndScrollEventListeners = null
      },
    },

    render() {
      return (
        <div ref="menu-container" class="vue-treeselect__menu-container" style={this.menuContainerStyle}>
          <transition name="vue-treeselect__menu--transition">
            {this.renderMenu()}
          </transition>
        </div>
      )
    },
  }
</script>
