import { Mark_Type } from "./../../ricos-types/index.js";
import { ReplaceStep } from "./../../tiptap/pm/transform.js";
import { DEFAULT_STYLE } from "./defaults.js";
import {
  canMergeAdjacentByStructure,
  getAnnotationSignature,
  isCrossParagraphMergeStyle
} from "./merge-rules.js";
const PREVIEW_WIDGET_KEY = "rn-preview";
const getTextblockDepth = ($pos) => {
  for (let depth = $pos.depth; depth > 0; depth--) {
    if ($pos.node(depth).isTextblock) {
      return depth;
    }
  }
  return 0;
};
const computeLca = (doc, ranges) => {
  let minFrom = Number.POSITIVE_INFINITY;
  let maxTo = Number.NEGATIVE_INFINITY;
  for (const range of ranges) {
    if (range.from < minFrom) {
      minFrom = range.from;
    }
    if (range.to > maxTo) {
      maxTo = range.to;
    }
  }
  const safeFrom = Math.max(0, Math.min(minFrom, doc.content.size));
  const safeTo = Math.max(0, Math.min(maxTo, doc.content.size));
  const $from = doc.resolve(safeFrom);
  const depth = getTextblockDepth($from);
  const start = depth > 0 ? $from.start(depth) : 0;
  const $to = doc.resolve(safeTo);
  const toDepth = getTextblockDepth($to);
  const wrapTo = toDepth > 0 ? $to.end(toDepth) : doc.content.size;
  return { start, wrapTo };
};
const enrichWithLca = (doc, group) => {
  const lca = computeLca(doc, group.ranges);
  return {
    ...group,
    lcaStart: lca.start,
    wrapTo: lca.wrapTo
  };
};
const isOverlayPreviewStyle = (style) => style !== "NONE" && style !== "CLASSIC";
const isTrackedAnnotationGroupContainedInPreview = (trackedGroup, previewOverride) => trackedGroup.ranges.every(
  (range) => range.from >= previewOverride.from && range.to <= previewOverride.to
);
const splitPreviewByTextblocks = (doc, from, to) => {
  const ranges = [];
  doc.nodesBetween(from, to, (node, pos) => {
    if (!node.isTextblock) {
      return true;
    }
    const blockStart = pos + 1;
    const blockEnd = pos + node.nodeSize - 1;
    const rangeStart = Math.max(from, blockStart);
    const rangeEnd = Math.min(to, blockEnd);
    if (rangeStart < rangeEnd) {
      ranges.push({ from: rangeStart, to: rangeEnd });
    }
    return false;
  });
  return ranges;
};
const buildSyntheticPreviewTrackedGroups = (state, override) => {
  const markType = state.schema.marks[Mark_Type.SKETCH];
  if (!markType) {
    return [];
  }
  const subRanges = splitPreviewByTextblocks(state.doc, override.from, override.to);
  if (subRanges.length === 0) {
    return [];
  }
  const mark = markType.create({
    style: override.style,
    color: override.color,
    animate: override.animate
  });
  const annotationRanges = subRanges.map(({ from, to }) => ({ from, to, mark }));
  const groups = groupAdjacentAnnotationRanges(state.doc, annotationRanges);
  return groups.map(
    (group, index) => enrichWithLca(state.doc, {
      ...group,
      id: groups.length === 1 ? PREVIEW_WIDGET_KEY : `${PREVIEW_WIDGET_KEY}-${index}`
    })
  );
};
const hasOnlyAllowedInlineGap = (doc, from, to, style) => {
  if (from >= to) {
    return true;
  }
  const allowHardBreak = isCrossParagraphMergeStyle(style);
  let hasContent = false;
  doc.nodesBetween(from, to, (node) => {
    if (hasContent) {
      return false;
    }
    if (node.isText) {
      hasContent = !!node.text;
      return false;
    }
    if (node.isLeaf) {
      if (allowHardBreak && node.type.name === "hardBreak") {
        return false;
      }
      hasContent = true;
      return false;
    }
    return true;
  });
  return !hasContent;
};
const isSameInlineParent = (doc, previousRange, nextFrom) => {
  const previousParent = doc.resolve(previousRange.from).parent;
  const nextParent = doc.resolve(nextFrom).parent;
  return previousParent === nextParent && previousParent.inlineContent;
};
const canExtendAnnotationRange = (doc, range, nextFrom, nextMark) => nextMark.eq(range.mark) && isSameInlineParent(doc, range, nextFrom) && hasOnlyAllowedInlineGap(doc, range.to, nextFrom, range.mark.attrs?.style || DEFAULT_STYLE);
const transactionInsertsSketchMark = (tr, markType) => {
  for (const step of tr.steps) {
    if (!(step instanceof ReplaceStep) || step.slice.size === 0) {
      continue;
    }
    let found = false;
    step.slice.content.descendants((node) => {
      if (found) {
        return false;
      }
      if (node.marks.some((mark) => mark.type === markType)) {
        found = true;
        return false;
      }
      return true;
    });
    if (found) {
      return true;
    }
  }
  return false;
};
const transactionTouchesTrackedGroup = (tr, trackedGroups) => {
  if (trackedGroups.length === 0) {
    return false;
  }
  let overallFrom = Number.POSITIVE_INFINITY;
  let overallTo = Number.NEGATIVE_INFINITY;
  for (const tracked of trackedGroups) {
    const groupFrom = tracked.ranges[0].from;
    const groupTo = tracked.ranges[tracked.ranges.length - 1].to;
    if (groupFrom < overallFrom) {
      overallFrom = groupFrom;
    }
    if (groupTo > overallTo) {
      overallTo = groupTo;
    }
  }
  for (let stepIndex = 0; stepIndex < tr.steps.length; stepIndex++) {
    const step = tr.steps[stepIndex];
    if (!(step instanceof ReplaceStep)) {
      continue;
    }
    const inverse = tr.mapping.slice(0, stepIndex).invert();
    const stepFromInPriorDoc = inverse.map(step.from);
    const stepToInPriorDoc = inverse.map(step.to);
    if (stepFromInPriorDoc <= overallTo && stepToInPriorDoc >= overallFrom) {
      return true;
    }
  }
  return false;
};
const findAnnotationRanges = (state) => {
  const markType = state.schema.marks[Mark_Type.SKETCH];
  if (!markType) {
    return [];
  }
  const ranges = [];
  const activeRef = { current: null };
  state.doc.descendants((node, pos) => {
    if (!node.isText) {
      return true;
    }
    const mark = node.marks.find((m) => m.type === markType);
    if (!mark) {
      if (activeRef.current) {
        ranges.push(activeRef.current);
        activeRef.current = null;
      }
      return true;
    }
    const nodeFrom = pos;
    const nodeTo = pos + node.nodeSize;
    const active = activeRef.current;
    if (active && canExtendAnnotationRange(state.doc, active, nodeFrom, mark)) {
      active.to = nodeTo;
      return true;
    }
    if (active) {
      ranges.push(active);
    }
    activeRef.current = { from: nodeFrom, to: nodeTo, mark };
    return true;
  });
  if (activeRef.current) {
    ranges.push(activeRef.current);
  }
  return ranges;
};
const getParagraphInfo = (doc, pos) => {
  const $pos = doc.resolve(pos);
  for (let depth = $pos.depth; depth > 0; depth--) {
    if ($pos.node(depth).type.name.toLowerCase() === "paragraph") {
      return {
        depth,
        start: $pos.before(depth),
        parent: $pos.node(depth - 1)
      };
    }
  }
  return null;
};
const canMergeAnnotationRanges = (doc, previousRange, range, style) => {
  const previousInfo = getParagraphInfo(doc, previousRange.from);
  const info = getParagraphInfo(doc, range.from);
  if (!previousInfo || !info) {
    return false;
  }
  if (!hasOnlyAllowedInlineGap(doc, previousRange.to, range.from, style)) {
    return false;
  }
  return canMergeAdjacentByStructure(style, {
    sameBlock: previousInfo.depth === info.depth && previousInfo.start === info.start,
    // Identity-compare naturally rejects different list items / table cells.
    sameStructuralContainer: previousInfo.parent === info.parent
  });
};
const groupAdjacentAnnotationRanges = (doc, ranges) => {
  const groups = [];
  for (const range of ranges) {
    const sig = getAnnotationSignature(range.mark.attrs ?? {});
    const style = range.mark.attrs?.style || DEFAULT_STYLE;
    const previousGroup = groups[groups.length - 1];
    const previousRange = previousGroup?.ranges[previousGroup.ranges.length - 1];
    if (previousGroup && previousRange && previousGroup.sig === sig && canMergeAnnotationRanges(doc, previousRange, range, style)) {
      previousGroup.ranges.push(range);
      continue;
    }
    groups.push({ sig, mark: range.mark, ranges: [range] });
  }
  return groups;
};
const buildAnnotationGroups = (state) => groupAdjacentAnnotationRanges(state.doc, findAnnotationRanges(state));
const buildAnnotationGroupText = (doc, group) => group.ranges.map((range) => doc.textBetween(range.from, range.to, "\n", "\n")).join("\n");
const annotationRangesOverlap = (first, second) => first.from < second.to && first.to > second.from;
const annotationGroupMatchesTrackedGroup = (doc, group, tracked) => buildAnnotationGroupText(doc, group) === buildAnnotationGroupText(doc, tracked) && group.ranges.some(
  (groupRange) => tracked.ranges.some((trackedGroup) => annotationRangesOverlap(groupRange, trackedGroup))
);
const reconcileTrackedAnnotationGroups = (state, previousTrackedGroups, nextRangeId) => {
  const groups = buildAnnotationGroups(state);
  const claimedPreviousIds = /* @__PURE__ */ new Set();
  const trackedGroups = groups.map((group) => {
    const previous = previousTrackedGroups.find(
      (candidate) => !claimedPreviousIds.has(candidate.id) && annotationGroupMatchesTrackedGroup(state.doc, group, candidate)
    );
    if (previous) {
      claimedPreviousIds.add(previous.id);
      return enrichWithLca(state.doc, { ...group, id: previous.id });
    }
    const id = `rn-${nextRangeId}`;
    nextRangeId++;
    return enrichWithLca(state.doc, { ...group, id });
  });
  return { trackedGroups, nextRangeId };
};
const mapTrackedGroupPositions = (tr, trackedGroups) => trackedGroups.map((trackedGroup) => ({
  ...trackedGroup,
  ranges: trackedGroup.ranges.map((range) => ({
    ...range,
    from: tr.mapping.map(range.from),
    to: tr.mapping.map(range.to, -1)
  }))
}));
const applyFastPathReconcile = (doc, mappedTrackedGroups) => {
  const survivingTrackedRanges = [];
  for (const group of mappedTrackedGroups) {
    const filteredRanges = group.ranges.filter((range) => range.from < range.to);
    if (filteredRanges.length === 0) {
      continue;
    }
    survivingTrackedRanges.push(enrichWithLca(doc, { ...group, ranges: filteredRanges }));
  }
  return survivingTrackedRanges;
};
export {
  applyFastPathReconcile,
  buildAnnotationGroupText,
  buildAnnotationGroups,
  buildSyntheticPreviewTrackedGroups,
  findAnnotationRanges,
  isOverlayPreviewStyle,
  isTrackedAnnotationGroupContainedInPreview,
  mapTrackedGroupPositions,
  reconcileTrackedAnnotationGroups,
  transactionInsertsSketchMark,
  transactionTouchesTrackedGroup
};
//# sourceMappingURL=range-tracking.js.map
