import { SKETCH_DATA_HOOK } from "./data-hooks.js";
import { DEFAULT_ANIMATE, DEFAULT_COLOR, DEFAULT_STYLE } from "./defaults.js";
import {
  canMergeAdjacentByStructure,
  getAnnotationSignature,
  isCrossParagraphMergeStyle
} from "./merge-rules.js";
import { getOverlayRects } from "./rect-utils.js";
import {
  findOverlayContainer,
  renderAnnotationOverlay,
  stableSeedFor
} from "./render-sketch.js";
const BLOCKING_CONTENT_SELECTOR = "img,video,iframe,canvas,svg:not(.sketch)";
const scheduledDocuments = /* @__PURE__ */ new WeakSet();
const rootStates = /* @__PURE__ */ new WeakMap();
const overlaysByAnchor = /* @__PURE__ */ new Map();
const getSignature = (target) => getAnnotationSignature({
  style: target.getAttribute("data-style"),
  color: target.getAttribute("data-color"),
  animate: target.getAttribute("data-animate") !== "false"
});
const INLINE_TAG_NAMES = /* @__PURE__ */ new Set([
  "SPAN",
  "A",
  "B",
  "I",
  "U",
  "EM",
  "STRONG",
  "MARK",
  "SMALL",
  "CODE",
  "KBD",
  "SAMP",
  "VAR",
  "SUB",
  "SUP",
  "ABBR",
  "CITE",
  "Q",
  "S",
  "DEL",
  "INS",
  "BR",
  "WBR"
]);
const findBlockRoot = (target) => {
  let current = target.parentElement;
  while (current?.parentElement && INLINE_TAG_NAMES.has(current.tagName)) {
    current = current.parentElement;
  }
  return current ?? target.parentElement ?? target;
};
const isParagraphBlock = (block) => block.tagName === "P";
const findStructuralContainer = (block) => {
  let current = block.parentElement;
  while (current && current.hasAttribute("data-breakout")) {
    current = current.parentElement;
  }
  return current;
};
const getCommonAncestor = (from, to) => {
  const ancestors = /* @__PURE__ */ new Set();
  let current = from;
  while (current) {
    ancestors.add(current);
    current = current.parentNode;
  }
  current = to;
  while (current) {
    if (ancestors.has(current)) {
      return current;
    }
    current = current.parentNode;
  }
  return document.body;
};
const isSoftBreakTextNode = (node) => node.nodeType === globalThis.Node.TEXT_NODE && node.textContent === "\n";
const hasContentBetween = (from, to, style) => {
  const allowHardBreak = isCrossParagraphMergeStyle(style);
  const root = getCommonAncestor(from, to);
  const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT);
  let isBetweenTargets = false;
  while (walker.nextNode()) {
    const current = walker.currentNode;
    if (current === from) {
      isBetweenTargets = true;
      continue;
    }
    if (current === to) {
      return false;
    }
    if (!isBetweenTargets || from.contains(current) || to.contains(current)) {
      continue;
    }
    if (current.nodeType === globalThis.Node.TEXT_NODE && current.textContent) {
      if (allowHardBreak && isSoftBreakTextNode(current)) {
        continue;
      }
      return true;
    }
    if (current instanceof HTMLElement && current.matches(BLOCKING_CONTENT_SELECTOR)) {
      return true;
    }
  }
  return false;
};
const canMergeSpans = (from, to, style) => {
  if (hasContentBetween(from, to, style)) {
    return false;
  }
  const fromBlock = findBlockRoot(from);
  const toBlock = findBlockRoot(to);
  if (fromBlock === toBlock) {
    return true;
  }
  if (!isParagraphBlock(fromBlock) || !isParagraphBlock(toBlock)) {
    return false;
  }
  const fromContainer = findStructuralContainer(fromBlock);
  const toContainer = findStructuralContainer(toBlock);
  return canMergeAdjacentByStructure(style, {
    sameBlock: false,
    // Object identity: two `<li>`s under the same `<ul>` are distinct
    // element instances, so cross list-item merging is naturally rejected.
    sameStructuralContainer: !!fromContainer && fromContainer === toContainer
  });
};
const findObservationRoot = (target) => {
  const blockRoot = findBlockRoot(target);
  const structural = findStructuralContainer(blockRoot);
  return structural ?? blockRoot ?? target.ownerDocument?.body;
};
const isInsideEditableRegion = (el) => {
  const nearest = el.closest("[contenteditable]");
  if (!nearest) {
    return false;
  }
  const value = nearest.getAttribute("contenteditable");
  return value === "true" || value === "";
};
const getRoughSpans = (root) => Array.from(root.querySelectorAll(`span[data-hook="${SKETCH_DATA_HOOK}"]`)).filter(
  (span) => !isInsideEditableRegion(span)
);
const buildAnnotationGroups = (spans) => {
  const groups = [];
  for (const span of spans) {
    const previousGroup = groups[groups.length - 1];
    const previousSpan = previousGroup?.[previousGroup.length - 1];
    const style = span.getAttribute("data-style") || DEFAULT_STYLE;
    if (previousGroup && previousSpan && getSignature(previousSpan) === getSignature(span) && canMergeSpans(previousSpan, span, style)) {
      previousGroup.push(span);
      continue;
    }
    groups.push([span]);
  }
  return groups;
};
const getAnimate = (target) => target.getAttribute("data-animate") !== "false";
let spanIdCounter = 0;
const spanIds = /* @__PURE__ */ new WeakMap();
const getSpanId = (span) => {
  const existing = spanIds.get(span);
  if (existing !== void 0) {
    return existing;
  }
  const id = String(++spanIdCounter);
  spanIds.set(span, id);
  return id;
};
const computeAnnotationGroupKey = (group) => {
  const target = group[0];
  const style = target.getAttribute("data-style") || DEFAULT_STYLE;
  const color = target.getAttribute("data-color") || DEFAULT_COLOR;
  const animate = target.getAttribute("data-animate") || String(DEFAULT_ANIMATE);
  return `${style}|${color}|${animate}|${group.map(getSpanId).join(",")}`;
};
const teardownOverlay = (entry) => {
  entry.pendingObserver?.disconnect();
  entry.pendingObserver = null;
  if (entry.handle) {
    const rootState = rootStates.get(entry.observationRoot);
    if (rootState) {
      rootState.mountedOverlays = rootState.mountedOverlays.filter((g) => g.handle !== entry.handle);
    }
    entry.handle.remove();
    entry.handle = null;
  }
};
const buildAnnotationGroupSeedText = (group) => group.map((span) => span.textContent ?? "").join("\n");
const createOverlay = (group, onRendered) => {
  const target = group[0];
  if (!target || !target.isConnected) {
    return null;
  }
  const style = target.getAttribute("data-style") || DEFAULT_STYLE;
  const color = target.getAttribute("data-color") || DEFAULT_COLOR;
  const animate = getAnimate(target);
  const seedText = buildAnnotationGroupSeedText(group);
  const seed = stableSeedFor(seedText);
  const observationRoot = findObservationRoot(target);
  const entry = {
    key: computeAnnotationGroupKey(group),
    group,
    style,
    color,
    seed,
    handle: null,
    observationRoot,
    pendingObserver: null
  };
  const render = (shouldAnimate) => {
    if (!target.isConnected) {
      return;
    }
    const handle = renderAnnotationOverlay(target, {
      style,
      color,
      animate: shouldAnimate,
      seed,
      getRects: () => getAnnotationGroupRects(group, style),
      onRendered
    });
    if (handle) {
      entry.handle = handle;
      registerMountedOverlay(observationRoot, {
        group,
        style,
        color,
        seed,
        handle,
        animate,
        // Seed from the rects we just drew so the next resize doesn't
        // misread this as a hidden→visible transition and replay.
        wasVisible: isAnnotationGroupVisible(getAnnotationGroupRects(group, style))
      });
    }
  };
  if (animate && typeof IntersectionObserver !== "undefined") {
    entry.pendingObserver = new IntersectionObserver(
      (entries) => {
        if (entries.some((e) => e.isIntersecting)) {
          entry.pendingObserver?.disconnect();
          entry.pendingObserver = null;
          render(true);
        }
      },
      { threshold: 0.1 }
    );
    entry.pendingObserver.observe(target);
  } else {
    render(false);
  }
  return entry;
};
const promoteMergedOverlayAnchors = (group) => {
  for (let i = 1; i < group.length; i++) {
    const promoted = overlaysByAnchor.get(group[i]);
    if (promoted) {
      teardownOverlay(promoted);
      overlaysByAnchor.delete(group[i]);
    }
  }
};
const collectAnnotationGroups = (doc) => {
  const spans = getRoughSpans(doc.body).filter((span) => span.isConnected);
  return buildAnnotationGroups(spans);
};
const createRenderCompletionTracker = (doc) => {
  let pendingRenders = 0;
  let renderScanComplete = false;
  const markGroupRendered = () => {
    pendingRenders -= 1;
    if (renderScanComplete && pendingRenders === 0) {
      markSketchesReady(doc);
    }
  };
  return {
    markGroupRendered,
    trackPendingRender: () => {
      pendingRenders += 1;
    },
    completeRenderScan: () => {
      renderScanComplete = true;
      if (pendingRenders === 0) {
        markSketchesReady(doc);
      }
    }
  };
};
const syncOverlayForGroup = (group, trackPendingRender, markGroupRendered) => {
  const anchor = group[0];
  if (!anchor) {
    return null;
  }
  promoteMergedOverlayAnchors(group);
  const key = computeAnnotationGroupKey(group);
  const existing = overlaysByAnchor.get(anchor);
  if (existing && existing.key === key) {
    return anchor;
  }
  if (existing) {
    teardownOverlay(existing);
    overlaysByAnchor.delete(anchor);
  }
  trackPendingRender();
  const entry = createOverlay(group, markGroupRendered);
  if (entry) {
    overlaysByAnchor.set(anchor, entry);
  } else {
    markGroupRendered();
  }
  return anchor;
};
const dropStaleOverlays = (liveAnchors) => {
  for (const [anchor, entry] of overlaysByAnchor) {
    if (!liveAnchors.has(anchor) || !anchor.isConnected) {
      teardownOverlay(entry);
      overlaysByAnchor.delete(anchor);
    }
  }
};
const reconcileOverlays = (doc) => {
  const renderCompletion = createRenderCompletionTracker(doc);
  const liveAnchors = /* @__PURE__ */ new Set();
  for (const group of collectAnnotationGroups(doc)) {
    const anchor = syncOverlayForGroup(
      group,
      renderCompletion.trackPendingRender,
      renderCompletion.markGroupRendered
    );
    if (anchor) {
      liveAnchors.add(anchor);
    }
  }
  dropStaleOverlays(liveAnchors);
  renderCompletion.completeRenderScan();
};
const markSketchesPending = (doc) => {
  const view = doc.defaultView;
  if (view) {
    view.ricosSketchesReady = false;
  }
};
const markSketchesReady = (doc) => {
  const view = doc.defaultView;
  if (!view) {
    return;
  }
  view.requestAnimationFrame(() => {
    view.requestAnimationFrame(() => {
      view.ricosSketchesReady = true;
    });
  });
};
const isAnnotationGroupVisible = (rects) => rects.some((r) => r.width > 0 && r.height > 0);
const shouldReplayRevealAnimation = (entry, rects) => entry.animate && isAnnotationGroupVisible(rects) && !entry.wasVisible;
const redrawRootOverlays = (root) => {
  const state = rootStates.get(root);
  if (!state) {
    return;
  }
  state.mountedOverlays = state.mountedOverlays.filter(({ group, handle }) => {
    const stillConnected = group.some((span) => span.isConnected);
    if (!stillConnected) {
      handle.remove();
    }
    return stillConnected;
  });
  for (const entry of state.mountedOverlays) {
    const { group, style, color, seed, handle } = entry;
    const rects = getAnnotationGroupRects(group, style);
    const visible = isAnnotationGroupVisible(rects);
    const shouldAnimate = shouldReplayRevealAnimation(entry, rects);
    entry.wasVisible = visible;
    const anchor = group[0];
    if (!anchor) {
      continue;
    }
    const container = findOverlayContainer(anchor);
    if (!container) {
      continue;
    }
    handle.redraw({
      style,
      color,
      seed,
      animate: shouldAnimate,
      rects,
      container
    });
  }
};
const ensureRootResizeObserver = (root) => {
  const existing = rootStates.get(root);
  if (existing) {
    return existing;
  }
  const state = {
    observer: null,
    mountedOverlays: [],
    resizeFrame: null,
    hasObservedInitialResize: false
  };
  if (typeof ResizeObserver !== "undefined") {
    state.observer = new ResizeObserver(() => {
      if (!state.hasObservedInitialResize) {
        state.hasObservedInitialResize = true;
        return;
      }
      if (state.resizeFrame !== null) {
        return;
      }
      state.resizeFrame = window.requestAnimationFrame(() => {
        state.resizeFrame = null;
        if (!root.isConnected) {
          state.observer?.disconnect();
          rootStates.delete(root);
          return;
        }
        redrawRootOverlays(root);
      });
    });
    state.observer.observe(root);
  }
  rootStates.set(root, state);
  return state;
};
const registerMountedOverlay = (root, overlay) => {
  const state = ensureRootResizeObserver(root);
  state.mountedOverlays = state.mountedOverlays.filter((g) => g.group[0] !== overlay.group[0]);
  state.mountedOverlays.push(overlay);
};
const getAnnotationGroupRects = (group, style) => {
  const rects = [];
  for (const span of group) {
    const list = span.getClientRects();
    for (let i = 0; i < list.length; i++) {
      rects.push(list[i]);
    }
  }
  return getOverlayRects(style, rects);
};
const scheduleSketchRender = (target) => {
  const doc = target.ownerDocument;
  if (!doc) {
    return;
  }
  markSketchesPending(doc);
  if (scheduledDocuments.has(doc)) {
    return;
  }
  scheduledDocuments.add(doc);
  (doc.defaultView ?? window).requestAnimationFrame(() => {
    scheduledDocuments.delete(doc);
    reconcileOverlays(doc);
  });
};
export {
  scheduleSketchRender
};
//# sourceMappingURL=schedule-sketch-render.js.map
