// "Near enough the newest edge to count as sticky." 50px is roughly
// two message-bubble margins; a user who is within that distance
// clearly wasn't scrolling back through history, so we treat their
// position as "stuck to newest" and re-pin on each items update.
const NEAR_NEWEST_THRESHOLD_PX = 50;
/**
 * Keeps a scroll container stuck to its "newest" edge as items are
 * appended, so long as the user hasn't scrolled away.
 *
 * We use imperative scrolling rather than `flex-direction: column-reverse`
 * so DOM order matches visual order (copy/paste, screen readers, tab
 * order all align with what the user sees). CSS `overflow-anchor: auto`
 * — the default and left enabled here — solves a different problem:
 * preserving scroll position when content is inserted *above* the
 * viewport. It doesn't stick the scroll to the newest edge when items
 * arrive below the viewport, which is what chat typically needs; that
 * requires the imperative approach in this class.
 *
 * Lifecycle:
 * - `beforeRender()` in `componentWillRender` — snapshot whether the
 *   user is near the newest edge, using the order the scroll position
 *   was last measured against. Capturing here (synchronously, before
 *   the new DOM lands) avoids the race a scroll-event listener would
 *   have with `componentDidRender`.
 * - `afterRender()` in `componentDidRender` — if the snapshot said
 *   "near newest", re-pin to the newest edge (new `scrollHeight`).
 * - `repinIfSticky()` for out-of-band re-pins (e.g. after the typing
 *   indicator's height transition finishes — its `scrollHeight`
 *   change lands after `componentDidRender`).
 *
 * Scroll-container contract: the passed element must itself be the
 * scrolling ancestor (the element whose `scrollTop` / `scrollHeight`
 * / `clientHeight` reflect the chat list's scroll state). In practice
 * that is the component's host, which sets `overflow: hidden auto` on
 * itself. If future refactors introduce an inner scrolling wrapper,
 * pass that wrapper instead.
 */
export class ScrollAnchor {
    constructor(scrollContainer, getOrder, initialOrder) {
        this.scrollContainer = scrollContainer;
        this.getOrder = getOrder;
        this.isSticky = true;
        this.paintedOrder = initialOrder;
    }
    beforeRender() {
        if (!this.scrollContainer.isConnected) {
            return;
        }
        // Measure against the order the scroll position was last painted
        // with. If `order` just changed, the scroll position still refers
        // to the previous layout until this render commits.
        this.isSticky =
            this.distanceFromNewestEdge(this.paintedOrder) <
                NEAR_NEWEST_THRESHOLD_PX;
    }
    afterRender() {
        if (!this.scrollContainer.isConnected) {
            return;
        }
        this.paintedOrder = this.getOrder();
        if (this.isSticky) {
            this.scrollToNewest();
        }
    }
    repinIfSticky() {
        if (!this.scrollContainer.isConnected) {
            return;
        }
        if (this.isSticky) {
            this.scrollToNewest();
        }
    }
    scrollToNewest() {
        const target = this.getOrder() === 'oldest-on-top'
            ? this.scrollContainer.scrollHeight
            : 0;
        // Reading `scrollTop` is a cheap (already-measured) lookup, and
        // short-circuiting avoids forcing a layout when we'd write the
        // same value — important under high-frequency re-renders
        // (streaming AI output).
        if (this.scrollContainer.scrollTop === target) {
            return;
        }
        this.scrollContainer.scrollTop = target;
    }
    distanceFromNewestEdge(order) {
        return order === 'oldest-on-top'
            ? this.scrollContainer.scrollHeight -
                this.scrollContainer.scrollTop -
                this.scrollContainer.clientHeight
            : this.scrollContainer.scrollTop;
    }
}
