<!-- 可缩放大图画布，统一处理预览图片的滚轮缩放与切图复位。 -->
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
import LoadingMedia from "@/components/creative/LoadingMedia.vue";

const props = defineProps<{
  src: string;
  alt: string;
}>();

const minScale = 1;
const maxScale = 5;
const scaleStep = 0.2;
const viewport = ref<HTMLElement>();
const scale = ref(minScale);
const offsetX = ref(0);
const offsetY = ref(0);
const fittedWidth = ref(0);
const fittedHeight = ref(0);
const aspectRatio = ref("16 / 9");
const dragging = ref(false);
let naturalWidth = 0;
let naturalHeight = 0;
let resizeObserver: ResizeObserver | undefined;
let dragPointerId: number | undefined;
let dragStartX = 0;
let dragStartY = 0;
let dragOffsetX = 0;
let dragOffsetY = 0;

/** 将缩放状态恢复到图片居中的原始尺寸。 */
function resetZoom() {
  scale.value = minScale;
  offsetX.value = 0;
  offsetY.value = 0;
  dragging.value = false;
  dragPointerId = undefined;
}

/** 按画布与图片原始尺寸计算完整居中的显示尺寸，避免固有尺寸超出画布后被裁切。 */
function updateFittedSize() {
  const container = viewport.value;
  if (!container || naturalWidth <= 0 || naturalHeight <= 0) return;
  const fitRatio = Math.min(container.clientWidth / naturalWidth, container.clientHeight / naturalHeight, 1);
  fittedWidth.value = naturalWidth * fitRatio;
  fittedHeight.value = naturalHeight * fitRatio;
}

/** 图片加载完成后记录原始尺寸，并将初始视图适配为完整显示。 */
function fitImage(event: Event) {
  const image = event.target as HTMLImageElement;
  naturalWidth = image.naturalWidth;
  naturalHeight = image.naturalHeight;
  if (naturalWidth > 0 && naturalHeight > 0) {
    aspectRatio.value = `${naturalWidth} / ${naturalHeight}`;
  }
  updateFittedSize();
  resetZoom();
}

/** 按当前倍率限制平移范围，确保放大图片不会被完全拖出画布。 */
function clampOffset(x: number, y: number, nextScale = scale.value) {
  const container = viewport.value;
  if (!container || fittedWidth.value <= 0 || fittedHeight.value <= 0) return { x: 0, y: 0 };
  const maxX = Math.max(0, (fittedWidth.value * nextScale - container.clientWidth) / 2);
  const maxY = Math.max(0, (fittedHeight.value * nextScale - container.clientHeight) / 2);
  return {
    x: Math.min(maxX, Math.max(-maxX, x)),
    y: Math.min(maxY, Math.max(-maxY, y)),
  };
}

/** 以滚轮指针所在位置为中心缩放图片，并限制可用倍率范围。 */
function zoomByWheel(event: WheelEvent) {
  const container = viewport.value;
  if (!container) return;
  const previousScale = scale.value;
  const direction = event.deltaY < 0 ? 1 : -1;
  const nextScale = Math.min(maxScale, Math.max(minScale, Number((previousScale + direction * scaleStep).toFixed(2))));
  if (nextScale === previousScale) return;
  if (nextScale === minScale) {
    resetZoom();
    return;
  }
  const bounds = container.getBoundingClientRect();
  const pointerX = event.clientX - bounds.left - bounds.width / 2;
  const pointerY = event.clientY - bounds.top - bounds.height / 2;
  const ratio = nextScale / previousScale;
  const nextOffset = clampOffset(
    pointerX - (pointerX - offsetX.value) * ratio,
    pointerY - (pointerY - offsetY.value) * ratio,
    nextScale,
  );
  scale.value = nextScale;
  offsetX.value = nextOffset.x;
  offsetY.value = nextOffset.y;
}

/** 在图片放大后开始记录鼠标拖拽位置。 */
function startDrag(event: PointerEvent) {
  if (event.pointerType === "mouse" && event.button !== 0) return;
  if (scale.value <= minScale) return;
  const target = event.currentTarget as HTMLElement;
  target.setPointerCapture(event.pointerId);
  dragPointerId = event.pointerId;
  dragStartX = event.clientX;
  dragStartY = event.clientY;
  dragOffsetX = offsetX.value;
  dragOffsetY = offsetY.value;
  dragging.value = true;
}

/** 根据鼠标位移平移放大图片，并约束图片可见边界。 */
function moveDrag(event: PointerEvent) {
  if (!dragging.value || event.pointerId !== dragPointerId) return;
  const nextOffset = clampOffset(dragOffsetX + event.clientX - dragStartX, dragOffsetY + event.clientY - dragStartY);
  offsetX.value = nextOffset.x;
  offsetY.value = nextOffset.y;
}

/** 结束当前图片拖拽并释放指针捕获。 */
function stopDrag(event: PointerEvent) {
  if (event.pointerId !== dragPointerId) return;
  const target = event.currentTarget as HTMLElement;
  if (target.hasPointerCapture(event.pointerId)) target.releasePointerCapture(event.pointerId);
  dragging.value = false;
  dragPointerId = undefined;
}

watch(
  () => props.src,
  () => {
    naturalWidth = 0;
    naturalHeight = 0;
    fittedWidth.value = 0;
    fittedHeight.value = 0;
    aspectRatio.value = "16 / 9";
    resetZoom();
  },
);

onMounted(() => {
  resizeObserver = new ResizeObserver(() => {
    updateFittedSize();
    resetZoom();
  });
  if (viewport.value) resizeObserver.observe(viewport.value);
});

onBeforeUnmount(() => resizeObserver?.disconnect());
</script>

<template>
  <div
    ref="viewport"
    class="zoomable-image"
    :class="{ 'is-zoomed': scale > minScale, 'is-dragging': dragging }"
    :style="{ '--zoomable-aspect-ratio': aspectRatio }"
    @wheel.prevent="zoomByWheel"
    @pointerdown="startDrag"
    @pointermove="moveDrag"
    @pointerup="stopDrag"
    @pointercancel="stopDrag"
  >
    <LoadingMedia
      type="image"
      class="zoomable-image__media"
      :src="src"
      :alt="alt"
      :style="{
        width: fittedWidth > 0 ? `${fittedWidth}px` : undefined,
        height: fittedHeight > 0 ? `${fittedHeight}px` : undefined,
        transform: `translate(${offsetX}px, ${offsetY}px) scale(${scale})`,
      }"
      @load="fitImage"
    />
  </div>
</template>

<style scoped>
.zoomable-image {
  width: 100%;
  height: auto;
  aspect-ratio: var(--zoomable-aspect-ratio, 16 / 9);
  max-height: calc(100dvh - 120px);
  min-width: 0;
  min-height: 0;
  position: relative;
  display: grid;
  place-items: center;
  overflow: hidden;
  cursor: zoom-in;
  touch-action: none;
  user-select: none;
}
.zoomable-image :deep(.zoomable-image__media) {
  max-width: none;
  max-height: none;
  display: block;
  object-fit: contain;
  pointer-events: none;
  transition: transform 0.16s ease-out;
}
.zoomable-image.is-zoomed {
  cursor: grab;
}
.zoomable-image.is-dragging {
  cursor: grabbing;
}
.zoomable-image.is-dragging :deep(.zoomable-image__media) {
  transition: none;
}
</style>
