<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref } from "vue";
import CrmCalendarLabDayRect from "~~/app/components/workspace/calendar/lab/CrmCalendarLabDayRect.vue";
import CrmCalendarLabMonthRect from "~~/app/components/workspace/calendar/lab/CrmCalendarLabMonthRect.vue";
import CrmCalendarLabWeekRect from "~~/app/components/workspace/calendar/lab/CrmCalendarLabWeekRect.vue";
import CrmCalendarLabYearRect from "~~/app/components/workspace/calendar/lab/CrmCalendarLabYearRect.vue";

type Level = "year" | "month" | "week" | "day";
type LevelState = {
  loaded: boolean;
  loading: boolean;
};

const LEVELS: Level[] = ["year", "month", "week", "day"];
const LEVEL_LABELS: Record<Level, string> = {
  year: "Year",
  month: "Month",
  week: "Week",
  day: "Day",
};

const NEXT_LEVEL: Record<Level, Level | null> = {
  year: "month",
  month: "week",
  week: "day",
  day: null,
};

const PREV_LEVEL: Record<Level, Level | null> = {
  year: null,
  month: "year",
  week: "month",
  day: "week",
};

const ZOOM_PRIME_STEPS = 2;
const ZOOM_PRIME_SCALE = 1.05;
const ZOOM_ANIMATION_MS = 2200;
const WORLD_PADDING = 20;

const WORLD_RECT_BY_LEVEL: Record<Level, { x: number; y: number; width: number; height: number }> = {
  year: { x: 80, y: 50, width: 1240, height: 760 },
  month: { x: 330, y: 195, width: 740, height: 430 },
  week: { x: 470, y: 275, width: 460, height: 270 },
  day: { x: 560, y: 330, width: 280, height: 165 },
};

const viewportRef = ref<HTMLDivElement | null>(null);
const resizeObserver = ref<ResizeObserver | null>(null);
const currentLevel = ref<Level>("year");
const transitionTarget = ref<Level | null>(null);
const hoveredLevel = ref<Level>("month");
const isAnimating = ref(false);
const useTransition = ref(false);

const camera = reactive({
  x: 0,
  y: 0,
  scaleX: 1,
  scaleY: 1,
});

const levelState = reactive<Record<Level, LevelState>>({
  year: { loaded: true, loading: false },
  month: { loaded: false, loading: false },
  week: { loaded: false, loading: false },
  day: { loaded: false, loading: false },
});

const wheelPrime = reactive({
  direction: "" as "" | "in" | "out",
  ticks: 0,
});

const pulseState = reactive({
  level: "" as "" | Level,
  scale: 1,
});

let animationTimer: ReturnType<typeof setTimeout> | null = null;
let primeTimer: ReturnType<typeof setTimeout> | null = null;
let loadTimer: ReturnType<typeof setTimeout> | null = null;

const displayLevel = computed(() => transitionTarget.value ?? currentLevel.value);
const displayIndex = computed(() => LEVELS.indexOf(displayLevel.value));
const canZoomIn = computed(() => NEXT_LEVEL[currentLevel.value] !== null);
const canZoomOut = computed(() => PREV_LEVEL[currentLevel.value] !== null);

const sceneStyle = computed(() => ({
  transform: `translate3d(${camera.x}px, ${camera.y}px, 0) scale(${camera.scaleX}, ${camera.scaleY})`,
  transformOrigin: "0 0",
  transition: useTransition.value ? `transform ${ZOOM_ANIMATION_MS}ms cubic-bezier(0.2, 0.8, 0.2, 1)` : "none",
}));

function pulseScaleFor(level: Level) {
  return pulseState.level === level ? pulseState.scale : 1;
}

function nextLevelOf(level: Level) {
  return NEXT_LEVEL[level];
}

function prevLevelOf(level: Level) {
  return PREV_LEVEL[level];
}

function levelByIndex(index: number): Level {
  return LEVELS[Math.max(0, Math.min(LEVELS.length - 1, index))] ?? "year";
}

function isNodeVisible(level: Level) {
  const currentIndex = LEVELS.indexOf(currentLevel.value);
  const targetIndex = transitionTarget.value ? LEVELS.indexOf(transitionTarget.value) : currentIndex;
  const maxIndex = Math.max(currentIndex, targetIndex);
  return LEVELS.indexOf(level) <= Math.min(maxIndex + 1, LEVELS.length - 1);
}

function showLevelContent(level: Level) {
  if (isAnimating.value) return false;
  return currentLevel.value === level;
}

function updateCameraForLevel(level: Level, animate: boolean) {
  const viewport = viewportRef.value;
  if (!viewport) return;

  const rect = WORLD_RECT_BY_LEVEL[level];
  const viewportWidth = Math.max(1, viewport.clientWidth);
  const viewportHeight = Math.max(1, viewport.clientHeight);
  const safeWidth = Math.max(1, viewportWidth - WORLD_PADDING * 2);
  const safeHeight = Math.max(1, viewportHeight - WORLD_PADDING * 2);

  const scaleX = safeWidth / rect.width;
  const scaleY = safeHeight / rect.height;

  const x = WORLD_PADDING - rect.x * scaleX;
  const y = WORLD_PADDING - rect.y * scaleY;

  useTransition.value = animate;
  camera.x = x;
  camera.y = y;
  camera.scaleX = scaleX;
  camera.scaleY = scaleY;
}

async function ensureGraphqlPayload(level: Level) {
  const state = levelState[level];
  if (state.loaded || state.loading) return;
  state.loading = true;

  if (loadTimer) {
    clearTimeout(loadTimer);
    loadTimer = null;
  }

  await new Promise<void>((resolve) => {
    loadTimer = setTimeout(() => {
      state.loading = false;
      state.loaded = true;
      resolve();
    }, 540);
  });
}

function startPulse(level: Level) {
  if (primeTimer) {
    clearTimeout(primeTimer);
    primeTimer = null;
  }

  pulseState.level = level;
  pulseState.scale = ZOOM_PRIME_SCALE;

  primeTimer = setTimeout(() => {
    pulseState.scale = 1;
    pulseState.level = "";
  }, 170);
}

function resetWheelPrime() {
  wheelPrime.direction = "";
  wheelPrime.ticks = 0;
}

async function runZoomTo(target: Level) {
  if (isAnimating.value) return;
  if (target === currentLevel.value) return;

  isAnimating.value = true;
  transitionTarget.value = target;

  await nextTick();
  updateCameraForLevel(target, true);

  if (animationTimer) {
    clearTimeout(animationTimer);
    animationTimer = null;
  }

  animationTimer = setTimeout(async () => {
    useTransition.value = false;
    currentLevel.value = target;
    transitionTarget.value = null;
    isAnimating.value = false;
    hoveredLevel.value = nextLevelOf(target) ?? target;
    await ensureGraphqlPayload(target);
  }, ZOOM_ANIMATION_MS + 30);
}

function onWheel(event: WheelEvent) {
  if (isAnimating.value) return;

  const direction: "in" | "out" = event.deltaY > 0 ? "in" : "out";
  const nextLevel = direction === "in" ? nextLevelOf(currentLevel.value) : prevLevelOf(currentLevel.value);
  if (!nextLevel) return;

  if (wheelPrime.direction !== direction) {
    wheelPrime.direction = direction;
    wheelPrime.ticks = 0;
  }

  const pulseTarget = direction === "in" ? (hoveredLevel.value === nextLevel ? hoveredLevel.value : nextLevel) : currentLevel.value;
  if (wheelPrime.ticks < ZOOM_PRIME_STEPS) {
    wheelPrime.ticks += 1;
    startPulse(pulseTarget);
    return;
  }

  resetWheelPrime();
  void runZoomTo(nextLevel);
}

function onSliderInput(event: Event) {
  const target = event.target as HTMLInputElement | null;
  if (!target) return;

  const next = levelByIndex(Number(target.value));
  resetWheelPrime();
  void runZoomTo(next);
}

function onZoomInClick() {
  const next = nextLevelOf(currentLevel.value);
  if (!next) return;
  resetWheelPrime();
  void runZoomTo(next);
}

function onZoomOutClick() {
  const prev = prevLevelOf(currentLevel.value);
  if (!prev) return;
  resetWheelPrime();
  void runZoomTo(prev);
}

onMounted(async () => {
  await nextTick();
  updateCameraForLevel(currentLevel.value, false);
  hoveredLevel.value = nextLevelOf(currentLevel.value) ?? currentLevel.value;

  resizeObserver.value = new ResizeObserver(() => {
    updateCameraForLevel(displayLevel.value, false);
  });

  if (viewportRef.value) {
    resizeObserver.value.observe(viewportRef.value);
  }
});

onBeforeUnmount(() => {
  if (animationTimer) clearTimeout(animationTimer);
  if (primeTimer) clearTimeout(primeTimer);
  if (loadTimer) clearTimeout(loadTimer);
  resizeObserver.value?.disconnect();
});
</script>

<template>
  <section class="calendar-lab-root">
    <header class="calendar-lab-toolbar">
      <div class="calendar-lab-toolbar-left">
        <button class="btn btn-xs" :disabled="!canZoomOut" @click="onZoomOutClick">-</button>
        <button class="btn btn-xs" :disabled="!canZoomIn" @click="onZoomInClick">+</button>
        <span class="calendar-lab-level-text">Current: {{ LEVEL_LABELS[currentLevel] }}</span>
      </div>

      <div class="calendar-zoom-inline" @click.stop>
        <input
          class="calendar-zoom-slider"
          type="range"
          min="0"
          max="3"
          step="1"
          :value="displayIndex"
          aria-label="Calendar zoom level"
          @input="onSliderInput"
        >
        <div class="calendar-zoom-marks" aria-hidden="true">
          <span
            v-for="index in 4"
            :key="`calendar-lab-zoom-mark-${index}`"
            class="calendar-zoom-mark"
            :class="displayIndex === index - 1 ? 'calendar-zoom-mark-active' : ''"
          />
        </div>
      </div>
    </header>

    <div
      ref="viewportRef"
      class="calendar-lab-viewport"
      :class="canZoomIn ? 'cursor-zoom-in' : 'cursor-zoom-out'"
      @wheel.prevent="onWheel"
    >
      <div class="calendar-lab-scene" :style="sceneStyle">
        <div class="calendar-lab-world">
          <div
            v-show="isNodeVisible('year')"
            class="calendar-lab-node calendar-lab-node-year"
            :class="[
              currentLevel === 'year' ? 'calendar-lab-node-active' : '',
              hoveredLevel === 'year' ? 'calendar-lab-node-hover' : '',
              transitionTarget === 'year' ? 'calendar-lab-node-target' : '',
            ]"
            @mouseenter="hoveredLevel = 'year'"
          >
            <CrmCalendarLabYearRect
              :is-active="currentLevel === 'year'"
              :is-loading="levelState.year.loading"
              :is-loaded="levelState.year.loaded"
              :show-content="showLevelContent('year')"
              :next-label="LEVEL_LABELS.month"
              :pulse-scale="pulseScaleFor('year')"
            />

            <div
              v-show="isNodeVisible('month')"
              class="calendar-lab-node calendar-lab-node-month"
              :class="[
                currentLevel === 'month' ? 'calendar-lab-node-active' : '',
                hoveredLevel === 'month' ? 'calendar-lab-node-hover' : '',
                transitionTarget === 'month' ? 'calendar-lab-node-target' : '',
              ]"
              @mouseenter="hoveredLevel = 'month'"
            >
              <CrmCalendarLabMonthRect
                :is-active="currentLevel === 'month'"
                :is-loading="levelState.month.loading"
                :is-loaded="levelState.month.loaded"
                :show-content="showLevelContent('month')"
                :next-label="LEVEL_LABELS.week"
                :pulse-scale="pulseScaleFor('month')"
              />

              <div
                v-show="isNodeVisible('week')"
                class="calendar-lab-node calendar-lab-node-week"
                :class="[
                  currentLevel === 'week' ? 'calendar-lab-node-active' : '',
                  hoveredLevel === 'week' ? 'calendar-lab-node-hover' : '',
                  transitionTarget === 'week' ? 'calendar-lab-node-target' : '',
                ]"
                @mouseenter="hoveredLevel = 'week'"
              >
                <CrmCalendarLabWeekRect
                  :is-active="currentLevel === 'week'"
                  :is-loading="levelState.week.loading"
                  :is-loaded="levelState.week.loaded"
                  :show-content="showLevelContent('week')"
                  :next-label="LEVEL_LABELS.day"
                  :pulse-scale="pulseScaleFor('week')"
                />

                <div
                  v-show="isNodeVisible('day')"
                  class="calendar-lab-node calendar-lab-node-day"
                  :class="[
                    currentLevel === 'day' ? 'calendar-lab-node-active' : '',
                    hoveredLevel === 'day' ? 'calendar-lab-node-hover' : '',
                    transitionTarget === 'day' ? 'calendar-lab-node-target' : '',
                  ]"
                  @mouseenter="hoveredLevel = 'day'"
                >
                  <CrmCalendarLabDayRect
                    :is-active="currentLevel === 'day'"
                    :is-loading="levelState.day.loading"
                    :is-loaded="levelState.day.loaded"
                    :show-content="showLevelContent('day')"
                    :pulse-scale="pulseScaleFor('day')"
                  />
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </section>
</template>

<style scoped>
.calendar-lab-root {
  height: calc(100dvh - 2.5rem);
  min-height: 620px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 10px;
}

.calendar-lab-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

.calendar-lab-toolbar-left {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.calendar-lab-level-text {
  font-size: 0.78rem;
  color: color-mix(in oklab, var(--color-base-content) 72%, transparent);
}

.calendar-lab-viewport {
  position: relative;
  flex: 1;
  min-height: 0;
  border-radius: 14px;
  border: 1px solid color-mix(in oklab, var(--color-base-content) 18%, transparent);
  background:
    radial-gradient(circle at 15% 15%, color-mix(in oklab, var(--color-base-content) 6%, transparent), transparent 40%),
    color-mix(in oklab, var(--color-base-100) 94%, transparent);
  overflow: hidden;
}

.calendar-lab-scene {
  width: 1400px;
  height: 900px;
  will-change: transform;
}

.calendar-lab-world {
  width: 1400px;
  height: 900px;
  position: relative;
}

.calendar-lab-node {
  position: absolute;
}

.calendar-lab-node-year {
  left: 80px;
  top: 50px;
  width: 1240px;
  height: 760px;
}

.calendar-lab-node-month {
  left: 250px;
  top: 145px;
  width: 740px;
  height: 430px;
}

.calendar-lab-node-week {
  left: 140px;
  top: 80px;
  width: 460px;
  height: 270px;
}

.calendar-lab-node-day {
  left: 90px;
  top: 55px;
  width: 280px;
  height: 165px;
}

.calendar-lab-node-active :deep(.calendar-lab-rect),
.calendar-lab-node-target :deep(.calendar-lab-rect) {
  border-color: color-mix(in oklab, var(--color-primary) 85%, transparent);
  box-shadow:
    0 0 0 2px color-mix(in oklab, var(--color-primary) 45%, transparent),
    0 16px 36px color-mix(in oklab, var(--color-base-content) 10%, transparent);
}

.calendar-lab-node-hover :deep(.calendar-lab-rect) {
  border-color: color-mix(in oklab, var(--color-primary) 70%, transparent);
}

:deep(.calendar-lab-rect) {
  width: 100%;
  height: 100%;
  border-radius: 14px;
  border: 1px solid color-mix(in oklab, var(--color-base-content) 24%, transparent);
  background: color-mix(in oklab, var(--color-base-100) 96%, transparent);
  padding: 12px;
  transform-origin: center center;
  transition:
    transform 200ms ease,
    border-color 180ms ease,
    box-shadow 180ms ease,
    background-color 180ms ease;
}

:deep(.calendar-lab-header) {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 8px;
}

:deep(.calendar-lab-title) {
  font-size: 0.84rem;
  font-weight: 700;
}

:deep(.calendar-lab-subtitle) {
  font-size: 0.7rem;
  color: color-mix(in oklab, var(--color-base-content) 65%, transparent);
}

:deep(.calendar-lab-loading),
:deep(.calendar-lab-meta),
:deep(.calendar-lab-hint) {
  margin-bottom: 8px;
  font-size: 0.72rem;
}

:deep(.calendar-lab-loading) {
  color: color-mix(in oklab, var(--color-warning) 85%, var(--color-base-content));
}

:deep(.calendar-lab-meta) {
  color: color-mix(in oklab, var(--color-success) 86%, var(--color-base-content));
}

:deep(.calendar-lab-hint) {
  color: color-mix(in oklab, var(--color-base-content) 68%, transparent);
}

:deep(.calendar-lab-grid-year) {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: 6px;
}

:deep(.calendar-lab-chip),
:deep(.calendar-lab-row),
:deep(.calendar-lab-day),
:deep(.calendar-lab-event) {
  border-radius: 9px;
  border: 1px solid color-mix(in oklab, var(--color-base-content) 16%, transparent);
  background: color-mix(in oklab, var(--color-base-200) 78%, transparent);
}

:deep(.calendar-lab-chip) {
  min-height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 0.74rem;
  font-weight: 600;
}

:deep(.calendar-lab-grid-month) {
  display: grid;
  grid-template-columns: 1fr;
  gap: 6px;
}

:deep(.calendar-lab-row) {
  min-height: 60px;
  display: flex;
  align-items: center;
  padding: 0 8px;
  font-size: 0.75rem;
  font-weight: 600;
}

:deep(.calendar-lab-grid-week) {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 6px;
}

:deep(.calendar-lab-day) {
  min-height: 70px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 0.72rem;
  font-weight: 600;
}

:deep(.calendar-lab-timeline) {
  display: grid;
  grid-template-columns: 1fr;
  gap: 6px;
}

:deep(.calendar-lab-event) {
  min-height: 44px;
  padding: 6px 8px;
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 0.72rem;
}

:deep(.calendar-lab-event span) {
  font-weight: 700;
  min-width: 50px;
}

.calendar-zoom-inline {
  position: relative;
  display: flex;
  align-items: center;
  width: 128px;
  height: 22px;
  padding: 0 10px;
}

.calendar-zoom-slider {
  width: 100%;
  height: 18px;
  margin: 0;
  background: transparent;
  -webkit-appearance: none;
  appearance: none;
  cursor: pointer;
}

.calendar-zoom-slider:focus-visible {
  outline: none;
}

.calendar-zoom-slider::-webkit-slider-runnable-track {
  height: 2px;
  border-radius: 999px;
  background: color-mix(in oklab, var(--color-base-content) 22%, transparent);
}

.calendar-zoom-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 10px;
  height: 10px;
  margin-top: -4px;
  border-radius: 999px;
  background: color-mix(in oklab, var(--color-base-content) 88%, transparent);
  border: 0;
}

.calendar-zoom-slider::-moz-range-track {
  height: 2px;
  border-radius: 999px;
  background: color-mix(in oklab, var(--color-base-content) 22%, transparent);
}

.calendar-zoom-slider::-moz-range-progress {
  height: 2px;
  border-radius: 999px;
  background: transparent;
}

.calendar-zoom-slider::-moz-range-thumb {
  width: 10px;
  height: 10px;
  border: 0;
  border-radius: 999px;
  background: color-mix(in oklab, var(--color-base-content) 88%, transparent);
}

.calendar-zoom-marks {
  position: absolute;
  inset: 0 10px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  pointer-events: none;
}

.calendar-zoom-mark {
  width: 4px;
  height: 4px;
  border-radius: 999px;
  background: color-mix(in oklab, var(--color-base-content) 35%, transparent);
}

.calendar-zoom-mark-active {
  background: color-mix(in oklab, var(--color-base-content) 85%, transparent);
}
</style>
