<script setup lang="ts">
import { CalendarDays, ChevronLeft, ChevronRight } from "@lucide/vue";
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";

const props = withDefaults(
  defineProps<{
    modelValue: string;
    mode: "month" | "date";
    placeholder?: string;
    disabled?: boolean;
    clearable?: boolean;
  }>(),
  {
    placeholder: "请选择日期",
    disabled: false,
    clearable: true,
  },
);

const emit = defineEmits<{
  (event: "update:modelValue", value: string): void;
  (event: "change", value: string): void;
}>();

type CalendarCell = {
  dateValue: string;
  day: number;
  muted: boolean;
};

const PANEL_WIDTH = 292;
const PANEL_OFFSET = 8;
const VIEWPORT_GAP = 12;

const rootRef = ref<HTMLElement | null>(null);
const panelRef = ref<HTMLElement | null>(null);
const panelStyle = ref<Record<string, string>>({});
const open = ref(false);
const panelYear = ref(new Date().getFullYear());
const panelMonth = ref(new Date().getMonth());

const monthNames = ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"];
const weekNames = ["一", "二", "三", "四", "五", "六", "日"];

const displayValue = computed(() => {
  if (!props.modelValue) {
    return props.placeholder;
  }
  if (props.mode === "month") {
    const parsed = parseMonth(props.modelValue);
    return parsed ? `${parsed.year}年${String(parsed.month + 1).padStart(2, "0")}月` : props.modelValue;
  }
  const parsed = parseDate(props.modelValue);
  return parsed
    ? `${parsed.year}年${String(parsed.month + 1).padStart(2, "0")}月${String(parsed.day).padStart(2, "0")}日`
    : props.modelValue;
});

const selectedMonthValue = computed(() => props.modelValue.slice(0, 7));
const todayDateValue = computed(() => formatDate(new Date()));
const currentMonthValue = computed(() => todayDateValue.value.slice(0, 7));

const calendarCells = computed<CalendarCell[]>(() => {
  const cells: CalendarCell[] = [];
  const firstDay = new Date(panelYear.value, panelMonth.value, 1);
  const firstOffset = (firstDay.getDay() + 6) % 7;
  const firstVisible = new Date(panelYear.value, panelMonth.value, 1 - firstOffset);

  for (let index = 0; index < 42; index += 1) {
    const date = new Date(firstVisible);
    date.setDate(firstVisible.getDate() + index);
    cells.push({
      dateValue: formatDate(date),
      day: date.getDate(),
      muted: date.getMonth() !== panelMonth.value,
    });
  }

  return cells;
});

watch(
  () => props.modelValue,
  async () => {
    if (open.value) {
      syncPanelFromValue();
      await nextTick();
      updatePanelPosition();
    }
  },
);

onMounted(() => {
  document.addEventListener("click", handleOutsideClick);
  window.addEventListener("resize", updatePanelPosition);
  window.addEventListener("scroll", updatePanelPosition, true);
});

onBeforeUnmount(() => {
  document.removeEventListener("click", handleOutsideClick);
  window.removeEventListener("resize", updatePanelPosition);
  window.removeEventListener("scroll", updatePanelPosition, true);
});

async function togglePanel(): Promise<void> {
  if (props.disabled) {
    return;
  }

  if (open.value) {
    closePanel();
    return;
  }

  syncPanelFromValue();
  open.value = true;
  await nextTick();
  updatePanelPosition();
}

function closePanel(): void {
  open.value = false;
}

function handleOutsideClick(event: MouseEvent): void {
  const target = event.target as Node;
  if (rootRef.value?.contains(target) || panelRef.value?.contains(target)) {
    return;
  }
  closePanel();
}

function updatePanelPosition(): void {
  if (!open.value) {
    return;
  }

  const trigger = rootRef.value?.querySelector<HTMLElement>(".date-picker-trigger");
  if (!trigger) {
    return;
  }

  const rect = trigger.getBoundingClientRect();
  const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
  const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
  const width = Math.min(PANEL_WIDTH, Math.max(220, viewportWidth - VIEWPORT_GAP * 2));
  const measuredHeight = panelRef.value?.offsetHeight;
  const panelHeight = measuredHeight || (props.mode === "date" ? 380 : 242);
  const minLeft = VIEWPORT_GAP;
  const maxLeft = Math.max(minLeft, viewportWidth - width - VIEWPORT_GAP);
  const left = clamp(rect.left, minLeft, maxLeft);
  const belowTop = rect.bottom + PANEL_OFFSET;
  const aboveTop = rect.top - panelHeight - PANEL_OFFSET;
  const fitsBelow = belowTop + panelHeight <= viewportHeight - VIEWPORT_GAP;
  const fitsAbove = aboveTop >= VIEWPORT_GAP;
  const top = fitsBelow
    ? belowTop
    : fitsAbove
      ? aboveTop
      : Math.max(VIEWPORT_GAP, viewportHeight - panelHeight - VIEWPORT_GAP);

  panelStyle.value = {
    top: `${Math.round(top)}px`,
    left: `${Math.round(left)}px`,
    width: `${Math.round(width)}px`,
  };
}

function syncPanelFromValue(): void {
  const parsed = props.mode === "month" ? parseMonth(props.modelValue) : parseDate(props.modelValue);
  const fallback = new Date();
  panelYear.value = parsed?.year ?? fallback.getFullYear();
  panelMonth.value = parsed?.month ?? fallback.getMonth();
}

function previousPanel(): void {
  if (props.mode === "month") {
    panelYear.value -= 1;
    return;
  }
  const date = new Date(panelYear.value, panelMonth.value - 1, 1);
  panelYear.value = date.getFullYear();
  panelMonth.value = date.getMonth();
}

function nextPanel(): void {
  if (props.mode === "month") {
    panelYear.value += 1;
    return;
  }
  const date = new Date(panelYear.value, panelMonth.value + 1, 1);
  panelYear.value = date.getFullYear();
  panelMonth.value = date.getMonth();
}

function selectMonth(monthIndex: number): void {
  commitValue(`${panelYear.value}-${String(monthIndex + 1).padStart(2, "0")}`);
}

function selectDate(dateValue: string): void {
  commitValue(dateValue);
}

function selectCurrent(): void {
  commitValue(props.mode === "month" ? currentMonthValue.value : todayDateValue.value);
}

function clearValue(): void {
  commitValue("");
}

function commitValue(value: string): void {
  emit("update:modelValue", value);
  emit("change", value);
  closePanel();
}

function parseMonth(value: string): { year: number; month: number } | null {
  const match = /^(\d{4})-(\d{2})$/.exec(value || "");
  if (!match) {
    return null;
  }
  const month = Number(match[2]) - 1;
  if (month < 0 || month > 11) {
    return null;
  }
  return { year: Number(match[1]), month };
}

function parseDate(value: string): { year: number; month: number; day: number } | null {
  const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value || "");
  if (!match) {
    return null;
  }
  const year = Number(match[1]);
  const month = Number(match[2]) - 1;
  const day = Number(match[3]);
  const date = new Date(year, month, day);
  if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) {
    return null;
  }
  return { year, month, day };
}

function formatDate(value: Date): string {
  return `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, "0")}-${String(value.getDate()).padStart(2, "0")}`;
}

function clamp(value: number, min: number, max: number): number {
  return Math.min(Math.max(value, min), max);
}
</script>

<template>
  <div ref="rootRef" class="date-picker" :class="{ 'is-open': open, 'is-disabled': disabled }">
    <button class="date-picker-trigger" type="button" :disabled="disabled" @click="togglePanel">
      <span :class="{ muted: !modelValue }">{{ displayValue }}</span>
      <CalendarDays :size="17" aria-hidden="true" />
    </button>

    <Teleport to="body">
      <div v-if="open" ref="panelRef" class="date-picker-panel" :style="panelStyle" role="dialog" aria-label="日期选择">
        <div class="date-picker-panel-head">
          <button class="date-picker-nav" type="button" title="上一个" @click="previousPanel">
            <ChevronLeft :size="17" aria-hidden="true" />
          </button>
          <strong>{{ panelYear }}{{ mode === "date" ? `年${String(panelMonth + 1).padStart(2, "0")}月` : "年" }}</strong>
          <button class="date-picker-nav" type="button" title="下一个" @click="nextPanel">
            <ChevronRight :size="17" aria-hidden="true" />
          </button>
        </div>

        <div v-if="mode === 'month'" class="month-grid">
          <button
            v-for="(month, index) in monthNames"
            :key="month"
            class="date-picker-cell"
            :class="{ active: `${panelYear}-${String(index + 1).padStart(2, '0')}` === selectedMonthValue }"
            type="button"
            @click="selectMonth(index)"
          >
            {{ month }}
          </button>
        </div>

        <div v-else class="calendar-shell">
          <div class="calendar-week-row">
            <span v-for="week in weekNames" :key="week">{{ week }}</span>
          </div>
          <div class="calendar-grid">
            <button
              v-for="cell in calendarCells"
              :key="cell.dateValue"
              class="date-picker-cell calendar-day"
              :class="{ active: cell.dateValue === modelValue, today: cell.dateValue === todayDateValue, muted: cell.muted }"
              type="button"
              @click="selectDate(cell.dateValue)"
            >
              {{ cell.day }}
            </button>
          </div>
        </div>

        <div class="date-picker-actions">
          <button v-if="clearable" class="date-picker-text-button" type="button" @click="clearValue">清空</button>
          <button class="date-picker-text-button primary" type="button" @click="selectCurrent">
            {{ mode === "month" ? "本月" : "今天" }}
          </button>
        </div>
      </div>
    </Teleport>
  </div>
</template>
