<template>
  <view class="theme-picker-trigger" @tap="openPicker">
    <slot />
  </view>
  <root-portal v-if="isVisible">
    <view class="theme-picker-overlay">
      <view class="theme-picker-mask" :catch-move="true" @tap="cancelPicker" />
      <view class="theme-picker-panel">
        <view class="theme-picker-header" :catch-move="true">
          <text class="theme-picker-action theme-picker-cancel" @tap="cancelPicker">取消</text>
          <text class="theme-picker-action theme-picker-confirm" @tap="confirmPicker">确定</text>
        </view>
        <picker-view
          class="theme-picker-view"
          indicator-style="height: 68rpx;"
          :value="draftIndexes"
          @change="handlePickerChange"
        >
          <picker-view-column v-for="(column, columnIndex) in columns" :key="columnIndex">
            <view v-for="item in column" :key="item.value" class="theme-picker-item">
              {{ item.label }}
            </view>
          </picker-view-column>
        </picker-view>
      </view>
    </view>
  </root-portal>
</template>

<script setup lang="ts">
import { computed, onBeforeUnmount, ref, watch } from 'vue'
import { lockThemePickerPageScroll, unlockThemePickerPageScroll } from './themePickerLock'

type ThemePickerMode = 'selector' | 'date'
type ThemePickerFields = 'year' | 'month' | 'day'
type PickerOptionValue = string | number

interface PickerOption {
  label: string
  value: PickerOptionValue
}

interface PickerViewChangeEvent {
  detail?: {
    value?: number[]
  }
}

const props = withDefaults(defineProps<{
  mode?: ThemePickerMode
  range?: PickerOptionValue[]
  value?: PickerOptionValue
  disabled?: boolean
  fields?: ThemePickerFields
  start?: string
  end?: string
}>(), {
  mode: 'selector',
  range: () => [],
  value: '',
  disabled: false,
  fields: 'day',
  start: '1970-01-01',
  end: '2999-01-01'
})

const emit = defineEmits<{
  change: [event: { detail: { value: PickerOptionValue } }]
  cancel: []
}>()

const isVisible = ref(false)
const hasLockedPageScroll = ref(false)
const draftIndexes = ref<number[]>([])
const columns = computed(() => props.mode === 'date' ? getDateColumns() : getSelectorColumns())

watch(() => [props.value, props.range, props.mode], () => {
  if (!isVisible.value) draftIndexes.value = getInitialIndexes()
})

watch(isVisible, syncPageScrollLock)

onBeforeUnmount(releasePageScrollLock)

function openPicker() {
  if (props.disabled) return
  draftIndexes.value = getInitialIndexes()
  isVisible.value = true
}

function cancelPicker() {
  isVisible.value = false
  emit('cancel')
}

function confirmPicker() {
  isVisible.value = false
  emit('change', { detail: { value: getConfirmedValue() } })
}

function handlePickerChange(event: PickerViewChangeEvent) {
  draftIndexes.value = normalizeIndexes(event.detail?.value || [])
}

function syncPageScrollLock(shouldLock: boolean) {
  if (shouldLock) {
    lockPageScroll()
    return
  }
  releasePageScrollLock()
}

function lockPageScroll() {
  if (hasLockedPageScroll.value) return
  lockThemePickerPageScroll()
  hasLockedPageScroll.value = true
}

function releasePageScrollLock() {
  if (!hasLockedPageScroll.value) return
  unlockThemePickerPageScroll()
  hasLockedPageScroll.value = false
}

function getInitialIndexes() {
  return props.mode === 'date' ? getDateIndexes() : [getSelectorIndex()]
}

function getSelectorIndex() {
  const valueIndex = Number(props.value)
  if (Number.isInteger(valueIndex) && props.range[valueIndex] !== undefined) return valueIndex
  const index = props.range.findIndex(item => item === props.value)
  return Math.max(index, 0)
}

function getConfirmedValue() {
  return props.mode === 'date' ? getSelectedDateValue() : draftIndexes.value[0] || 0
}

function getSelectorColumns(): PickerOption[][] {
  return [props.range.map(item => ({ label: String(item), value: item }))]
}

function getDateColumns(): PickerOption[][] {
  const parts = getSelectedDateParts()
  const years = getYearOptions()
  const months = getMonthOptions(parts.year)
  const columns: PickerOption[][] = [toPickerOptions(years, '年')]
  if (props.fields !== 'year') columns.push(toPickerOptions(months, '月'))
  if (props.fields === 'day') columns.push(toPickerOptions(getDayOptions(parts.year, parts.month), '日'))
  return columns
}

function getDateIndexes() {
  const parts = getClampedDateParts(props.value)
  const indexes = [getYearOptions().indexOf(parts.year)]
  if (props.fields !== 'year') indexes.push(getMonthOptions(parts.year).indexOf(parts.month))
  if (props.fields === 'day') indexes.push(getDayOptions(parts.year, parts.month).indexOf(parts.day))
  return indexes.map(index => Math.max(index, 0))
}

function normalizeIndexes(indexes: number[]) {
  if (props.mode !== 'date') return [Math.max(indexes[0] || 0, 0)]
  const parts = getDatePartsFromIndexes(indexes)
  return getDateIndexesFromParts(parts)
}

function getDateIndexesFromParts(parts: DateParts) {
  const indexes = [getYearOptions().indexOf(parts.year)]
  if (props.fields !== 'year') indexes.push(getMonthOptions(parts.year).indexOf(parts.month))
  if (props.fields === 'day') indexes.push(getDayOptions(parts.year, parts.month).indexOf(parts.day))
  return indexes.map(index => Math.max(index, 0))
}

function getSelectedDateParts() {
  return getDatePartsFromIndexes(draftIndexes.value)
}

function getDatePartsFromIndexes(indexes: number[]) {
  const year = getValueByIndex(getYearOptions(), indexes[0])
  const month = getValueByIndex(getMonthOptions(year), indexes[1])
  const day = getValueByIndex(getDayOptions(year, month), indexes[2])
  return normalizeDateParts({ year, month, day })
}

function getSelectedDateValue() {
  const parts = getSelectedDateParts()
  if (props.fields === 'year') return String(parts.year)
  if (props.fields === 'month') return `${parts.year}-${padNumber(parts.month)}`
  return `${parts.year}-${padNumber(parts.month)}-${padNumber(parts.day)}`
}

interface DateParts {
  year: number
  month: number
  day: number
}

function getClampedDateParts(value?: PickerOptionValue): DateParts {
  const date = parseDate(String(value || '')) || new Date()
  return getPartsFromDate(clampDate(date))
}

function parseDate(value: string) {
  const match = /^(\d{4})(?:-(\d{1,2}))?(?:-(\d{1,2}))?$/.exec(value)
  if (!match) return undefined
  return new Date(Number(match[1]), Number(match[2] || 1) - 1, Number(match[3] || 1))
}

function clampDate(date: Date) {
  const start = parseDate(props.start) || new Date(1970, 0, 1)
  const end = parseDate(props.end) || new Date(2999, 0, 1)
  if (date < start) return start
  if (date > end) return end
  return date
}

function getPartsFromDate(date: Date): DateParts {
  return { year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate() }
}

function normalizeDateParts(parts: DateParts): DateParts {
  const monthOptions = getMonthOptions(parts.year)
  const month = getClosestValue(monthOptions, parts.month)
  const day = getClosestValue(getDayOptions(parts.year, month), parts.day)
  return { year: parts.year, month, day }
}

function getYearOptions() {
  const startYear = (parseDate(props.start) || new Date(1970, 0, 1)).getFullYear()
  const endYear = (parseDate(props.end) || new Date(2999, 0, 1)).getFullYear()
  return createNumberRange(startYear, endYear)
}

function getMonthOptions(year: number) {
  const start = parseDate(props.start) || new Date(1970, 0, 1)
  const end = parseDate(props.end) || new Date(2999, 0, 1)
  const min = year === start.getFullYear() ? start.getMonth() + 1 : 1
  const max = year === end.getFullYear() ? end.getMonth() + 1 : 12
  return createNumberRange(min, max)
}

function getDayOptions(year: number, month: number) {
  const start = parseDate(props.start) || new Date(1970, 0, 1)
  const end = parseDate(props.end) || new Date(2999, 0, 1)
  const min = isSameMonth(start, year, month) ? start.getDate() : 1
  const max = isSameMonth(end, year, month) ? end.getDate() : getMonthDayCount(year, month)
  return createNumberRange(min, max)
}

function isSameMonth(date: Date, year: number, month: number) {
  return date.getFullYear() === year && date.getMonth() + 1 === month
}

function getMonthDayCount(year: number, month: number) {
  return new Date(year, month, 0).getDate()
}

function createNumberRange(start: number, end: number) {
  return Array.from({ length: end - start + 1 }, (_, index) => start + index)
}

function getValueByIndex(values: number[], index = 0) {
  return values[Math.min(Math.max(index, 0), values.length - 1)]
}

function getClosestValue(values: number[], value: number) {
  return values.includes(value) ? value : values[0]
}

function toPickerOptions(values: number[], unit: string) {
  return values.map(value => ({ label: `${padNumber(value)}${unit}`, value }))
}

function padNumber(value: number) {
  return value < 10 ? `0${value}` : String(value)
}
</script>

<style lang="scss">
.theme-picker-trigger {
  display: block;
}

.theme-picker-overlay {
  position: fixed;
  inset: 0;
  z-index: 5000;
}

.theme-picker-mask {
  position: absolute;
  inset: 0;
  background: rgb(0 0 0 / 60%);
}

.theme-picker-panel {
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  background: $bg-page;
}

.theme-picker-header {
  position: relative;
  display: flex;
  padding: 18px 30px;
  font-size: $font-lg;
  background: $bg-page;
}

.theme-picker-header::after {
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  height: 1px;
  content: "";
  background: $border-color-light;
  transform: scaleY(0.5);
}

.theme-picker-action {
  flex: 1;
  line-height: 40px;
}

.theme-picker-cancel {
  color: $text-secondary;
  text-align: left;
}

.theme-picker-confirm {
  color: $text-primary;
  text-align: right;
}

.theme-picker-view {
  width: 100%;
  height: 476px;
  background: $bg-page;
}

.theme-picker-item {
  height: 68px;
  font-size: $font-lg;
  line-height: 68px;
  color: $text-primary;
  text-align: center;
}
</style>
