<script setup>
import { ChevronLeft, ChevronRight } from 'lucide-vue-next'
import {
  addDays,
  addMonths,
  endOfMonth,
  endOfWeek,
  format,
  isAfter,
  isBefore,
  isSameDay,
  isSameMonth,
  isWithinInterval,
  startOfDay,
  startOfMonth,
  startOfWeek,
  subMonths,
} from 'date-fns'

const props = defineProps({
  mode: {
    type: String,
    default: 'single',
  },
  selected: {
    type: [Object, Date, null],
    default: null,
  },
  numberOfMonths: {
    type: Number,
    default: 1,
  },
  disabled: {
    type: Object,
    default: () => ({}),
  },
  showOutsideDays: {
    type: Boolean,
    default: true,
  },
})

const emit = defineEmits(['select'])

function resolveInitialMonth() {
  if (props.mode === 'range' && props.selected?.from) {
    return startOfMonth(props.selected.from)
  }

  if (props.selected instanceof Date) {
    return startOfMonth(props.selected)
  }

  return startOfMonth(new Date())
}

const currentMonth = ref(resolveInitialMonth())
const hoveredDay = ref(null)

watch(
  () => props.selected,
  (value) => {
    if (props.mode === 'range' && value?.from) {
      currentMonth.value = startOfMonth(value.from)
      return
    }

    if (value instanceof Date) {
      currentMonth.value = startOfMonth(value)
    }
  },
  { deep: true },
)

const weekdays = computed(() => {
  const weekStart = startOfWeek(new Date(), { weekStartsOn: 0 })
  return Array.from({ length: 7 }, (_, index) => addDays(weekStart, index))
})

const months = computed(() => {
  return Array.from({ length: props.numberOfMonths }, (_, index) => {
    const month = addMonths(currentMonth.value, index)
    return {
      key: format(month, 'yyyy-MM'),
      date: month,
      title: format(month, 'MMMM yyyy'),
      weeks: buildMonthWeeks(month),
    }
  })
})

function buildMonthWeeks(month) {
  const start = startOfWeek(startOfMonth(month), { weekStartsOn: 0 })
  const end = endOfWeek(endOfMonth(month), { weekStartsOn: 0 })
  const days = []
  let cursor = start

  while (!isAfter(cursor, end)) {
    days.push(cursor)
    cursor = addDays(cursor, 1)
  }

  const weeks = []

  for (let index = 0; index < days.length; index += 7) {
    weeks.push(days.slice(index, index + 7))
  }

  return weeks
}

function isDisabled(day) {
  if (!props.disabled?.before) {
    return false
  }

  return isBefore(day, startOfDay(props.disabled.before))
}

function isRangeStart(day) {
  return props.mode === 'range' && props.selected?.from && isSameDay(day, props.selected.from)
}

function isRangeEnd(day) {
  return props.mode === 'range' && props.selected?.to && isSameDay(day, props.selected.to)
}

function isRangeMiddle(day) {
  if (props.mode !== 'range' || !props.selected?.from || !props.selected?.to) {
    return false
  }

  if (isRangeStart(day) || isRangeEnd(day)) {
    return false
  }

  return isWithinInterval(day, {
    start: props.selected.from,
    end: props.selected.to,
  })
}

function resolveRangePreviewBounds() {
  if (props.mode !== 'range' || !props.selected?.from || props.selected?.to || !hoveredDay.value) {
    return null
  }

  const from = props.selected.from
  const hovered = hoveredDay.value

  return isBefore(hovered, from)
    ? { start: hovered, end: from }
    : { start: from, end: hovered }
}

function isRangePreview(day) {
  const bounds = resolveRangePreviewBounds()

  if (!bounds) {
    return false
  }

  if (isRangeStart(day)) {
    return false
  }

  if (isSameDay(day, bounds.start) || isSameDay(day, bounds.end)) {
    return true
  }

  return isWithinInterval(day, bounds)
}

function isSelected(day) {
  if (props.mode === 'range') {
    return isRangeStart(day) || isRangeEnd(day) || isRangeMiddle(day)
  }

  return props.selected instanceof Date && isSameDay(day, props.selected)
}

function selectDay(day) {
  if (isDisabled(day)) {
    return
  }

  if (props.mode === 'range') {
    const selected = props.selected || {}

    if (!selected.from || selected.to) {
      emit('select', { from: day, to: undefined })
      return
    }

    if (isBefore(day, selected.from)) {
      emit('select', { from: day, to: selected.from })
      return
    }

    emit('select', { from: selected.from, to: day })
    return
  }

  emit('select', day)
}

function handleDayMouseEnter(day) {
  if (isDisabled(day)) {
    return
  }

  hoveredDay.value = day
}

function handleCalendarMouseLeave() {
  hoveredDay.value = null
}
</script>

<template>
  <div class="p-3 relative w-fit" @mouseleave="handleCalendarMouseLeave">
    <div :class="['flex flex-col space-y-4', numberOfMonths > 1 ? 'sm:flex-row sm:space-x-4 sm:space-y-0' : '']">
      <div
        v-for="month in months"
        :key="month.key"
        class="space-y-4"
      >
        <div class="flex justify-center pt-1 relative items-center">
          <div class="text-sm font-medium text-slate-900">
            {{ month.title }}
          </div>

          <button
            v-if="month.key === months[0]?.key"
            type="button"
            class="absolute left-3 top-4 h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100 border border-slate-200 rounded-md flex items-center justify-center transition-colors hover:bg-slate-100"
            @click="currentMonth = subMonths(currentMonth, 1)"
          >
            <ChevronLeft class="h-4 w-4" />
          </button>

          <button
            v-if="month.key === months[months.length - 1]?.key"
            type="button"
            class="absolute right-3 top-4 h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100 border border-slate-200 rounded-md flex items-center justify-center transition-colors hover:bg-slate-100"
            @click="currentMonth = addMonths(currentMonth, 1)"
          >
            <ChevronRight class="h-4 w-4" />
          </button>
        </div>

        <table class="w-full border-collapse space-y-1">
          <thead>
            <tr class="flex">
              <th
                v-for="weekday in weekdays"
                :key="weekday.toISOString()"
                class="text-slate-500 rounded-md w-9 font-normal text-[0.8rem]"
              >
                {{ format(weekday, 'EEEEEE') }}
              </th>
            </tr>
          </thead>

          <tbody>
            <tr
              v-for="(week, weekIndex) in month.weeks"
              :key="`${month.key}-week-${weekIndex}`"
              class="flex w-full mt-2"
            >
              <td
                v-for="day in week"
                :key="day.toISOString()"
                class="h-9 w-9 p-0 text-center text-sm"
              >
                <button
                  v-if="showOutsideDays || isSameMonth(day, month.date)"
                  type="button"
                  :disabled="isDisabled(day)"
                  :class="[
                    'h-9 w-9 p-0 font-normal rounded-md transition-colors flex items-center justify-center',
                    isSelected(day) && !isRangeMiddle(day) ? 'bg-brand-orange text-white font-semibold shadow-md shadow-brand-orange/20' : '',
                    isRangeMiddle(day) ? 'bg-slate-100 text-slate-900' : '',
                    isRangePreview(day) ? 'bg-brand-orange/10 text-brand-orange' : '',
                    !isSelected(day) && !isRangePreview(day) ? 'text-slate-900 hover:bg-brand-orange/10 hover:text-brand-orange' : '',
                    isSameDay(day, new Date()) && !isSelected(day) ? 'font-bold text-brand-orange' : '',
                    !isSameMonth(day, month.date) ? 'text-slate-400 opacity-50' : '',
                    isDisabled(day) ? 'text-slate-400 opacity-50 pointer-events-none' : '',
                  ]"
                  @click="selectDay(day)"
                  @mouseenter="handleDayMouseEnter(day)"
                >
                  {{ format(day, 'd') }}
                </button>
              </td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
  </div>
</template>
