<script setup>
import { ref, computed, reactive, watch, onUnmounted, nextTick } from "vue";
import { useNamespace, useLocale } from "@yh-ui/hooks";
import { useComponentTheme } from "@yh-ui/theme";
import { imageMagnifierProps, imageMagnifierEmits } from "./image-magnifier";
defineOptions({ name: "YhImageMagnifier" });
const props = defineProps(imageMagnifierProps);
const emit = defineEmits(imageMagnifierEmits);
const ns = useNamespace("image-magnifier");
const { t } = useLocale();
const { themeStyle } = useComponentTheme(
  "image-magnifier",
  computed(() => props.themeOverrides)
);
const rootStyle = computed(() => {
  const { width, height, loadingColor } = props;
  return {
    ...themeStyle.value,
    width: typeof width === "number" ? `${width}px` : width,
    height: typeof height === "number" ? `${height}px` : height,
    ...loadingColor ? { "--yh-magnifier-loading-color": loadingColor } : {}
  };
});
const currentIndex = ref(props.modelValue);
watch(
  () => props.modelValue,
  (val) => {
    currentIndex.value = val;
  }
);
const galleryImages = computed(() => {
  if (props.images.length > 0) return props.images;
  return [{ src: props.src, zoomSrc: props.zoomSrc, alt: props.alt }];
});
const currentImage = computed(
  () => galleryImages.value[currentIndex.value] ?? galleryImages.value[0]
);
const currentSrc = computed(() => currentImage.value.src);
const currentZoomSrc = computed(() => currentImage.value.zoomSrc || currentImage.value.src);
const currentAlt = computed(() => currentImage.value.alt || props.alt);
const getSwitchToImageLabel = (index) => t("imagemagnifier.switchToImage", { index: index + 1 });
const getGalleryItemAlt = (index) => t("imagemagnifier.galleryItem", { index: index + 1 });
const fullscreenCloseLabel = computed(() => t("imagemagnifier.close"));
const switchImage = (index) => {
  currentIndex.value = index;
  emit("update:modelValue", index);
  emit("image-change", index);
};
const isHdLoaded = ref(false);
const isHdLoading = ref(false);
const preloadHdImage = () => {
  if (isHdLoaded.value || isHdLoading.value) return;
  const zoomUrl = currentZoomSrc.value;
  if (!zoomUrl) return;
  isHdLoading.value = true;
  const img = new Image();
  img.onload = () => {
    isHdLoaded.value = true;
    isHdLoading.value = false;
  };
  img.onerror = () => {
    isHdLoading.value = false;
  };
  img.src = zoomUrl;
};
watch(currentIndex, () => {
  isHdLoaded.value = false;
  isHdLoading.value = false;
});
const visible = ref(false);
const maskPos = reactive({ top: 0, left: 0 });
const largePos = reactive({ top: 0, left: 0 });
const containerRef = ref(null);
const currentScale = ref(props.scale);
watch(
  () => props.scale,
  (val) => {
    currentScale.value = val;
  }
);
const resolvedPosition = ref("right");
const computeSmartPosition = () => {
  if (props.position !== "auto" && props.position !== "right" && props.position !== "left" && props.position !== "inside")
    return;
  if (props.position !== "auto") {
    resolvedPosition.value = props.position;
    return;
  }
  if (!containerRef.value) {
    resolvedPosition.value = "right";
    return;
  }
  const rect = containerRef.value.getBoundingClientRect();
  const previewW = props.maskWidth * currentScale.value;
  const rightSpace = window.innerWidth - rect.right;
  const leftSpace = rect.left;
  if (rightSpace >= previewW + props.offset) {
    resolvedPosition.value = "right";
  } else if (leftSpace >= previewW + props.offset) {
    resolvedPosition.value = "left";
  } else {
    resolvedPosition.value = "inside";
  }
};
const handleMouseEnter = () => {
  visible.value = true;
  computeSmartPosition();
  preloadHdImage();
  emit("enter");
  emit("zoom-start");
};
const handleMouseLeave = () => {
  visible.value = false;
  currentScale.value = props.scale;
  emit("leave");
  emit("zoom-end");
};
const handleMouseMove = (e) => {
  if (!containerRef.value) return;
  const rect = containerRef.value.getBoundingClientRect();
  const mouseX = e.clientX - rect.left;
  const mouseY = e.clientY - rect.top;
  let left = mouseX - props.maskWidth / 2;
  let top = mouseY - props.maskHeight / 2;
  left = Math.max(0, Math.min(left, rect.width - props.maskWidth));
  top = Math.max(0, Math.min(top, rect.height - props.maskHeight));
  maskPos.left = left;
  maskPos.top = top;
  if (resolvedPosition.value === "inside") {
    largePos.left = props.maskWidth / 2 - mouseX * currentScale.value;
    largePos.top = props.maskHeight / 2 - mouseY * currentScale.value;
  } else {
    largePos.left = -left * currentScale.value;
    largePos.top = -top * currentScale.value;
  }
};
const handleWheel = (e) => {
  if (!props.wheelZoom || !visible.value) return;
  e.preventDefault();
  const delta = e.deltaY > 0 ? -props.scaleStep : props.scaleStep;
  currentScale.value = Math.round(
    Math.min(props.maxScale, Math.max(props.minScale, currentScale.value + delta)) * 10
  ) / 10;
  emit("scale-change", currentScale.value);
};
let wheelListener = null;
const addWheelListener = () => {
  if (!props.wheelZoom || !containerRef.value) return;
  removeWheelListener();
  wheelListener = (e) => {
    handleWheel(e);
  };
  containerRef.value.addEventListener("wheel", wheelListener, { passive: false });
};
const removeWheelListener = () => {
  if (wheelListener && containerRef.value) {
    containerRef.value.removeEventListener("wheel", wheelListener);
    wheelListener = null;
  }
};
watch(
  [containerRef, () => props.wheelZoom],
  ([el, wz]) => {
    if (el && wz) {
      nextTick(addWheelListener);
    } else {
      removeWheelListener();
    }
  },
  { immediate: true }
);
const previewStyle = computed(() => {
  const isInside = resolvedPosition.value === "inside";
  const base = {
    position: "absolute",
    overflow: "hidden",
    zIndex: 100,
    pointerEvents: "none"
  };
  if (isInside) {
    return {
      ...base,
      left: `${maskPos.left}px`,
      top: `${maskPos.top}px`,
      width: `${props.maskWidth}px`,
      height: `${props.maskHeight}px`,
      borderRadius: props.maskShape === "circle" ? "50%" : "var(--yh-radius-base)",
      boxShadow: "0 0 0 1px rgba(255,255,255,0.6), 0 4px 16px rgba(0,0,0,0.25)",
      zIndex: 10,
      backgroundColor: "var(--yh-bg-color-overlay)"
    };
  }
  return {
    ...base,
    top: "0",
    width: `${props.maskWidth * currentScale.value}px`,
    height: `${props.maskHeight * currentScale.value}px`,
    ...resolvedPosition.value === "right" ? { left: `calc(100% + ${props.offset}px)` } : {},
    ...resolvedPosition.value === "left" ? { right: `calc(100% + ${props.offset}px)` } : {}
  };
});
const largeImageStyle = computed(() => {
  const w = containerRef.value?.offsetWidth ?? 0;
  const h = containerRef.value?.offsetHeight ?? 0;
  return {
    width: `${w * currentScale.value}px`,
    height: `${h * currentScale.value}px`,
    transform: `translate(${largePos.left}px, ${largePos.top}px)`,
    position: "absolute",
    top: "0",
    left: "0",
    opacity: isHdLoaded.value || !isHdLoading.value ? 1 : 0.5,
    transition: "opacity 0.2s"
  };
});
const maskStyle = computed(() => ({
  width: `${props.maskWidth}px`,
  height: `${props.maskHeight}px`,
  transform: `translate(${maskPos.left}px, ${maskPos.top}px)`,
  backgroundColor: props.maskColor,
  position: "absolute",
  top: "0",
  left: "0"
}));
const minimapIndicatorStyle = computed(() => {
  if (!containerRef.value) return {};
  const cW = containerRef.value.offsetWidth || 1;
  const cH = containerRef.value.offsetHeight || 1;
  const ratioX = maskPos.left / cW;
  const ratioY = maskPos.top / cH;
  const ratioW = props.maskWidth / cW;
  const ratioH = props.maskHeight / cH;
  return {
    left: `${ratioX * 100}%`,
    top: `${ratioY * 100}%`,
    width: `${ratioW * 100}%`,
    height: `${ratioH * 100}%`
  };
});
const fullscreenVisible = ref(false);
const openFullscreen = () => {
  if (!props.clickToFullscreen) return;
  fullscreenVisible.value = true;
};
const closeFullscreen = () => {
  fullscreenVisible.value = false;
};
const handleFullscreenKeydown = (e) => {
  if (e.key === "Escape") closeFullscreen();
};
watch(fullscreenVisible, (val) => {
  if (typeof document === "undefined") return;
  if (val) {
    document.addEventListener("keydown", handleFullscreenKeydown);
    document.documentElement.style.overflow = "hidden";
    document.body.style.overflow = "hidden";
  } else {
    document.removeEventListener("keydown", handleFullscreenKeydown);
    document.documentElement.style.overflow = "";
    document.body.style.overflow = "";
  }
});
onUnmounted(() => {
  removeWheelListener();
  if (typeof document !== "undefined") {
    document.removeEventListener("keydown", handleFullscreenKeydown);
    document.documentElement.style.overflow = "";
    document.body.style.overflow = "";
  }
});
defineExpose({ visible, currentScale, currentIndex, switchImage });
</script>

<template>
  <div
    :class="ns.b()"
    :style="rootStyle"
    ref="containerRef"
    @mouseenter="handleMouseEnter"
    @mouseleave="handleMouseLeave"
    @mousemove="handleMouseMove"
  >
    <!-- 主图区域：点击全屏绑定在 wrapper 上，避免 img pointer-events:none 吃掉事件 -->
    <div
      :class="[ns.e('image-wrapper'), ns.is('bordered', props.border), ns.is('shadow', props.shadow)]"
      :style="props.clickToFullscreen ? 'cursor:zoom-in' : ''"
      @click="openFullscreen"
    >
      <slot>
        <img :src="currentSrc" :alt="currentAlt" :class="ns.e('image')" />
      </slot>

      <!-- 渐进加载提示 -->
      <div v-if="visible && isHdLoading" :class="ns.e('loading')">
        <div :class="ns.e('loading-bar')" />
      </div>

      <!-- 遮罩 (外部模式) -->
      <div
        v-if="visible && resolvedPosition !== 'inside'"
        :class="[ns.e('mask'), ns.is(props.maskShape)]"
        :style="maskStyle"
      />

      <!-- 内部透镜预览 (inside) -->
      <div
        v-if="visible && resolvedPosition === 'inside'"
        :class="ns.e('preview')"
        :style="previewStyle"
      >
        <img
          :src="currentZoomSrc"
          :class="ns.e('large-image')"
          :style="largeImageStyle"
          :alt="currentAlt"
        />
        <!-- 地图缩略导航 -->
        <div v-if="props.showMinimap" :class="ns.e('minimap')">
          <img :src="currentSrc" :class="ns.e('minimap-img')" />
          <div :class="ns.e('minimap-indicator')" :style="minimapIndicatorStyle" />
        </div>
      </div>

      <!-- 提示文字 -->
      <div v-if="!visible && props.title" :class="ns.e('title')">
        <slot name="title">{{ props.title }}</slot>
      </div>
    </div>

    <!-- 外部预览图 (right/left) -->
    <div
      v-if="visible && resolvedPosition !== 'inside'"
      :class="ns.e('preview')"
      :style="previewStyle"
    >
      <img
        :src="currentZoomSrc"
        :class="ns.e('large-image')"
        :style="largeImageStyle"
        :alt="currentAlt"
      />
      <!-- 地图缩略导航 -->
      <div v-if="props.showMinimap" :class="ns.e('minimap')">
        <img :src="currentSrc" :class="ns.e('minimap-img')" :alt="currentAlt" />
        <div :class="ns.e('minimap-indicator')" :style="minimapIndicatorStyle" />
      </div>
    </div>

    <!-- 图片库缩略图 -->
    <div v-if="galleryImages.length > 1" :class="ns.e('gallery')">
      <button
        v-for="(img, idx) in galleryImages"
        :key="idx"
        :class="[ns.e('gallery-item'), ns.is('active', idx === currentIndex)]"
        @click="switchImage(idx)"
        type="button"
        :aria-label="getSwitchToImageLabel(idx)"
      >
        <img :src="img.src" :alt="img.alt || getGalleryItemAlt(idx)" />
      </button>
    </div>

    <!-- 当前倍率显示 (wheelZoom 开启时) -->
    <div v-if="props.wheelZoom && visible" :class="ns.e('scale-badge')"> {{ currentScale }}x </div>

    <!-- 全屏弹层：Esc 通过 document 级监听处理，不依赖元素焦点 -->
    <Teleport to="body">
      <div v-if="fullscreenVisible" :class="[ns.b(), ns.m('fullscreen-wrapper')]">
        <div
          :class="ns.e('fullscreen-overlay')"
          role="dialog"
          aria-modal="true"
          @click.self="closeFullscreen"
        >
          <button
            :class="ns.e('fullscreen-close')"
            @click="closeFullscreen"
            type="button"
            :aria-label="fullscreenCloseLabel"
          >
            <slot name="close-icon">✕</slot>
          </button>
          <div :class="ns.e('fullscreen-body')">
            <slot name="fullscreen" :src="currentZoomSrc" :alt="currentAlt">
              <img :src="currentZoomSrc" :alt="currentAlt" :class="ns.e('fullscreen-image')" />
            </slot>
          </div>
          <div v-if="galleryImages.length > 1" :class="ns.e('fullscreen-gallery')">
            <button
              v-for="(img, idx) in galleryImages"
              :key="idx"
              :class="[ns.e('fullscreen-thumb'), ns.is('active', idx === currentIndex)]"
              @click="switchImage(idx)"
              type="button"
            >
              <img :src="img.src" :alt="img.alt || getGalleryItemAlt(idx)" />
            </button>
          </div>
        </div>
      </div>
    </Teleport>
  </div>
</template>

<style>
@charset "UTF-8";
/**
 * YH-UI Sass Variables
 * 原具体的 CSS 变量输出规则 (:root, html.dark 等) 已移至 root.scss，以防止各组件样式编译时产生重复的全局变量 CSS 冗余。
 */
.yh-image-magnifier {
  --yh-magnifier-preview-border: var(--yh-border-color-lighter);
  --yh-magnifier-preview-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
  --yh-magnifier-mask-bg: rgba(0, 0, 0, 0.2);
  --yh-magnifier-mask-border: rgba(255, 255, 255, 0.6);
  --yh-magnifier-radius: var(--yh-radius-base);
  --yh-magnifier-loading-color: var(--yh-color-primary);
  --yh-magnifier-gallery-active-border: var(--yh-color-primary);
  --yh-magnifier-badge-bg: rgba(0, 0, 0, 0.55);
  --yh-magnifier-badge-color: #fff;
  --yh-magnifier-minimap-size: 80px;
  --yh-magnifier-fullscreen-bg: rgba(0, 0, 0, 0.88);
  --yh-magnifier-title-bg: rgba(0, 0, 0, 0.5);
  position: relative;
  width: 100%;
  display: inline-block;
  cursor: crosshair;
}
.yh-image-magnifier__image-wrapper {
  position: relative;
  width: 100%;
  overflow: hidden;
  border-radius: var(--yh-magnifier-radius);
  background-color: var(--yh-fill-color-light);
}
.yh-image-magnifier__image-wrapper.is-bordered {
  border: 1px solid var(--yh-border-color-lighter);
}
.yh-image-magnifier__image-wrapper.is-shadow {
  box-shadow: var(--yh-shadow-light);
}

.yh-image-magnifier__image {
  display: block;
  width: 100%;
  height: auto;
  object-fit: contain;
  pointer-events: none;
}

.yh-image-magnifier__mask {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 10;
  background-color: var(--yh-magnifier-mask-bg);
  border: 1px solid var(--yh-magnifier-mask-border);
  pointer-events: none;
  box-sizing: border-box;
}
.yh-image-magnifier__mask.is-square {
  border-radius: 2px;
}
.yh-image-magnifier__mask.is-circle {
  border-radius: 50%;
}

.yh-image-magnifier__preview {
  position: absolute;
  top: 0;
  z-index: 100;
  overflow: hidden;
  border: 1px solid var(--yh-magnifier-preview-border);
  box-shadow: var(--yh-magnifier-preview-shadow);
  background-color: var(--yh-bg-color-overlay);
  border-radius: var(--yh-magnifier-radius);
}
.yh-image-magnifier__large-image {
  position: absolute;
  top: 0;
  left: 0;
  display: block;
  max-width: none;
  pointer-events: none;
  will-change: transform;
  transition: none;
}

.yh-image-magnifier__loading {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 2px;
  overflow: hidden;
  pointer-events: none;
  z-index: 20;
}

.yh-image-magnifier__loading-bar {
  height: 100%;
  width: 40%;
  background: var(--yh-magnifier-loading-color);
  border-radius: 2px;
  animation: yh-magnifier-loading-slide 1.2s ease-in-out infinite;
}

.yh-image-magnifier__minimap {
  position: absolute;
  bottom: 8px;
  right: 8px;
  width: var(--yh-magnifier-minimap-size);
  height: var(--yh-magnifier-minimap-size);
  border: 1px solid var(--yh-magnifier-preview-border);
  border-radius: 4px;
  overflow: hidden;
  background-color: var(--yh-bg-color-overlay);
  pointer-events: none;
  opacity: 0.85;
}

.yh-image-magnifier__minimap-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
}

.yh-image-magnifier__minimap-indicator {
  position: absolute;
  top: 0;
  left: 0;
  border: 1.5px solid var(--yh-magnifier-gallery-active-border);
  background: rgba(var(--yh-color-primary-rgb, 64, 158, 255), 0.2);
  box-sizing: border-box;
  pointer-events: none;
}

.yh-image-magnifier__scale-badge {
  position: absolute;
  top: 8px;
  left: 8px;
  padding: 2px 8px;
  background: var(--yh-magnifier-badge-bg);
  color: var(--yh-magnifier-badge-color);
  font-size: var(--yh-font-size-xs);
  border-radius: 10px;
  pointer-events: none;
  user-select: none;
  z-index: 50;
  letter-spacing: 0.5px;
  backdrop-filter: blur(4px);
}

.yh-image-magnifier__title {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  padding: 8px 12px;
  background: var(--yh-magnifier-title-bg);
  color: white;
  font-size: var(--yh-font-size-xs);
  text-align: center;
  backdrop-filter: blur(4px);
  pointer-events: none;
}

.yh-image-magnifier__gallery {
  display: flex;
  gap: 8px;
  margin-top: 8px;
  flex-wrap: wrap;
}

.yh-image-magnifier__gallery-item {
  width: 56px;
  height: 56px;
  border: 2px solid transparent;
  border-radius: 4px;
  overflow: hidden;
  cursor: pointer;
  padding: 0;
  background: var(--yh-fill-color-light);
  transition: border-color var(--yh-transition-fast);
  flex-shrink: 0;
}
.yh-image-magnifier__gallery-item img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
}
.yh-image-magnifier__gallery-item:hover {
  border-color: var(--yh-magnifier-gallery-active-border);
  opacity: 0.85;
}
.yh-image-magnifier__gallery-item.is-active {
  border-color: var(--yh-magnifier-gallery-active-border);
}

.yh-image-magnifier--fullscreen-wrapper {
  position: fixed;
  inset: 0;
  z-index: 100000;
  pointer-events: all;
  display: flex;
  align-items: center;
  justify-content: center;
}

.yh-image-magnifier__fullscreen-overlay {
  width: 100%;
  height: 100%;
  background: var(--yh-magnifier-fullscreen-bg);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  animation: yh-fade-in 0.2s ease;
}

.yh-image-magnifier__fullscreen-close {
  position: absolute;
  top: 20px;
  right: 24px;
  width: 36px;
  height: 36px;
  border: none;
  background: rgba(255, 255, 255, 0.15);
  color: #fff;
  font-size: 18px;
  border-radius: 50%;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background var(--yh-transition-fast);
}
.yh-image-magnifier__fullscreen-close:hover {
  background: rgba(255, 255, 255, 0.3);
}

.yh-image-magnifier__fullscreen-body {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 1;
  max-width: 90vw;
  max-height: 80vh;
  overflow: hidden;
}

.yh-image-magnifier__fullscreen-image {
  max-width: 100%;
  max-height: 80vh;
  object-fit: contain;
  border-radius: 4px;
  user-select: none;
  pointer-events: none;
}

.yh-image-magnifier__fullscreen-gallery {
  display: flex;
  gap: 8px;
  padding: 16px;
  overflow-x: auto;
  max-width: 90vw;
}

.yh-image-magnifier__fullscreen-thumb {
  flex-shrink: 0;
  width: 60px;
  height: 60px;
  padding: 0;
  border: 2px solid transparent;
  border-radius: 4px;
  overflow: hidden;
  cursor: pointer;
  background: rgba(255, 255, 255, 0.1);
  transition: border-color var(--yh-transition-fast);
}
.yh-image-magnifier__fullscreen-thumb img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.yh-image-magnifier__fullscreen-thumb.is-active {
  border-color: var(--yh-magnifier-gallery-active-border);
}
.yh-image-magnifier__fullscreen-thumb:hover {
  border-color: rgba(255, 255, 255, 0.5);
}

@keyframes yh-magnifier-loading-slide {
  0% {
    transform: translateX(-100%);
  }
  100% {
    transform: translateX(350%);
  }
}
@keyframes yh-fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}
</style>
