<script setup lang="ts">
import type { EntryType } from "@cents/domain";
import { ArrowLeft, CircleEllipsis } from "@lucide/vue";
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
import {
  categories,
  entryTypes,
  type Category,
  type EntryTypeOption,
} from "../data/categories";

type SelectorLevel = "type" | "category" | "subcategory";
type SelectorAction = "other-category" | "other-subcategory";
type SelectorOption = (EntryTypeOption | Category) & { selectorAction?: SelectorAction };

const props = defineProps<{
  open: boolean;
  entryType: EntryType;
  categoryPath: Category[];
}>();

const emit = defineEmits<{
  "update:entryType": [value: EntryType];
  "update:categoryPath": [value: Category[]];
  confirm: [];
  close: [];
}>();

const FOCUS_ANGLE = 270;
const OPTION_ANGLE = 36;
const SCREEN_EDGE_GAP = 4;
const selectorLevel = ref<SelectorLevel>("type");
const wheelStage = ref<HTMLElement | null>(null);
const wheelRotation = ref(0);
const wheelPointerId = ref<number | null>(null);
let pointerLastAngle = 0;
let pointerLastTime = 0;
let pointerVelocity = 0;
let pointerTravel = 0;
let inertiaFrame: number | null = null;

const selectedParent = computed(() => props.categoryPath[0]);
const selectorOptions = computed<SelectorOption[]>(() => {
  if (selectorLevel.value === "type") return entryTypes;
  if (selectorLevel.value === "category") {
    return prioritizeOptions(withOtherOption(categories[props.entryType], "other-category"));
  }
  return prioritizeOptions(
    withOtherOption(selectedParent.value?.children ?? [], "other-subcategory"),
  );
});
const selectedIndex = computed(() => {
  const selectedId =
    selectorLevel.value === "type"
      ? props.entryType
      : selectorLevel.value === "category"
        ? selectedParent.value?.id
        : props.categoryPath[1]?.id;
  return selectorOptions.value.findIndex((option) => option.id === selectedId);
});
const levelLabel = computed(() => {
  if (selectorLevel.value === "type") return "选择收支类型";
  if (selectorLevel.value === "category") return "选择大类";
  return `选择${selectedParent.value?.label ?? ""}细类`;
});
const wheelIsCircular = computed(() => selectorOptions.value.length * OPTION_ANGLE >= 360);
const defaultRotation = computed(() => {
  const optionSpan = Math.max(0, selectorOptions.value.length - 1) * OPTION_ANGLE;
  return normalizeCircleAngle(-90 - optionSpan / 2);
});
const rotationBounds = ref({ min: 0, max: 0 });
const rotorStyle = computed(() => ({
  "--wheel-rotation": `${wheelRotation.value}deg`,
}));

watch(
  () => props.open,
  async (open) => {
    stopInertia();
    if (!open) return;
    selectorLevel.value = "type";
    await nextTick();
    resetShellScroll();
    updateRotationBounds();
    arrangeOptions();
  },
);

watch(selectorOptions, async () => {
  await nextTick();
  resetShellScroll();
  updateRotationBounds();
  arrangeOptions();
});

onMounted(() => window.addEventListener("resize", updateRotationBounds));
onBeforeUnmount(() => {
  stopInertia();
  window.removeEventListener("resize", updateRotationBounds);
});

function optionStyle(option: SelectorOption, index: number) {
  return {
    "--option-angle": `${index * OPTION_ANGLE}deg`,
    "--option-index": index,
    "--option-color": option.color,
    "--option-border-color": darkenHexColor(option.color),
  };
}

function isOptionSelected(option: SelectorOption, index: number) {
  if (selectorLevel.value === "type") return index === selectedIndex.value;
  if (selectorLevel.value === "category") return option.id === selectedParent.value?.id;
  return option.id === props.categoryPath[1]?.id;
}

function chooseOption(option: SelectorOption) {
  if (pointerTravel > 5) return;

  if (option.selectorAction === "other-category") {
    emit("update:categoryPath", []);
    emit("confirm");
    return;
  }
  if (option.selectorAction === "other-subcategory") {
    if (selectedParent.value) emit("update:categoryPath", [selectedParent.value]);
    emit("confirm");
    return;
  }

  if (selectorLevel.value === "type") {
    const nextType = option.id as EntryType;
    if (nextType !== props.entryType) emit("update:categoryPath", []);
    emit("update:entryType", nextType);
    selectorLevel.value = "category";
    return;
  }

  const category = option as Category;
  if (selectorLevel.value === "category") {
    const currentChild = selectedParent.value?.id === category.id ? props.categoryPath[1] : undefined;
    emit("update:categoryPath", currentChild ? [category, currentChild] : [category]);
    if (category.children?.length) selectorLevel.value = "subcategory";
    else emit("confirm");
    return;
  }

  if (!selectedParent.value) return;
  emit("update:categoryPath", [selectedParent.value, category]);
  emit("confirm");
}

function goBack() {
  if (selectorLevel.value === "type") {
    emit("close");
    return;
  }
  if (selectorLevel.value === "subcategory") {
    if (selectedParent.value) emit("update:categoryPath", [selectedParent.value]);
    selectorLevel.value = "category";
    return;
  }
  if (selectorLevel.value === "category") {
    emit("update:categoryPath", []);
    selectorLevel.value = "type";
  }
}

function arrangeOptions() {
  // Keep the unused arc centered on the clipped, screen-right side.
  wheelRotation.value = defaultRotation.value;
}

function updateRotationBounds() {
  if (wheelIsCircular.value) return;
  const stageBounds = wheelStage.value?.getBoundingClientRect();
  const shellBounds = wheelStage.value?.closest<HTMLElement>(".app-shell")?.getBoundingClientRect();
  const option = wheelStage.value?.querySelector<HTMLElement>(".wheel-option");
  const optionBounds = option?.getBoundingClientRect();
  if (!stageBounds || !shellBounds || !option || !optionBounds) {
    rotationBounds.value = { min: defaultRotation.value, max: defaultRotation.value };
    wheelRotation.value = clampRotation(wheelRotation.value);
    return;
  }

  const centerX = stageBounds.left + stageBounds.width / 2;
  const optionCenterX = optionBounds.left + optionBounds.width / 2;
  const optionCenterY = optionBounds.top + optionBounds.height / 2;
  const centerY = stageBounds.top + stageBounds.height / 2;
  const orbitRadius = Math.hypot(optionCenterX - centerX, optionCenterY - centerY);
  const optionRadius = Number.parseFloat(getComputedStyle(option).width) / 2;
  const centerLimitX = shellBounds.right - optionRadius - SCREEN_EDGE_GAP;
  const edgeRatio = Math.max(-1, Math.min(1, (centerLimitX - centerX) / orbitRadius));
  const edgeAngle = (Math.asin(edgeRatio) * 180) / Math.PI;
  const optionSpan = Math.max(0, selectorOptions.value.length - 1) * OPTION_ANGLE;
  const visibleArc = 180 + edgeAngle * 2;

  if (optionSpan <= visibleArc) {
    rotationBounds.value = { min: defaultRotation.value, max: defaultRotation.value };
    wheelRotation.value = clampRotation(wheelRotation.value);
    return;
  }

  rotationBounds.value = {
    min: 360 + edgeAngle - optionSpan,
    max: 180 - edgeAngle,
  };
  wheelRotation.value = clampRotation(wheelRotation.value);
}

function withOtherOption(options: Category[], action: SelectorAction): SelectorOption[] {
  if (options.some((option) => option.label === "其他")) return options;
  const source =
    action === "other-category"
      ? entryTypes.find((option) => option.id === props.entryType)
      : selectedParent.value;
  if (!source) return options;
  return [
    ...options,
    {
      id: `__${action}`,
      label: "其他",
      color: source.color,
      tint: source.tint,
      icon: CircleEllipsis,
      selectorAction: action,
    },
  ];
}

function prioritizeOptions(options: SelectorOption[]) {
  const other = options.find((option) => option.label === "其他");
  if (!other) return options;
  const regularOptions = options.filter((option) => option !== other);
  const arranged = Array<SelectorOption>(options.length);
  const lastRegularIndex = options.length - 2;
  const centerIndex = Math.floor((options.length - 1) / 2);
  const priorityIndexes = [centerIndex];

  for (let offset = 1; priorityIndexes.length < regularOptions.length; offset += 1) {
    if (centerIndex - offset >= 0) priorityIndexes.push(centerIndex - offset);
    if (centerIndex + offset <= lastRegularIndex) priorityIndexes.push(centerIndex + offset);
  }

  regularOptions.forEach((option, index) => {
    arranged[priorityIndexes[index]] = option;
  });
  arranged[options.length - 1] = other;
  return arranged;
}

function pointerAngle(event: PointerEvent) {
  const bounds = wheelStage.value?.getBoundingClientRect();
  if (!bounds) return 0;
  const centerX = bounds.left + bounds.width / 2;
  const centerY = bounds.top + bounds.height / 2;
  return (Math.atan2(event.clientY - centerY, event.clientX - centerX) * 180) / Math.PI;
}

function startWheelDrag(event: PointerEvent) {
  stopInertia();
  wheelPointerId.value = event.pointerId;
  pointerLastAngle = pointerAngle(event);
  pointerLastTime = performance.now();
  pointerVelocity = 0;
  pointerTravel = 0;
}

function moveWheelDrag(event: PointerEvent) {
  if (wheelPointerId.value !== event.pointerId) return;
  const angle = pointerAngle(event);
  const delta = normalizeAngle(angle - pointerLastAngle);
  const now = performance.now();
  const elapsed = Math.max(1, now - pointerLastTime);
  const instantaneousVelocity = delta / elapsed;
  wheelRotation.value = applyBoundaryResistance(wheelRotation.value + delta);
  pointerTravel += Math.abs(delta);
  if (pointerTravel > 5 && wheelStage.value && !wheelStage.value.hasPointerCapture(event.pointerId)) {
    wheelStage.value.setPointerCapture(event.pointerId);
  }
  pointerVelocity = Math.max(
    -1.2,
    Math.min(1.2, pointerVelocity * 0.62 + instantaneousVelocity * 0.38),
  );
  pointerLastAngle = angle;
  pointerLastTime = now;
}

function endWheelDrag(event: PointerEvent) {
  if (wheelPointerId.value !== event.pointerId) return;
  wheelPointerId.value = null;
  if (wheelStage.value?.hasPointerCapture(event.pointerId)) {
    wheelStage.value.releasePointerCapture(event.pointerId);
  }
  const releaseDelay = Math.max(0, performance.now() - pointerLastTime - 16);
  pointerVelocity *= Math.exp(-releaseDelay / 90);
  const boundedRotation = clampRotation(wheelRotation.value);
  if (boundedRotation !== wheelRotation.value) {
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
      wheelRotation.value = boundedRotation;
      pointerVelocity = 0;
    } else {
      startBoundarySpring(boundedRotation);
    }
    return;
  }
  if (
    pointerTravel > 5 &&
    Math.abs(pointerVelocity) > 0.008 &&
    !window.matchMedia("(prefers-reduced-motion: reduce)").matches
  ) {
    startInertia();
  } else {
    pointerVelocity = 0;
  }
}

function cancelOptionClick(event: MouseEvent) {
  if (pointerTravel <= 5) return;
  event.preventDefault();
  event.stopPropagation();
  pointerTravel = 0;
}

function startInertia() {
  let previousTime = performance.now();
  const step = (now: number) => {
    const elapsed = Math.min(32, now - previousTime);
    previousTime = now;
    const nextRotation = wheelRotation.value + pointerVelocity * elapsed;
    const boundedRotation = clampRotation(nextRotation);
    if (boundedRotation !== nextRotation) {
      wheelRotation.value = applyBoundaryResistance(nextRotation);
      inertiaFrame = null;
      startBoundarySpring(boundedRotation);
      return;
    }
    wheelRotation.value = nextRotation;
    pointerVelocity *= Math.exp(-elapsed / 280);
    if (Math.abs(pointerVelocity) < 0.002) {
      inertiaFrame = null;
      pointerVelocity = 0;
      return;
    }
    inertiaFrame = requestAnimationFrame(step);
  };
  inertiaFrame = requestAnimationFrame(step);
}

function startBoundarySpring(target: number) {
  let velocity = pointerVelocity * 1000;
  let previousTime = performance.now();
  const step = (now: number) => {
    const elapsed = Math.min(0.032, (now - previousTime) / 1000);
    previousTime = now;
    const displacement = target - wheelRotation.value;
    const acceleration = displacement * 150 - velocity * 20;
    velocity += acceleration * elapsed;
    wheelRotation.value += velocity * elapsed;
    if (Math.abs(displacement) < 0.05 && Math.abs(velocity) < 2) {
      wheelRotation.value = target;
      inertiaFrame = null;
      pointerVelocity = 0;
      return;
    }
    inertiaFrame = requestAnimationFrame(step);
  };
  inertiaFrame = requestAnimationFrame(step);
}

function stopInertia() {
  if (inertiaFrame !== null) cancelAnimationFrame(inertiaFrame);
  inertiaFrame = null;
  pointerVelocity = 0;
}

function normalizeAngle(value: number) {
  return ((value + 180) % 360 + 360) % 360 - 180;
}

function normalizeCircleAngle(value: number) {
  return ((value % 360) + 360) % 360;
}

function clampRotation(value: number) {
  if (wheelIsCircular.value) return value;
  return Math.max(rotationBounds.value.min, Math.min(rotationBounds.value.max, value));
}

function applyBoundaryResistance(value: number) {
  const bounded = clampRotation(value);
  if (bounded === value) return value;
  const overflow = value - bounded;
  return bounded + Math.sign(overflow) * (1 - Math.exp(-Math.abs(overflow) / 45)) * 18;
}

function darkenHexColor(hex: string) {
  const normalized = hex.replace("#", "");
  if (!/^[\da-f]{6}$/i.test(normalized)) return hex;
  const channels = [0, 2, 4].map((offset) =>
    Math.round(Number.parseInt(normalized.slice(offset, offset + 2), 16) * 0.82),
  );
  return `#${channels.map((channel) => channel.toString(16).padStart(2, "0")).join("")}`;
}

function resetShellScroll() {
  const shell = wheelStage.value?.closest<HTMLElement>(".app-shell");
  if (shell) shell.scrollLeft = 0;
}
</script>

<template>
  <Transition name="coordinate-selector">
    <div v-if="open" class="category-wheel-selector" :aria-label="levelLabel" @click.stop>
      <span class="wheel-level-label">{{ levelLabel }}</span>

      <div
        ref="wheelStage"
        class="category-wheel-stage"
        :class="{ dragging: wheelPointerId !== null }"
        @pointerdown="startWheelDrag"
        @pointermove="moveWheelDrag"
        @pointerup="endWheelDrag"
        @pointercancel="endWheelDrag"
        @click.capture="cancelOptionClick"
      >
        <div :key="selectorLevel" class="category-wheel-rotor" :style="rotorStyle">
          <button
            v-for="(option, index) in selectorOptions"
            :key="`${selectorLevel}-${option.id}-${index}`"
            type="button"
            class="wheel-option"
            :class="{
              selected: isOptionSelected(option, index),
              'other-option': option.label === '其他',
            }"
            :style="optionStyle(option, index)"
            :aria-label="option.label"
            @click.stop="chooseOption(option)"
          >
            <span class="wheel-option-content">
              <component :is="option.icon" :size="21" />
              <span class="wheel-option-label">{{ option.label }}</span>
            </span>
          </button>
        </div>
      </div>

      <div class="wheel-center-controls">
        <button type="button" class="wheel-back" :aria-label="selectorLevel === 'type' ? '退出分类选择' : '返回上一级'" @click="goBack">
          <ArrowLeft :size="16" />
          <span>返回</span>
        </button>
      </div>
    </div>
  </Transition>
</template>
