/**
 * Comment anchor helpers — convert between Yjs **relative positions** and
 * the Base64 wire format the comments REST API speaks.
 *
 * Why relative positions: storing absolute offsets (e.g. char 42–58) is
 * fatal under concurrent editing because someone inserting text before
 * char 42 silently shifts the meaning of every saved anchor. Yjs ships
 * `createRelativePositionFromTypeIndex` /
 * `createAbsolutePositionFromRelativePosition` for exactly this case —
 * they identify a slot in the CRDT graph that survives concurrent edits.
 *
 * The wire shape is `{ from: string; to: string }` where each end is the
 * Base64 of `Y.encodeRelativePosition(rel)`. The slice 4 server treats
 * the field as opaque; this module is the only place in the workspace
 * that interprets it.
 *
 * ## Choosing the right Yjs type to anchor against
 *
 * `createRelativePositionFromTypeIndex(type, index)` interprets `index`
 * differently depending on the type:
 *
 * - `Y.XmlText` / `Y.Text` — index is the character offset within the
 *   text. **This is what comment anchors use** because character offsets
 *   are the natural unit for a text selection.
 * - `Y.XmlFragment` / `Y.XmlElement` — index counts *children*. Useful
 *   for "before/after this child" anchors but not what we want here.
 *
 * Slice 5's TipTap binding maps a PM selection to the underlying
 * `Y.XmlText` instance + character offset before calling `encodeAnchor`,
 * and inverts that mapping after `decodeAnchor` returns. The y-prosemirror
 * plugin keeps a `mapping` for this conversion; consumers that aren't
 * using y-prosemirror can talk to the Y types directly.
 */
import * as Y from 'yjs';
/**
 * Build a comment anchor from an editor selection. Returns `null` when
 * the range is empty or inverted — the caller should not save anchors
 * that would resolve to nothing.
 */
export function encodeAnchor(input) {
    if (input.from >= input.to)
        return null;
    if (input.from < 0)
        return null;
    // `createRelativePositionFromTypeIndex` clamps `index` to `[0, type.length]`
    // internally, but we surface an explicit failure when the *intent* is
    // out of bounds so the caller can flag a stale selection.
    if (input.to > input.yType.length)
        return null;
    const relFrom = Y.createRelativePositionFromTypeIndex(input.yType, input.from);
    const relTo = Y.createRelativePositionFromTypeIndex(input.yType, input.to);
    return {
        from: encodeBase64(Y.encodeRelativePosition(relFrom)),
        to: encodeBase64(Y.encodeRelativePosition(relTo)),
    };
}
/**
 * Resolve a stored anchor back to live editor positions. Returns `null`
 * when:
 *
 * - the anchor bytes are not valid base64 / not a Yjs relative position,
 * - the relative position cannot be mapped (the anchored content was
 *   deleted entirely), or
 * - the two ends collapse to the same offset (text between them was
 *   deleted, effectively detaching the comment).
 *
 * Slice 5 surfaces detached comments in the side panel with a small
 * "anchor lost" badge so editors can still resolve / discuss them.
 */
export function decodeAnchor(input) {
    let relFromBytes;
    let relToBytes;
    try {
        relFromBytes = decodeBase64(input.anchor.from);
        relToBytes = decodeBase64(input.anchor.to);
    }
    catch {
        return null;
    }
    let relFrom;
    let relTo;
    try {
        relFrom = Y.decodeRelativePosition(relFromBytes);
        relTo = Y.decodeRelativePosition(relToBytes);
    }
    catch {
        return null;
    }
    const absFrom = Y.createAbsolutePositionFromRelativePosition(relFrom, input.doc);
    const absTo = Y.createAbsolutePositionFromRelativePosition(relTo, input.doc);
    if (absFrom === null || absTo === null)
        return null;
    // Both ends must land on the same text node — otherwise the user's
    // original selection has been split across nodes and we cannot paint
    // a clean highlight without further work in slice 5.
    if (absFrom.type !== absTo.type)
        return null;
    if (absFrom.index >= absTo.index)
        return null;
    return {
        yType: absFrom.type,
        from: absFrom.index,
        to: absTo.index,
    };
}
/**
 * Base64 encode without depending on Node's `Buffer` (the admin bundle
 * is browser-first). We hand-roll the encode because both `btoa` and
 * `Buffer` have ergonomic mis-fits for `Uint8Array` payloads.
 */
function encodeBase64(bytes) {
    if (typeof btoa === 'function') {
        let binary = '';
        for (let i = 0; i < bytes.byteLength; i++) {
            binary += String.fromCharCode(bytes[i]);
        }
        return btoa(binary);
    }
    const BufferCtor = globalThis.Buffer;
    if (BufferCtor)
        return BufferCtor.from(bytes).toString('base64');
    throw new Error('No base64 encoder available');
}
function decodeBase64(s) {
    if (typeof atob === 'function') {
        const binary = atob(s);
        const out = new Uint8Array(binary.length);
        for (let i = 0; i < binary.length; i++) {
            out[i] = binary.charCodeAt(i);
        }
        return out;
    }
    const BufferCtor = globalThis
        .Buffer;
    if (BufferCtor)
        return new Uint8Array(BufferCtor.from(s, 'base64'));
    throw new Error('No base64 decoder available');
}
//# sourceMappingURL=comment-anchor.js.map