import { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from "react";
var SCROLL_TOLERANCE = 20;
/** 视为"已脱离底部"的阈值，区别于 SCROLL_TOLERANCE，用于跟随判定的滞回区 */ var PIN_THRESHOLD = 80;
/** rAF 渐进滚动每帧推进比例（0-1），值越大越快、越接近 1 越接近 instant */ var ANIMATION_LERP = 0.25;
/** rAF 渐进滚动单帧最小步长（px），避免接近底部时进度过慢 */ var ANIMATION_MIN_STEP = 6;
/** 接近底部不足该距离时直接吸底，避免亚像素抖动 */ var ANIMATION_SNAP_DISTANCE = 1.5;
/** wheel 累计上滑距离阈值（px），超过才认为是用户主动上滑（过滤触摸板惯性回弹） */ var WHEEL_UP_INTENT_THRESHOLD = 16;
/** 累计 wheel 重置时间（ms），距离上次 wheel 超过该时间则重置累计值 */ var WHEEL_INTENT_RESET_MS = 120;
export var useAutoScroll = function useAutoScroll() {
    var props = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : {};
    var _ref, _props_scrollTolerance, _props_pinThreshold;
    var containerRef = useRef(null);
    var lastScrollHeight = useRef(0);
    /** 跟随状态：用户未主动离开底部，则一直跟随新内容到底部 */ var isPinned = useRef(true);
    /** 当前是否贴底（用于回调判断变更） */ var lastIsAtBottom = useRef(true);
    /**
   * 标记最近若干次滚动是否由程序触发，用于过滤 scroll 事件中的程序行为。
   * Safari/iOS Safari 在某些场景下 scroll 事件相对 scrollTop 赋值为异步派发，
   * 同步置位/复位会让 handleScroll 把程序滚动当成用户滚动。改为「计数 + RAF 复位」：
   * - 写 scrollTop 前 ++
   * - 下一帧（异步 scroll 事件已派发完）再 --
   * handleScroll 内只要计数 > 0 即视为程序行为。
   */ var programmaticScrollCount = useRef(0);
    /** RAF 句柄，用于程序滚动计数的延迟复位 */ var programmaticResetRaf = useRef(null);
    /** rAF 渐进滚动句柄 */ var rafId = useRef(null);
    /** 渐进滚动的目标位置 */ var animationTarget = useRef(0);
    /** onContentChange 的 RAF 合并句柄，把同一帧内多次回调合并为一次实际处理 */ var contentChangeRaf = useRef(null);
    var mutationObserver = useRef(null);
    var resizeObserver = useRef(null);
    // 优先使用新名 scrollTolerance，回退到 deprecated 的 SCROLL_TOLERANCE，最后才是默认值
    var tolerance = (_ref = (_props_scrollTolerance = props.scrollTolerance) !== null && _props_scrollTolerance !== void 0 ? _props_scrollTolerance : props.SCROLL_TOLERANCE) !== null && _ref !== void 0 ? _ref : SCROLL_TOLERANCE;
    // pinThreshold 必须 >= tolerance，否则会出现"既算贴底又算脱离"的逻辑空洞
    var pinThreshold = Math.max((_props_pinThreshold = props.pinThreshold) !== null && _props_pinThreshold !== void 0 ? _props_pinThreshold : PIN_THRESHOLD, tolerance);
    /**
   * 用 ref 持有最新回调与 scrollBehavior，避免引用变化导致 effect 反复重建监听。
   * 必须在 effect 中赋值，渲染期写 ref 在 React 18 并发渲染 / StrictMode 下不安全
   * （被丢弃的渲染版本会污染 ref）。
   */ var onResizeRef = useRef(props.onResize);
    var onScrollStateChangeRef = useRef(props.onScrollStateChange);
    var scrollBehaviorRef = useRef(props.scrollBehavior);
    useLayoutEffect(function() {
        onResizeRef.current = props.onResize;
        onScrollStateChangeRef.current = props.onScrollStateChange;
        scrollBehaviorRef.current = props.scrollBehavior;
    });
    /**
   * 循环引用 / 不可序列化 deps 的回退签名递增计数器：
   * 不能仅用 `__cyclic_${length}__` 作为回退 key —— 两次不同的循环引用对象只要 length
   * 相同，回退 key 就完全相同，导致重挂载逻辑失效。每次回退时单调递增，确保每次都能触发重挂载。
   */ var cyclicDepsCounter = useRef(0);
    /**
   * 把 deps 序列化成稳定长度的字符串 key，作为下游 effect 的依赖：
   * - 直接展开 `...props.deps` 会让 effect 依赖数组长度可变，触发 React 运行时报错
   * - 用 JSON.stringify 替代 String()+join，避免对象 deps 全部退化为 [object Object]
   *   导致引用变化无法触发重挂载
   * - 序列化失败（循环引用等）时使用单调递增的回退 key，保证每次都能触发重挂载
   */ var depsKey = useMemo(function() {
        if (!props.deps || props.deps.length === 0) return '';
        try {
            return JSON.stringify(props.deps);
        } catch (unused) {
            cyclicDepsCounter.current += 1;
            return "__cyclic_".concat(props.deps.length, "_").concat(cyclicDepsCounter.current, "__");
        }
    }, [
        props.deps
    ]);
    var cancelAnimation = useCallback(function() {
        if (rafId.current !== null) {
            cancelAnimationFrame(rafId.current);
            rafId.current = null;
        }
    }, []);
    /**
   * 把 scrollTop 写入操作包装为「程序滚动」：
   * - 写入前计数 +1，使 handleScroll 识别后立即 return
   * - 在下一个 RAF 中 -1，确保即便 Safari/iOS Safari 异步派发 scroll 事件也能被过滤
   */ var beginProgrammaticScroll = useCallback(function() {
        programmaticScrollCount.current += 1;
        // 上一次复位还没跑就让它跑完，避免 RAF 句柄丢失
        if (programmaticResetRaf.current !== null) {
            cancelAnimationFrame(programmaticResetRaf.current);
        }
        programmaticResetRaf.current = requestAnimationFrame(function() {
            programmaticResetRaf.current = null;
            // 一帧内可能多次写 scrollTop（jumpToBottom + 后续动画首帧），所以这里清零而不是 -1
            programmaticScrollCount.current = 0;
        });
    }, []);
    /**
   * 上一次通知的 pinned 状态，配合 lastIsAtBottom 共同决定是否需要通知。
   */ var lastIsPinned = useRef(true);
    /**
   * 触发跟随状态/贴底状态回调，仅在 isAtBottom 或 isPinned 变化时通知。
   */ var notifyState = useCallback(function() {
        var container = containerRef.current;
        if (!container) return;
        var distance = container.scrollHeight - container.scrollTop - container.clientHeight;
        var atBottom = distance <= tolerance;
        var pinnedNow = isPinned.current;
        if (atBottom !== lastIsAtBottom.current || pinnedNow !== lastIsPinned.current) {
            var _onScrollStateChangeRef_current;
            lastIsAtBottom.current = atBottom;
            lastIsPinned.current = pinnedNow;
            (_onScrollStateChangeRef_current = onScrollStateChangeRef.current) === null || _onScrollStateChangeRef_current === void 0 ? void 0 : _onScrollStateChangeRef_current.call(onScrollStateChangeRef, {
                isAtBottom: atBottom,
                isPinned: pinnedNow
            });
        }
    }, [
        tolerance
    ]);
    /**
   * 立即跳到底部（不走 rAF）。
   * scrollTop 赋值会同步派发 scroll 事件，所以同步设置/清除标志即可
   * 完整过滤掉这次程序滚动，无需异步延迟。
   */ var jumpToBottom = useCallback(function() {
        var container = containerRef.current;
        if (!container) return;
        cancelAnimation();
        beginProgrammaticScroll();
        container.scrollTop = container.scrollHeight;
        notifyState();
    }, [
        cancelAnimation,
        beginProgrammaticScroll,
        notifyState
    ]);
    /**
   * 基于 rAF 的渐进滚动到底部。每帧重新读取 scrollHeight，
   * 新内容追加时目标会自动跟随增长，不会出现"动画结束才发现还没到底"。
   */ var animateToBottom = useCallback(function() {
        var container = containerRef.current;
        if (!container) return;
        animationTarget.current = container.scrollHeight - container.clientHeight;
        if (rafId.current !== null) {
            // 已有动画在跑，目标已被更新，无需新开
            return;
        }
        var step = function step1() {
            rafId.current = null;
            var el = containerRef.current;
            if (!el) return;
            // 实时刷新目标，跟随新内容
            animationTarget.current = el.scrollHeight - el.clientHeight;
            var current = el.scrollTop;
            var remaining = animationTarget.current - current;
            if (remaining <= ANIMATION_SNAP_DISTANCE) {
                beginProgrammaticScroll();
                el.scrollTop = animationTarget.current;
                notifyState();
                return;
            }
            var delta = Math.max(remaining * ANIMATION_LERP, ANIMATION_MIN_STEP);
            beginProgrammaticScroll();
            el.scrollTop = current + delta;
            rafId.current = requestAnimationFrame(step);
        };
        rafId.current = requestAnimationFrame(step);
    }, [
        beginProgrammaticScroll,
        notifyState
    ]);
    /**
   * 内容增长 / 容器尺寸变化时触发的核心检查（实际执行体）。
   * - 未 pinned：只通知状态变化
   * - pinned + 内容增大：按 scrollBehavior 跟随
   * - pinned + 内容缩小：用 instant 立即贴底（避免折叠/输入框收起后底部留白）
   */ var performContentChange = useCallback(function() {
        var _onResizeRef_current;
        var container = containerRef.current;
        if (!container) return;
        (_onResizeRef_current = onResizeRef.current) === null || _onResizeRef_current === void 0 ? void 0 : _onResizeRef_current.call(onResizeRef, {
            width: container.clientWidth,
            height: container.clientHeight
        });
        var currentScrollHeight = container.scrollHeight;
        var prevScrollHeight = lastScrollHeight.current;
        lastScrollHeight.current = currentScrollHeight;
        if (!isPinned.current) {
            notifyState();
            return;
        }
        var grew = currentScrollHeight > prevScrollHeight;
        var shrunk = currentScrollHeight < prevScrollHeight;
        if (grew) {
            var _scrollBehaviorRef_current;
            var behavior = (_scrollBehaviorRef_current = scrollBehaviorRef.current) !== null && _scrollBehaviorRef_current !== void 0 ? _scrollBehaviorRef_current : 'smooth';
            if (behavior === 'auto') {
                jumpToBottom();
            } else {
                animateToBottom();
            }
            return;
        }
        if (shrunk) {
            // 内容缩小时立即贴底，无需平滑动画
            jumpToBottom();
            return;
        }
        notifyState();
    }, [
        animateToBottom,
        jumpToBottom,
        notifyState
    ]);
    /**
   * onContentChange 的对外入口：用 RAF 合并同一帧内多次回调。
   * - ResizeObserver / MutationObserver 在内容剧烈变化时一帧内可能触发数十次
   * - 每次都同步读 scrollHeight/scrollTop/clientHeight 会强制 reflow，叠加形成性能放大
   * - RAF 合并后保证一帧内最多执行一次实际处理，且仍能跟上 60fps 内容变化
   */ var onContentChange = useCallback(function() {
        if (contentChangeRaf.current !== null) return;
        contentChangeRaf.current = requestAnimationFrame(function() {
            contentChangeRaf.current = null;
            performContentChange();
        });
    }, [
        performContentChange
    ]);
    // 监听用户主动交互（wheel / touch / 键盘）以判断是否脱离底部
    useEffect(function() {
        var container = containerRef.current;
        if (!container) return;
        /** 用户操作判定：取消当前动画、根据当前距底距离更新 pinned */ var onUserInteract = function onUserInteract() {
            cancelAnimation();
            var distance = container.scrollHeight - container.scrollTop - container.clientHeight;
            var wasPinned = isPinned.current;
            // 只有距底超过 pinThreshold 才认为脱离；贴底则保持/恢复跟随
            if (distance > pinThreshold) {
                isPinned.current = false;
            } else if (distance <= tolerance) {
                isPinned.current = true;
            }
            if (wasPinned !== isPinned.current) {
                notifyState();
            }
        };
        /**
     * wheel：累计上滑距离超过阈值才认为是真实的"上滑意图"，
     * 用于过滤触摸板惯性回弹带来的瞬时负 deltaY。
     */ var wheelUpAccum = 0;
        var lastWheelTs = 0;
        var handleWheel = function handleWheel(e) {
            var now = e.timeStamp || Date.now();
            // 距上次 wheel 间隔过长则重置累计
            if (now - lastWheelTs > WHEEL_INTENT_RESET_MS) wheelUpAccum = 0;
            lastWheelTs = now;
            if (e.deltaY < 0) {
                wheelUpAccum += -e.deltaY;
            } else if (e.deltaY > 0) {
                // 正向滚动直接清零，避免负噪声累加
                wheelUpAccum = 0;
            }
            if (wheelUpAccum < WHEEL_UP_INTENT_THRESHOLD) return;
            cancelAnimation();
            var distance = container.scrollHeight - container.scrollTop - container.clientHeight;
            if (distance > tolerance) {
                var wasPinned = isPinned.current;
                isPinned.current = false;
                if (wasPinned) notifyState();
            }
        };
        /** touch：开始滑动时取消动画，结束时根据位置重新判定 */ var handleTouchStart = function handleTouchStart() {
            cancelAnimation();
        };
        var handleTouchMove = function handleTouchMove() {
            onUserInteract();
        };
        /** 键盘：上、PgUp、Home、Space(Shift) 视为上滑意图 */ var handleKeyDown = function handleKeyDown(e) {
            var upKeys = [
                'ArrowUp',
                'PageUp',
                'Home'
            ];
            var downKeys = [
                'ArrowDown',
                'PageDown',
                'End',
                ' ',
                'Spacebar'
            ];
            if (upKeys.includes(e.key) || e.shiftKey && e.key === ' ') {
                cancelAnimation();
                var distance = container.scrollHeight - container.scrollTop - container.clientHeight;
                if (distance > tolerance) {
                    var wasPinned = isPinned.current;
                    isPinned.current = false;
                    if (wasPinned) notifyState();
                }
            } else if (downKeys.includes(e.key)) {
                // 用户主动下滑/到底，下一帧再判断是否回到底部并恢复 pinned
                requestAnimationFrame(function() {
                    return onUserInteract();
                });
            }
        };
        /** scroll：仅做"贴底状态"通知，不参与 pinned 判定 */ var handleScroll = function handleScroll() {
            // 计数 > 0 即视为程序滚动；Safari 等浏览器异步派发 scroll 事件时也能被正确过滤
            if (programmaticScrollCount.current > 0) return;
            notifyState();
            // 用户已经手动滚回底部，可恢复跟随
            var distance = container.scrollHeight - container.scrollTop - container.clientHeight;
            if (distance <= tolerance && !isPinned.current) {
                isPinned.current = true;
                notifyState();
            }
        };
        container.addEventListener('wheel', handleWheel, {
            passive: true
        });
        container.addEventListener('touchstart', handleTouchStart, {
            passive: true
        });
        container.addEventListener('touchmove', handleTouchMove, {
            passive: true
        });
        container.addEventListener('keydown', handleKeyDown);
        container.addEventListener('scroll', handleScroll, {
            passive: true
        });
        return function() {
            container.removeEventListener('wheel', handleWheel);
            container.removeEventListener('touchstart', handleTouchStart);
            container.removeEventListener('touchmove', handleTouchMove);
            container.removeEventListener('keydown', handleKeyDown);
            container.removeEventListener('scroll', handleScroll);
        };
    }, [
        cancelAnimation,
        notifyState,
        pinThreshold,
        tolerance,
        depsKey
    ]);
    /**
   * 区分「首次挂载」与「depsKey 变化导致的重挂载」：
   * - 首次挂载：仅初始化基线 lastScrollHeight，**不主动触发 jumpToBottom**，
   *   避免改变下游组件（ChatLayout/ThoughtChainList 等）的初始展示行为
   * - 重挂载：基线被重置后，若 isPinned 为 true 且内容自上次挂载后已增长，
   *   主动 jumpToBottom 一次以避免漏跟随
   */ var hasMountedRef = useRef(false);
    // 内容尺寸变化监听：ResizeObserver + MutationObserver 双保险
    useEffect(function() {
        var container = containerRef.current;
        if (!container) return;
        // 进入 effect 前的旧基线（用于判断重挂载期间内容是否已增长）
        var previousBaseline = lastScrollHeight.current;
        var currentScrollHeight = container.scrollHeight;
        lastScrollHeight.current = currentScrollHeight;
        /**
     * ResizeObserver 仅监听 container 本身 + 直接子元素：
     * - 老实现 querySelectorAll('*') 把所有后代节点（聊天列表常 1k+ 个）一次性塞进同一个
     *   observer，初次挂载即 O(n) 注册、任意子元素 1px 变动都触发回调，形成性能放大风暴
     * - container 自身 box 高度由直接子元素堆叠而成；只 observe 直接子元素就能感知 90% 的内容尺寸变化
     * - 深层后代的尺寸变化（图片懒加载、代码块异步高亮等）由下面的 MutationObserver
     *   `characterData/childList` + onContentChange 统一兜底
     */ var hasResizeObserver = typeof ResizeObserver !== 'undefined';
        if (hasResizeObserver) {
            resizeObserver.current = new ResizeObserver(function() {
                onContentChange();
            });
            resizeObserver.current.observe(container);
            // 仅 observe 直接子元素，避免遍历整棵子树
            Array.from(container.children).forEach(function(el) {
                var _resizeObserver_current;
                (_resizeObserver_current = resizeObserver.current) === null || _resizeObserver_current === void 0 ? void 0 : _resizeObserver_current.observe(el);
            });
        }
        // MutationObserver 监听 DOM 结构变化，仅把新增的直接子节点加入 ResizeObserver
        mutationObserver.current = new MutationObserver(function(mutations) {
            var structureChanged = false;
            mutations.forEach(function(m) {
                var _ref, _ref1;
                var _m_addedNodes, _m_removedNodes;
                if (((_ref = (_m_addedNodes = m.addedNodes) === null || _m_addedNodes === void 0 ? void 0 : _m_addedNodes.length) !== null && _ref !== void 0 ? _ref : 0) > 0) {
                    structureChanged = true;
                    // 仅当新增的是 container 直接子节点时才 observe；深层 add 由 characterData/childList 触发 onContentChange 即可
                    if (m.target === container) {
                        m.addedNodes.forEach(function(node) {
                            var _resizeObserver_current;
                            if (node.nodeType !== 1) return;
                            (_resizeObserver_current = resizeObserver.current) === null || _resizeObserver_current === void 0 ? void 0 : _resizeObserver_current.observe(node);
                        });
                    }
                }
                if (((_ref1 = (_m_removedNodes = m.removedNodes) === null || _m_removedNodes === void 0 ? void 0 : _m_removedNodes.length) !== null && _ref1 !== void 0 ? _ref1 : 0) > 0) {
                    structureChanged = true;
                    if (m.target === container) {
                        m.removedNodes.forEach(function(node) {
                            var _resizeObserver_current;
                            if (node.nodeType !== 1) return;
                            (_resizeObserver_current = resizeObserver.current) === null || _resizeObserver_current === void 0 ? void 0 : _resizeObserver_current.unobserve(node);
                        });
                    }
                }
                if (m.type === 'characterData') {
                    structureChanged = true;
                }
            });
            if (structureChanged) onContentChange();
        });
        mutationObserver.current.observe(container, {
            childList: true,
            subtree: true,
            attributes: false,
            characterData: true
        });
        // 仅在「重挂载且内容自上次挂载已增长」时主动补一次跟随；首次挂载不主动滚动，
        // 避免 isPinned 默认为 true 导致首屏被强制吸到底，破坏下游组件的初始展示
        if (hasMountedRef.current) {
            var grewDuringRemount = currentScrollHeight > previousBaseline;
            if (isPinned.current && grewDuringRemount) {
                jumpToBottom();
            } else {
                // 通知状态变化（脱离 / 贴底切换），但不强制滚动
                notifyState();
            }
        }
        hasMountedRef.current = true;
        return function() {
            var _mutationObserver_current, _resizeObserver_current;
            (_mutationObserver_current = mutationObserver.current) === null || _mutationObserver_current === void 0 ? void 0 : _mutationObserver_current.disconnect();
            (_resizeObserver_current = resizeObserver.current) === null || _resizeObserver_current === void 0 ? void 0 : _resizeObserver_current.disconnect();
            mutationObserver.current = null;
            resizeObserver.current = null;
        };
    }, [
        onContentChange,
        jumpToBottom,
        notifyState,
        depsKey
    ]);
    // 卸载时清理动画 + RAF 合并句柄 + 程序滚动复位句柄
    useEffect(function() {
        return function() {
            cancelAnimation();
            if (contentChangeRaf.current !== null) {
                cancelAnimationFrame(contentChangeRaf.current);
                contentChangeRaf.current = null;
            }
            if (programmaticResetRaf.current !== null) {
                cancelAnimationFrame(programmaticResetRaf.current);
                programmaticResetRaf.current = null;
            }
        };
    }, [
        cancelAnimation
    ]);
    /**
   * 手动滚动到底部。
   * - 默认 'auto' 立即滚动（无动画）
   * - 传 'smooth' 使用 rAF 渐进滚动
   * 调用后会自动恢复 pinned 状态。
   */ var scrollToBottom = useCallback(function() {
        var behavior = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : 'auto';
        isPinned.current = true;
        if (behavior === 'smooth') {
            animateToBottom();
        } else {
            jumpToBottom();
        }
    }, [
        animateToBottom,
        jumpToBottom
    ]);
    /** 当前是否贴底 */ var isAtBottom = useCallback(function() {
        var container = containerRef.current;
        if (!container) return true;
        var distance = container.scrollHeight - container.scrollTop - container.clientHeight;
        return distance <= tolerance;
    }, [
        tolerance
    ]);
    return {
        containerRef: containerRef,
        scrollToBottom: scrollToBottom,
        isAtBottom: isAtBottom
    };
};
export default useAutoScroll;
