// Port of shadcn's `message-scroller.tsx`'s `MessageScrollerContent`. Wraps
// `@shadcn/react/message-scroller`'s `MessageScroller.Content`, which owns a
// `MutationObserver` (childList) driving `handleContentChange`, plus a
// `ResizeObserver` on the same element driving `handleResize`, and renders a
// hidden tail spacer div as its last child (`data-message-scroller-spacer`).
//
// Ported using the same `<lifecycle>` idiom as viewport.marko for both
// observers; DOM/attrs/classes match the source exactly, including the
// spacer's `aria-hidden` + `hidden` + `data-message-scroller-spacer` markup.
import { cn } from "#lib/utils.ts";
import type { MessageScrollerControllerGetter } from "./lib/controller.ts";
import { content as styles } from "./classes.ts";

export interface Input extends Marko.Input<"div"> {
  /** Controller getter provided by the nearest `<message-scroller-provider>`. */
  controller: MessageScrollerControllerGetter;
  /** Class name for the internal tail spacer used when anchoring turns near the top. */
  spacerClassName?: string;
}

<const/{ controller, class: className, content, spacerClassName, role, "aria-relevant": ariaRelevant, ...rest }=input/>

<div/el
  ...rest
  role=(role ?? "log")
  aria-relevant=(ariaRelevant ?? "additions")
  data-slot="message-scroller-content"
  class=cn(styles.root, className)
>
  <${content}/>
  <div/spacerEl aria-hidden="true" data-message-scroller-spacer hidden class=spacerClassName/>
</div>

<lifecycle
  onMount() {
    const contentEl = el();
    controller().setContentElement(contentEl);
    controller().setSpacerElement(spacerEl());

    // Do NOT call controller().handleContentChange() here unconditionally:
    // Marko mounts a child (this component) before its ancestor
    // (viewport.marko/message-scroller.marko) finishes mounting, so at this
    // point the controller's viewport/root refs are often still null. The
    // initial sync is instead triggered by setContentElement itself once
    // root, viewport AND content have all registered — see
    // lib/controller.ts's tryInitialSync (and notes/style-ports.md's
    // "pending-scroll" section for the bug this replaces).

    let mutationObserver: MutationObserver | undefined;
    if (typeof MutationObserver !== "undefined") {
      mutationObserver = new MutationObserver(() => {
        controller().handleContentChange();
      });
      mutationObserver.observe(contentEl, { childList: true });
    }

    let resizeFrame = 0;
    let resizeObserver: ResizeObserver | undefined;
    if (typeof ResizeObserver !== "undefined") {
      // Coalesce into rAF: handleResize mutates the spacer inside this
      // observed element, and resizing an observed element during delivery
      // fires "ResizeObserver loop completed with undelivered notifications"
      // (same rationale as the source).
      resizeObserver = new ResizeObserver(() => {
        window.cancelAnimationFrame(resizeFrame);
        resizeFrame = window.requestAnimationFrame(() => controller().handleResize());
      });
      resizeObserver.observe(contentEl);
    }

    // Marko merges onMount's RETURNED OBJECT into `this` (returning a bare
    // cleanup function is silently ignored) — onDestroy reads it back.
    return {
      cleanup: () => {
        mutationObserver?.disconnect();
        window.cancelAnimationFrame(resizeFrame);
        resizeObserver?.disconnect();
      },
    };
  }
  onDestroy() {
    this.cleanup?.();
    controller().setContentElement(null);
    controller().setSpacerElement(null);
  }
/>
