<script setup>
import { ref, computed, watch, onMounted, onBeforeUnmount, shallowRef, nextTick } from "vue";
import { useNamespace } from "@yh-ui/hooks";
import { useComponentTheme } from "@yh-ui/theme";
import { affixProps, affixEmits } from "./affix";
defineOptions({
  name: "YhAffix"
});
const props = defineProps(affixProps);
const emit = defineEmits(affixEmits);
const ns = useNamespace("affix");
const { themeStyle } = useComponentTheme(
  "affix",
  computed(() => props.themeOverrides)
);
const root = shallowRef();
const content = shallowRef();
const target = shallowRef();
const fixed = ref(false);
const _scrollTop = ref(0);
const transform = ref(0);
const isIntersecting = ref(false);
const rootRect = ref({
  top: 0,
  bottom: 0,
  left: 0,
  width: 0,
  height: 0
});
const updateRect = () => {
  if (!root.value) return;
  const rect = root.value.getBoundingClientRect();
  rootRect.value = {
    top: rect.top,
    bottom: rect.bottom,
    left: rect.left,
    width: rect.width,
    height: rect.height
  };
};
const update = () => {
  if (!root.value || props.disabled) return;
  if (!isIntersecting.value && !fixed.value) return;
  updateRect();
  const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
  let shouldFix = false;
  if (props.position === "top") {
    if (props.target) {
      const targetRect = target.value.getBoundingClientRect();
      shouldFix = props.offset > rootRect.value.top && targetRect.bottom > 0;
      const diff = targetRect.bottom - props.offset - rootRect.value.height;
      transform.value = diff < 0 ? diff : 0;
    } else {
      shouldFix = props.offset > rootRect.value.top;
    }
  } else {
    if (props.target) {
      const targetRect = target.value.getBoundingClientRect();
      shouldFix = viewportHeight - props.offset < rootRect.value.bottom && targetRect.top < viewportHeight;
      const diff = viewportHeight - targetRect.top - props.offset - rootRect.value.height;
      transform.value = diff < 0 ? diff : 0;
    } else {
      shouldFix = viewportHeight - props.offset < rootRect.value.bottom;
    }
  }
  if (shouldFix !== fixed.value) {
    fixed.value = shouldFix;
    emit("change", shouldFix);
  }
  const currentScrollTop = window.scrollY || document.documentElement.scrollTop;
  _scrollTop.value = currentScrollTop;
  emit("scroll", { scrollTop: currentScrollTop, fixed: fixed.value });
};
const handleScroll = () => {
  update();
};
let scrollContainer = null;
const removeScrollListener = () => {
  if (scrollContainer) {
    scrollContainer.removeEventListener("scroll", handleScroll);
    scrollContainer = null;
  }
};
const updateTarget = () => {
  if (ro && target.value) {
    ro.unobserve(target.value);
  }
  removeScrollListener();
  if (props.target) {
    const el = document.querySelector(props.target);
    target.value = el || void 0;
    if (el) {
      scrollContainer = el;
      el.addEventListener("scroll", handleScroll, { passive: true });
      if (ro) {
        ro.observe(el);
      }
    } else {
      console.warn(`Target not found: ${props.target}`);
    }
  } else {
    target.value = void 0;
    scrollContainer = window;
    window.addEventListener("scroll", handleScroll, { passive: true });
  }
};
let io = null;
let ro = null;
const initObservers = () => {
  io = new IntersectionObserver(
    (entries) => {
      isIntersecting.value = entries[0].isIntersecting;
      if (isIntersecting.value) update();
    },
    {
      rootMargin: "500px 0px 500px 0px"
    }
  );
  if (root.value) io.observe(root.value);
  ro = new ResizeObserver(() => update());
  if (root.value) ro.observe(root.value);
  if (content.value) ro.observe(content.value);
  if (target.value) ro.observe(target.value);
};
onMounted(() => {
  updateTarget();
  window.addEventListener("resize", handleScroll);
  initObservers();
  update();
});
onBeforeUnmount(() => {
  removeScrollListener();
  window.removeEventListener("resize", handleScroll);
  io?.disconnect();
  ro?.disconnect();
});
watch(
  () => [props.offset, props.position, props.disabled],
  () => {
    nextTick(update);
  },
  { deep: true }
);
watch(
  () => props.target,
  () => {
    updateTarget();
    nextTick(update);
  }
);
const placeholderStyle = computed(() => {
  return {
    ...themeStyle.value,
    height: fixed.value ? `${rootRect.value.height}px` : "",
    width: fixed.value ? `${rootRect.value.width}px` : ""
  };
});
const affixStyle = computed(() => {
  if (!fixed.value) return {};
  const offset = `${props.offset}px`;
  const style = {
    position: "fixed",
    zIndex: props.zIndex,
    width: `${rootRect.value.width}px`,
    left: `${rootRect.value.left}px`,
    // 通过 transform translateY 结合 transform.value 实现容器边缘推开动画
    transform: `translateY(${transform.value}px)`,
    transition: "all 0.15s cubic-bezier(0.4, 0, 0.2, 1)",
    ...props.affixStyle
  };
  if (props.position === "top") {
    style.top = offset;
  } else {
    style.bottom = offset;
  }
  return style;
});
defineExpose({
  update,
  fixed,
  scrollTop: _scrollTop
});
</script>

<template>
  <div ref="root" :class="ns.b()" :style="placeholderStyle">
    <!-- 使用 Teleport 解决父级 transform 导致的定位降级（超越标准实现） -->
    <Teleport :to="props.appendTo" :disabled="!props.teleported || !fixed">
      <div ref="content" :class="[ns.e('inner'), { [ns.is('fixed')]: fixed }]" :style="affixStyle">
        <slot :fixed="fixed" />
      </div>
    </Teleport>
  </div>
</template>

<style>
@charset "UTF-8";
/**
 * YH-UI Sass Variables
 * 原具体的 CSS 变量输出规则 (:root, html.dark 等) 已移至 root.scss，以防止各组件样式编译时产生重复的全局变量 CSS 冗余。
 */
.yh-affix {
  position: relative;
  --yh-affix-z-index: var(--yh-z-index-fixed);
  --yh-affix-shadow: var(--yh-shadow-md);
  --yh-affix-transition-duration: var(--yh-duration-base);
  --yh-affix-transition-timing: cubic-bezier(0.4, 0, 0.2, 1);
}
.yh-affix__inner {
  transition: all var(--yh-affix-transition-duration) var(--yh-affix-transition-timing);
}
.yh-affix__inner.is-fixed {
  box-shadow: var(--yh-affix-shadow);
  z-index: var(--yh-affix-z-index);
}

:root.dark .yh-affix,
html.dark .yh-affix,
body.dark .yh-affix {
  --yh-affix-shadow: var(--yh-shadow-lg);
}
</style>
