<script lang="ts">
import type { Ref } from 'vue'
import type { DismissableLayerEmits, DismissableLayerProps } from '@/DismissableLayer'
import type { PopperContentProps } from '@/Popper'

import { createContext, getActiveElement, useFocusGuards, useForwardExpose, useForwardProps, useHideOthers } from '@/shared'
import { useBodyScrollLock } from '@/shared/useBodyScrollLock'

export type ComboboxContentImplEmits = DismissableLayerEmits

export interface ComboboxContentImplProps extends PopperContentProps, DismissableLayerProps {
  /**
   * The positioning mode to use, <br>
   * `inline` is the default and you can control the position using CSS. <br>
   * `popper` positions content in the same way as our other primitives, for example `Popover` or `DropdownMenu`.
   */
  position?: 'inline' | 'popper'
  /** The document.body will be lock, and scrolling will be disabled. */
  bodyLock?: boolean
  /**
   * When `true`, hides the content when there are no items matching the filter.
   * @defaultValue false
   */
  hideWhenEmpty?: boolean
}

export const [injectComboboxContentContext, provideComboboxContentContext]
  = createContext<{
    position: Ref<'inline' | 'popper'>
  }>('ComboboxContent')
</script>

<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref, toRefs } from 'vue'
import { DismissableLayer } from '@/DismissableLayer'
import { FocusScope } from '@/FocusScope'
import { ListboxContent } from '@/Listbox'
import { PopperContent } from '@/Popper'
import { Primitive } from '@/Primitive'
import { injectComboboxRootContext } from './ComboboxRoot.vue'

const props = withDefaults(defineProps<ComboboxContentImplProps>(), {
  position: 'inline',
})
const emits = defineEmits<ComboboxContentImplEmits>()

const { position } = toRefs(props)
const rootContext = injectComboboxRootContext()

const isEmpty = computed(() => rootContext.ignoreFilter.value
  ? rootContext.allItems.value.size === 0
  : rootContext.filterState.value.count === 0,
)

const { forwardRef, currentElement } = useForwardExpose()
useBodyScrollLock(props.bodyLock)
useFocusGuards()
useHideOthers(rootContext.parentElement)

const pickedProps = computed(() => {
  if (props.position === 'popper')
    return props
  else return {}
})

const forwardedProps = useForwardProps(pickedProps.value)

const popperStyle = {
  // Ensure border-box for floating-ui calculations
  'boxSizing': 'border-box',
  '--reka-combobox-content-transform-origin':
        'var(--reka-popper-transform-origin)',
  '--reka-combobox-content-available-width':
        'var(--reka-popper-available-width)',
  '--reka-combobox-content-available-height':
        'var(--reka-popper-available-height)',
  '--reka-combobox-trigger-width': 'var(--reka-popper-anchor-width)',
  '--reka-combobox-trigger-height': 'var(--reka-popper-anchor-height)',
}

provideComboboxContentContext({ position })

// Handle case where input position within the content
const isInputWithinContent = ref(false)
onMounted(() => {
  if (rootContext.inputElement.value) {
    isInputWithinContent.value = currentElement.value.contains(rootContext.inputElement.value)
    if (isInputWithinContent.value) {
      rootContext.inputElement.value.focus()
    }
  }
})

onUnmounted(() => {
  const activeElement = getActiveElement()
  if (isInputWithinContent.value && (!activeElement || activeElement === document.body)) {
    rootContext.triggerElement.value?.focus()
  }
})

function isEventTargetWithinCombobox(target: EventTarget | null) {
  if (rootContext.parentElement.value?.contains(target as Node))
    return true

  // A `<label>` associated (via `for`) with an element inside the combobox forwards its
  // click/focus to that control, so interacting with it should not dismiss the content.
  // Without this, clicking such a label while open dismisses on `pointerdown` and the
  // forwarded click/focus immediately re-opens it.
  const label = target instanceof Element ? target.closest('label') : null
  const control = label?.control
  return !!control && !!rootContext.parentElement.value?.contains(control)
}
</script>

<template>
  <ListboxContent as-child>
    <FocusScope
      as-child
      @mount-auto-focus.prevent
      @unmount-auto-focus.prevent
    >
      <DismissableLayer
        as-child
        :disable-outside-pointer-events="disableOutsidePointerEvents"
        @dismiss="rootContext.onOpenChange(false)"
        @focus-outside="(ev) => {
          // if focusing inside the combobox (or a label tied to it), prevent dismiss
          if (isEventTargetWithinCombobox(ev.target)) ev.preventDefault()
          emits('focusOutside', ev)
        }"
        @interact-outside="emits('interactOutside', $event)"
        @escape-key-down="emits('escapeKeyDown', $event)"
        @pointer-down-outside="(ev) => {
          // if clicking inside the combobox (or a label tied to it), prevent dismiss
          if (isEventTargetWithinCombobox(ev.target)) ev.preventDefault()
          emits('pointerDownOutside', ev)
        }"
      >
        <component
          :is="position === 'popper' ? PopperContent : Primitive"
          v-bind="{ ...$attrs, ...forwardedProps }"
          :id="rootContext.contentId"
          :ref="forwardRef"
          :memo-dependencies="position === 'popper'
            ? [rootContext.filterSearch.value, rootContext.filterState.value]
            : undefined"
          :data-state="rootContext.open.value ? 'open' : 'closed'"
          :data-empty="isEmpty ? '' : undefined"
          :style="{
            // flex layout so we can place the scroll buttons properly
            // When hideWhenEmpty is true, hide the content when no items match
            display: (props.hideWhenEmpty && isEmpty) ? 'none' : 'flex',
            flexDirection: 'column',
            // reset the outline by default as the content MAY get focused
            outline: 'none',
            ...(position === 'popper' ? popperStyle : {}),
          }"
        >
          <slot />
        </component>
      </DismissableLayer>
    </FocusScope>
  </ListboxContent>
</template>
