<script setup lang="ts" generic="TOption extends CheckboxGroupOption">
import type { AcceptableValue, Direction } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { useTextDirection } from '@vueuse/core'
import { computed, useId, watch } from 'vue'
import { cn } from '#utils'
import { Label } from '../label/label'
import { Checkbox } from './checkbox'

export interface CheckboxGroupOption {
  label: string
  value: AcceptableValue
  description?: string
  disabled?: boolean
  id?: string
}

defineOptions({
  name: 'BuiltInCheckboxGroup',
})

const props = defineProps<{
  options: TOption[]
  disabled?: boolean
  min?: number
  max?: number
  dir?: Direction
  class?: HTMLAttributes['class']
  optionClass?: HTMLAttributes['class']
  itemClass?: HTMLAttributes['class']
  labelClass?: HTMLAttributes['class']
}>()

const emit = defineEmits<{
  change: [value: AcceptableValue[]]
}>()

const slots = defineSlots<{
  option?: (props: {
    option: TOption
    checked: boolean
    disabled: boolean
    id: string
  }) => any
}>()

const value = defineModel<AcceptableValue[]>({
  default: () => [],
})

const documentDir = useTextDirection({
  observe: true,
})

const dir = computed<Direction>(() => props.dir ?? (documentDir.value === 'rtl' ? 'rtl' : 'ltr'))
const baseId = useId()
const checkedCount = computed(() => value.value.length)

watch(value, (newValue) => {
  emit('change', newValue)
})

function getOptionId(option: TOption, index: number) {
  return option.id || `${baseId}-${index}`
}

function getOptionKey(option: TOption, index: number) {
  if (option.id) {
    return option.id
  }

  return typeof option.value === 'string' || typeof option.value === 'number'
    ? option.value
    : index
}

function isOptionDisabled(option: TOption) {
  if (props.disabled || option.disabled) {
    return true
  }

  const checked = isOptionChecked(option)

  if (checked && props.min !== undefined && checkedCount.value <= props.min) {
    return true
  }

  if (!checked && props.max !== undefined && checkedCount.value >= props.max) {
    return true
  }

  return false
}

function findOptionIndex(option: TOption) {
  return value.value.findIndex(item => Object.is(item, option.value))
}

function isOptionChecked(option: TOption) {
  return findOptionIndex(option) > -1
}

function updateOptionChecked(option: TOption, checked: boolean) {
  const nextValue = [...value.value]
  const optionIndex = nextValue.findIndex(item => Object.is(item, option.value))

  if (checked) {
    if (optionIndex === -1) {
      nextValue.push(option.value)
    }
  }
  else if (optionIndex > -1) {
    nextValue.splice(optionIndex, 1)
  }

  value.value = nextValue
}

function handleOptionModelValueChange(option: TOption, checked: boolean | 'indeterminate') {
  if (isOptionDisabled(option)) {
    return
  }

  updateOptionChecked(option, checked === true)
}

function handleCustomOptionClick(option: TOption) {
  if (!slots.option || isOptionDisabled(option)) {
    return
  }

  updateOptionChecked(option, !isOptionChecked(option))
}

function handleCustomOptionKeydown(option: TOption) {
  if (!slots.option || isOptionDisabled(option)) {
    return
  }

  updateOptionChecked(option, !isOptionChecked(option))
}
</script>

<template>
  <div
    role="group"
    :dir="dir"
    :class="cn('grid gap-3', props.class)"
  >
    <div
      v-for="(option, index) in props.options"
      :key="getOptionKey(option, index)"
      :tabindex="slots.option && !isOptionDisabled(option) ? 0 : undefined"
      :role="slots.option ? 'checkbox' : undefined"
      :aria-checked="slots.option ? `${isOptionChecked(option)}` : undefined"
      :aria-disabled="slots.option ? `${isOptionDisabled(option)}` : undefined"
      :class="
        cn(
          'flex gap-2',
          option.description ? 'items-start' : 'items-center',
          slots.option && 'outline-none focus-visible:ring-ring/50 focus-visible:ring-3 rounded-xl',
          props.optionClass,
        )
      "
      @click="handleCustomOptionClick(option)"
      @keydown.enter.prevent="handleCustomOptionKeydown(option)"
      @keydown.space.prevent="handleCustomOptionKeydown(option)"
    >
      <Checkbox
        :id="getOptionId(option, index)"
        :model-value="isOptionChecked(option)"
        :disabled="isOptionDisabled(option)"
        :class="cn(props.itemClass, slots.option && 'hidden')"
        @update:model-value="handleOptionModelValueChange(option, $event)"
      />
      <Label
        :for="slots.option ? undefined : getOptionId(option, index)"
        :class="
          cn(
            'min-w-0 flex-1 cursor-pointer gap-0',
            option.description ? 'items-start' : 'items-center',
            isOptionDisabled(option) && 'cursor-not-allowed opacity-60',
            props.labelClass,
          )
        "
      >
        <slot
          :id="getOptionId(option, index)"
          name="option"
          :option="option"
          :checked="isOptionChecked(option)"
          :disabled="isOptionDisabled(option)"
        >
          <div class="gap-1 grid min-w-0">
            <span class="truncate">{{ option.label }}</span>
            <span
              v-if="option.description"
              class="text-xs text-muted-foreground leading-5 font-normal"
            >
              {{ option.description }}
            </span>
          </div>
        </slot>
      </Label>
    </div>
  </div>
</template>
