UNPKG

lib0

Version:

isomorphic utility functions

4,837 lines • 204 kB
/**
 * @beta this API is about to change
 *
 * ## Consumer API
 *
 * This module exports a large surface, but a consumer only needs a handful of it:
 * - **build / apply / inspect:** {@link create} (the constructor) and the {@link DeltaBuilder} methods
 *   it returns (`insert`/`delete`/`retain`/`modify`/`setAttr`/`addMark`/…, `apply`, `rebase`, `done`);
 *   {@link clone}, {@link slice}, {@link diff}, {@link inverse}, and `toJSON`/`equals`/`isEmpty` on the result.
 *   The content/attr builder methods are also exported standalone as shorthands for a `create()`-headed
 *   chain — `delta.insert(..)` ≡ `delta.create().insert(..)`, see {@link insert}. The `delete`
 *   shorthand is spelled {@link delete_} (`delete` is a reserved word).
 * - **schemas:** {@link $delta} (define a typed delta schema) and {@link $deltaAny} (the catch-all).
 * - **types:** `Delta`, `DeltaBuilder`, `DeltaAny`, `DeltaConf` for annotations.
 *
 * Everything else is `@internal` plumbing: the `*Op` classes and `$*Op` schema sentinels exist for the
 * transformer layer's V8-stable dispatch (do not construct them); the `*RootMark*` helpers,
 * {@link cloneShallow}, {@link random}, the merge helpers, and the remaining `$*` schemas are for
 * cross-module / custom-transformer use, not application code. The other `DeltaConf*` typedefs are the
 * inference machinery behind the fluent builder's typed chain — never named directly.
 *
 * ## Mutability
 *
 * Deltas are mutable by default. But references are often shared, by marking a Delta as "done". You
 * may only modify deltas by applying other deltas to them. Casting a Delta to a DeltaBuilder
 * manually, will likely modify "shared" state.
 */

import * as list from '../list.js'
import * as object from '../object.js'
import * as equalityTrait from '../trait/equality.js'
import * as fingerprintTrait from '../trait/fingerprint.js'
import * as arr from '../array.js'
import * as fun from '../function.js'
import * as s from '../schema.js'
import * as error from '../error.js'
import * as math from '../math.js'
import * as rabin from '../hash/rabin.js'
import * as encoding from '../encoding.js'
import * as buffer from '../buffer.js'
import * as patience from '../diff/patience.js'
import * as prng from '../prng.js'
import * as rand from '../random.js'

/**
 * Provenance metadata on a content/attr op: *who/what* inserted, deleted, or formatted it. The canonical
 * shape below is a convention — apply/diff/equality treat attribution as an **opaque** object (never branching
 * on these field names), so custom keys flow through — with ONE exception: the nested `format` key merges per
 * inner key (one level), so applying `{format:{italic:[…]}}` *adds* italic while keeping a pre-existing
 * `{format:{bold:[…]}}`, and `{format:{bold:null}}` removes just that inner key (an emptied `format` is
 * dropped). Every other key is flat/wholesale. The `format`/`formatAt` part exists only on
 * children (content ops), never on node attribute ops. As an *update value* on `retain`/`modify`/`*Attr`,
 * attribution uses the same unified tri-state as {@link Formats} (`undefined` skip / `null`
 * clear / `{k:v}` set / `{k:null}` remove) — see the delta `readme.md` "Formats & Attributions". The blanket
 * `null`-clear channel caveat on {@link Formats} applies here too; note `rebase` does not
 * reconcile attribution at all (concurrent attribution edits don't converge), so prefer deterministic
 * attribution assignment outside of concurrent editing.
 *
 * @typedef {{
 *   insert?: string[]
 *   insertAt?: number
 *   delete?: string[]
 *   deleteAt?: number
 *   format?: Record<string,string[]>
 *   formatAt?: number
 * }} Attribution
 */

/**
 * @type {s.Schema<Attribution>}
 */
export const $attribution = /* @__PURE__ */(() => s.$object({
  insert: s.$array(s.$string).optional,
  insertAt: s.$number.optional,
  delete: s.$array(s.$string).optional,
  deleteAt: s.$number.optional,
  format: s.$record(s.$string, s.$array(s.$string)).optional,
  formatAt: s.$number.optional
}))()

/**
 * Rich-text formatting attributes (`{ bold: true, … }`). As an *update value* on `retain`/`modify` it is a
 * unified tri-state (identical to {@link Attribution}): `undefined`/omitted = skip, `null` = clear all,
 * `{k:v}` = set, `{k:null}` = remove key — see the delta `readme.md` "Formats & Attributions".
 *
 * ⚠️ **Blanket `null` clear is a local-only utility — never send it over a channel.** A `null` clear carries
 * no key information, so it cannot be reconciled key-by-key under `rebase`: a clear *always wins* a concurrent
 * edit (priority-independent — that is what makes it converge), which silently DROPS the other side's set. For
 * collaborative / transmitted data, clear keys individually with `{k:null}` removals instead — those reconcile
 * by priority and converge without data loss (this is what {@link diff} emits and what the rebase fuzz uses).
 *
 * @typedef {{ [key: string]: any }} Formats
 */

/**
 * @typedef {{ id: string, key: number|string, assoc: 1|-1, attrs?: object, transient?: boolean }} MarkJSON
 */

/**
 * @typedef {{
 *   type: 'delta',
 *   name?: string,
 *   attrs?: { [Key in string|number]: DeltaAttrOpJSON },
 *   children?: Array<DeltaListOpJSON>,
 *   marks?: Array<MarkJSON>,
 *   deleteMarks?: Array<string>
 * }} DeltaJSON
 */

/**
 * @typedef {{ type: 'insert', insert: string|Array<any>, format?: { [key: string]: any }, attribution?: Attribution } | { delete: number } | { type: 'retain', retain: number, format?: { [key:string]: any }|null, attribution?: Attribution|null } | { type: 'modify', value: object, format?: { [key:string]: any }|null, attribution?: Attribution|null }} DeltaListOpJSON
 */

/**
 * @typedef {{ type: 'insert', value: any, attribution?: Attribution } | { type: 'delete', attribution?: Attribution } | { type: 'modify', value: DeltaJSON, attribution?: Attribution|null }} DeltaAttrOpJSON
 */

/**
 * @typedef {TextOp|InsertOp<any>|DeleteOp<any>|RetainOp|ModifyOp<any>} ChildrenOpAny
 */

/**
 * @typedef {SetAttrOp<any>|DeleteAttrOp<any>|ModifyAttrOp} AttrOpAny
 */

/**
 * @typedef {ChildrenOpAny|AttrOpAny} _OpAny
 */

/**
 * @type {s.Schema<DeltaAttrOpJSON>}
 */
export const $deltaMapChangeJson = /* @__PURE__ */(() => s.$union(
  s.$object({ type: s.$literal('insert'), value: s.$any, attribution: $attribution.optional }),
  s.$object({ type: s.$literal('modify'), value: s.$any, attribution: $attribution.optional }),
  s.$object({ type: s.$literal('delete'), attribution: $attribution.optional })
))()

/**
 * @template {{[key:string]: any} | null | undefined} Attrs
 * @param {Attrs} attrs
 * @return {Attrs}
 */
const _cloneAttrs = attrs => attrs == null ? attrs : { ...attrs }

/**
 * A retain with neither format nor attribution — positional only, not content. `done()` trims a
 * trailing run of them; fingerprint, equality and `isEmpty` ignore that run. (`format: null` is a
 * clear instruction and therefore content — the same rule `done()` applies.)
 *
 * @param {ChildrenOpAny} op
 */
const _isPlainRetain = op => $retainOp.check(op) && op.format === undefined && op.attribution === undefined

/**
 * The number of content ops in a children list: its length minus the trailing plain-retain run (found
 * by walking from the right). The first `n` ops from `cs.start` are the content.
 *
 * @param {list.List<ChildrenOpAny>} cs
 * @return {number}
 */
const _contentLen = cs => {
  let n = cs.len
  for (let end = cs.end; end !== null && _isPlainRetain(end); end = end.prev) n--
  return n
}

/**
 * Shallow per-key tri-state merge of `update` into `base` (the usual `format`-dimension semantics, also reused
 * as the inner step of {@link mergeAttr}). Per key: `undefined` skips, `null` removes, anything else sets.
 * `resolve` true (data semantics) applies a `null` removal by deleting the key; false (instruction semantics)
 * keeps the `null` verbatim so it still removes when the instruction is later applied. When `resolve` is true
 * the `base` is canonicalised too — a `{k:null}` already present in `base` has nothing to clear on settled
 * data, so it is dropped rather than copied through. (The used-context call sites pass pre-resolved
 * companions — see {@link resolveUsedData} — so for them this is a no-op safety net; it matters for the
 * apply/diff paths that merge instructions onto stored values.)
 *
 * @param {{[k:string]:any}|null|undefined} base
 * @param {{[k:string]:any}} update
 * @param {boolean} resolve
 * @return {{[k:string]:any}}
 */
const mergeShallow = (base, update, resolve) => {
  const r = /** @type {{[k:string]:any}} */ ({})
  // copy base; when resolving, drop any `{k:null}` removal already in base (settled data carries no `null` leaf)
  if (s.$objectAny.check(base)) for (const k in base) { if (!resolve || base[k] !== null) r[k] = base[k] }
  for (const k in update) {
    const v = update[k]
    if (v === undefined) continue // skip this key
    else if (v !== null) r[k] = v // set this key
    else if (resolve) delete r[k] // data: apply the removal
    else r[k] = null // instruction: keep the `null` removal verbatim for later
  }
  return r
}
/**
 * Merge for the `attribution` dimension: the usual shallow tri-state ({@link mergeShallow}) **except** the
 * `format` key — attribution's one structured sub-field — is itself merged per inner key (one level only, same
 * tri-state). This realises "first merge the formats, then merge the whole attribution". The nesting applies to
 * the `format` key alone; every other attribution key (`insert`/`delete` arrays, the `*At` numbers, …) stays a
 * leaf. An emptied `format` is dropped (a settled attribution never stores `format: {}`). When `resolve` is
 * true the `base` is canonicalised too (see {@link mergeShallow}): a `{k:null}` leaf in `base`, and a
 * `{innerK:null}` inside `base.format`, have nothing to clear on settled data, so they are dropped — an
 * emptied `base.format` is removed. This collapses a `useAttribution({format:{bold:null}})`-style context on
 * a data op instead of storing the removal.
 *
 * @param {{[k:string]:any}|null|undefined} base
 * @param {{[k:string]:any}} update
 * @param {boolean} resolve see {@link mergeShallow}
 * @return {{[k:string]:any}}
 */
const mergeAttr = (base, update, resolve) => {
  // copy base; when resolving, canonicalise it — drop a `{k:null}` leaf and resolve `base.format`'s own
  // inner removals (dropping an emptied `format`). On an instruction merge (`!resolve`) base is copied verbatim.
  const r = /** @type {{[k:string]:any}} */ ({})
  if (s.$objectAny.check(base)) {
    for (const k in base) {
      const bv = base[k]
      if (resolve && bv === null) continue // drop a base leaf removal
      if (resolve && k === 'format' && s.$objectAny.check(bv)) {
        const f = mergeShallow(bv, {}, true) // resolve base.format's own `{innerK:null}` removals
        if (!object.isEmpty(f)) r[k] = f // drop an emptied format
      } else r[k] = bv
    }
  }
  for (const k in update) {
    const v = update[k]
    if (v === undefined) continue // skip this key
    else if (k === 'format' && s.$objectAny.check(v)) { // the one nested key: merge per inner key (one level)
      const sub = mergeShallow(r[k], v, resolve)
      if (object.isEmpty(sub)) delete r[k] // emptied format ⇒ drop the key
      else r[k] = sub
    } else if (v !== null) r[k] = v // set leaf
    else if (resolve) delete r[k] // data: apply the removal
    else r[k] = null // instruction: keep the `null` removal verbatim for later
  }
  return r
}
/**
 * Resolve a retain's tri-state `format`/`attribution` instruction against an EMPTY base — used by a
 * `final` (materializing) apply for a retain that extends beyond existing content: a `{k:null}` removal or
 * a `null` clear has nothing to act on, so it collapses to `undefined` (skip); a set survives. `deep`
 * selects the attribution merge (its nested `format` key merges one level, {@link mergeAttr}) vs. the
 * shallow `format` merge ({@link mergeShallow}).
 *
 * @param {{[k:string]:any}|null|undefined} arg
 * @param {boolean} deep
 * @return {{[k:string]:any}|undefined}
 */
const resolveBeyond = (arg, deep) => {
  if (arg == null) return undefined // undefined = skip; null = clear-all with nothing to clear = no-op
  const m = deep ? mergeAttr(undefined, arg, true) : mergeShallow(undefined, arg, true)
  return object.isEmpty(m) ? undefined : m
}
/**
 * Resolve a `used*` context (set via {@link DeltaBuilder#useFormats}/`useAttribution`) for **data**
 * consumption: settled data stores canonical values only
 * (no `null` leaves — see {@link combineData}), while the slot itself keeps removal instructions
 * verbatim for the instruction ops it governs. Returns the SAME object when it is already canonical
 * (the common case — provenance contexts carry no removals), so every data op under one context shares
 * one interned object; otherwise strips the removals ONCE into a shared copy (`{}` collapses to `null`).
 * Never called per op — {@link DeltaBuilder#_getUsedFormatsData} caches the result per context object.
 *
 * @param {{[k:string]:any}|null} used
 * @param {boolean} deep `true` for the `attribution` dimension (its `format` key resolves one level
 *   deeper via {@link mergeAttr}); `false` for `format` (shallow, {@link mergeShallow})
 * @return {{[k:string]:any}|null}
 */
const resolveUsedData = (used, deep) => {
  if (used === null) return null
  let empty = true
  let canonical = true
  for (const k in used) {
    empty = false
    const v = used[k]
    if (v === null) { canonical = false; break } // a top-level removal (incl. `format: null`)
    if (deep && k === 'format' && s.$objectAny.check(v)) {
      // mirror mergeAttr's resolve-mode base handling: an emptied `format` is dropped on data, and an
      // inner `{k:null}` removal is stripped — either makes the context non-canonical for data ops
      let formatCanonical = false // empty `format: {}` ⇒ non-canonical
      for (const ik in v) {
        if (v[ik] === null) { formatCanonical = false; break }
        formatCanonical = true
      }
      if (!formatCanonical) { canonical = false; break }
    }
  }
  if (empty) return null
  if (canonical) return used
  const m = deep ? mergeAttr(used, {}, true) : mergeShallow(used, {}, true)
  return object.isEmpty(m) ? null : m
}
/**
 * Combine a builder's `used*` context with a per-call format/attribution argument for an
 * **instruction** op (`retain`/`modify`/`modifyAttr`) under the unified tri-state: `undefined` — or an
 * empty object, an empty update — inherits the context BY IDENTITY (no traversal, no copy; contexts
 * are interned); `null` clears (ignoring the context); a non-empty object merges over the context,
 * keeping `{k:null}` removals verbatim so they still remove when the instruction is later applied.
 * An empty result is a no-op → `undefined` (skip).
 *
 * @param {{[k:string]:any}|null} used the raw context slot (removal instructions intact)
 * @param {{[k:string]:any}|null|undefined} arg
 * @param {boolean} deep `true` for the `attribution` dimension (its `format` key merges per inner key via
 *   {@link mergeAttr}); `false` for `format` (shallow, {@link mergeShallow}).
 * @return {{[k:string]:any}|null|undefined}
 */
const combineInstr = (used, arg, deep) => {
  if (arg === null) return null // explicit clear — must precede the isEmpty check (object.isEmpty(null) is true)
  if (arg === undefined || object.isEmpty(arg)) return used === null || object.isEmpty(used) ? undefined : used
  const r = deep ? mergeAttr(used, arg, false) : mergeShallow(used, arg, false)
  return object.isEmpty(r) ? undefined : r
}
/**
 * Combine for a **data** op (`insert`/`text`/`setAttr`/`deleteAttr`): the stored value is a canonical
 * object or `null` ("none") — never a `{k:null}` removal, there is nothing to remove on settled data.
 * `usedData` is the builder's RESOLVED context companion ({@link resolveUsedData}, cached by
 * {@link DeltaBuilder#_getUsedFormatsData}) and thus already canonical (`null` or a non-empty
 * no-`null` object), so `undefined` — or an empty object, an empty update — inherits it BY IDENTITY
 * (no traversal, no copy); `null` clears; a non-empty object merges over it, resolving the argument's
 * own `{k:null}` removals. `{}` results collapse to `null`.
 *
 * @param {{[k:string]:any}|null} usedData
 * @param {{[k:string]:any}|null|undefined} arg
 * @param {boolean} deep see {@link combineInstr}
 * @return {{[k:string]:any}|null}
 */
const combineData = (usedData, arg, deep) => {
  if (arg === null) return null // explicit clear — must precede the isEmpty check (object.isEmpty(null) is true)
  if (arg === undefined || object.isEmpty(arg)) return usedData
  const r = deep ? mergeAttr(usedData, arg, true) : mergeShallow(usedData, arg, true)
  return object.isEmpty(r) ? null : /** @type {{[k:string]:any}} */ (r)
}
/**
 * Freeze a maybe-delta for a frozen clone (a scalar passes through untouched). `done()` is
 * fingerprint-neutral (it only trims trailing plain retains, which are not fingerprinted), so the
 * clone may carry the op's cached fingerprint over.
 *
 * @template {any} MaybeDelta
 * @param {MaybeDelta} maybeDelta
 * @return {MaybeDelta}
 */
const _markMaybeDeltaAsDone = maybeDelta => $deltaAny.check(maybeDelta) ? /** @type {MaybeDelta} */ (maybeDelta.done()) : maybeDelta

/**
 * Invariants shared by all op classes below (TextOp, InsertOp, DeleteOp,
 * RetainOp, ModifyOp, SetAttrOp, DeleteAttrOp, ModifyAttrOp):
 *
 * - **Only code inside `delta.js` may mutate op fields.** External consumers
 *   treat ops as immutable; structural fields are JSDoc-annotated `@readonly`
 *   to reinforce this. Mutation is permitted only while the owning Delta is
 *   not `done` — every builder entry point routes through `modDeltaCheck`
 *   to enforce this at runtime.
 * - **Any mutation of a fingerprinted field MUST null `_fingerprint`.** The
 *   fingerprint is a lazy cache; if it has already been computed and the
 *   underlying data changes without invalidating it, every subsequent
 *   fingerprint read (and any `diff` / equality check that relies on it) is
 *   wrong. Fields covered: insert, delete, retain, format, attribution,
 *   value, key.
 * - **A `clone` copies `_fingerprint` when the copy is content-identical:** a full range and the
 *   same key. Freezing a nested value with `done()` is fingerprint-neutral (trailing plain retains
 *   are not fingerprinted), so nothing else can differ. The root delta returned by
 *   `clone`/`slice`/`cloneDeep` never inherits a fingerprint.
 *
 * @internal not part of the consumer API — a content op; build deltas via {@link create}/{@link DeltaBuilder}.
 */
export class TextOp extends list.ListNode {
  /**
   * @param {string} insert
   * @param {Formats|null} format
   * @param {Attribution?} attribution
   */
  constructor (insert, format, attribution) {
    super()
    // Whenever this is modified, make sure to clear _fingerprint
    /**
     * @readonly
     * @type {string}
     */
    this.insert = insert
    /**
     * @readonly
     * @type {Formats|null}
     */
    this.format = format
    this.attribution = attribution
    /**
     * @type {string?}
     */
    this._fingerprint = null
  }

  /**
   * @return {'insert'}
   */
  get type () {
    return 'insert'
  }

  get length () {
    return this.insert.length
  }

  get fingerprint () {
    return this._fingerprint || (this._fingerprint = buffer.toBase64(encoding.encode(encoder => {
      encoding.writeVarUint(encoder, 0) // textOp type: 0
      encoding.writeVarString(encoder, this.insert)
      encoding.writeAny(encoder, this.format)
      encoding.writeAny(encoder, this.attribution)
    })))
  }

  /**
   * Remove a part of the operation (similar to Array.splice)
   *
   * @param {number} offset
   * @param {number} len
   */
  _splice (offset, len) {
    this._fingerprint = null
    // @ts-ignore
    this.insert = this.insert.slice(0, offset) + this.insert.slice(offset + len)
    return this
  }

  /**
   * @return {DeltaListOpJSON}
   */
  toJSON () {
    const { insert, format, attribution } = this
    return object.assign(/** @type {{type: 'insert', insert: string}} */ ({ type: 'insert', insert }), format != null ? { format } : ({}), attribution != null ? { attribution } : ({}))
  }

  /**
   * @param {TextOp} other
   */
  [equalityTrait.EqualityTraitSymbol] (other) {
    return $textOp.check(other) &&
      fun.equalityDeep(this.insert, other.insert) &&
      (
        (object.isEmpty(this.format) && object.isEmpty(other.format)) || fun.equalityDeep(this.format, other.format)
      ) &&
      fun.equalityDeep(this.attribution, other.attribution)
  }

  /**
   * @param {number} [start]
   * @param {number} [end]
   * @param {boolean} [_markAsDone] accepted for a uniform children-op `clone` signature; ignored (text
   * holds no nested deltas to freeze).
   * @return {TextOp}
   */
  clone (start = 0, end = this.length, _markAsDone = true) {
    const cpy = new TextOp(this.insert.slice(start, end), _cloneAttrs(this.format), _cloneAttrs(this.attribution))
    if (start === 0 && end === this.length) cpy._fingerprint = this._fingerprint
    return cpy
  }
}

/**
 * @template {any} ArrayContent
 * @internal not part of the consumer API — a content op; build deltas via {@link create}/{@link DeltaBuilder}.
 */
export class InsertOp extends list.ListNode {
  /**
   * @param {Array<ArrayContent>} insert
   * @param {Formats|null} format
   * @param {Attribution?} attribution
   */
  constructor (insert, format, attribution) {
    super()
    /**
     * @readonly
     * @type {Array<ArrayContent>}
     */
    this.insert = insert
    /**
     * @readonly
     * @type {Formats?}
     */
    this.format = format
    /**
     * @readonly
     * @type {Attribution?}
     */
    this.attribution = attribution
    /**
     * @type {string?}
     */
    this._fingerprint = null
  }

  /**
   * @return {'insert'}
   */
  get type () {
    return 'insert'
  }

  get length () {
    return this.insert.length
  }

  /**
   * @param {number} i
   * @return {Extract<ArrayContent,DeltaAny>}
   */
  _modValue (i) {
    /**
     * @type {any}
     */
    let d = this.insert[i]
    this._fingerprint = null
    $deltaAny.expect(d)
    if (d.isDone) {
      // @ts-ignore
      this.insert[i] = (d = clone(d))
      return d
    }
    return d
  }

  get fingerprint () {
    return this._fingerprint || (this._fingerprint = buffer.toBase64(encoding.encode(encoder => {
      encoding.writeVarUint(encoder, 1) // insertOp type: 1
      encoding.writeVarUint(encoder, this.insert.length)
      this.insert.forEach(ins => {
        encoding.writeVarString(encoder, fingerprintTrait.fingerprint(/** @type {any} */ (ins)))
      })
      encoding.writeAny(encoder, this.format)
      encoding.writeAny(encoder, this.attribution)
    })))
  }

  /**
   * Remove a part of the operation (similar to Array.splice)
   *
   * @param {number} offset
   * @param {number} len
   */
  _splice (offset, len) {
    this._fingerprint = null
    this.insert.splice(offset, len)
    return this
  }

  /**
   * @return {DeltaListOpJSON}
   */
  toJSON () {
    const { insert, format, attribution } = this
    return object.assign({ type: /** @type {'insert'} */ ('insert'), insert: insert.map(ins => $deltaAny.check(ins) ? ins.toJSON() : ins) }, format ? { format } : ({}), attribution != null ? { attribution } : ({}))
  }

  /**
   * @param {InsertOp<ArrayContent>} other
   */
  [equalityTrait.EqualityTraitSymbol] (other) {
    return $insertOp.check(other) &&
      fun.equalityDeep(this.insert, other.insert) &&
      (
        (object.isEmpty(this.format) && object.isEmpty(other.format)) || fun.equalityDeep(this.format, other.format)
      ) &&
      fun.equalityDeep(this.attribution, other.attribution)
  }

  /**
   * @param {number} [start]
   * @param {number} [end]
   * @param {boolean} [markAsDone] freeze the cloned child deltas (the default — a shared clone must be
   * immutable). Pass `false` only when the caller is the SOLE owner of the cloned range (a *move*, e.g.
   * `splitHere` which `_splice`s the range out of the source), so the content can stay mutable and is
   * not re-cloned on the next modify (see {@link modValue}).
   * @return {InsertOp<ArrayContent>}
   */
  clone (start = 0, end = this.length, markAsDone = true) {
    const insert = this.insert.slice(start, end)
    const cpy = new InsertOp(markAsDone ? insert.map(_markMaybeDeltaAsDone) : insert, _cloneAttrs(this.format), _cloneAttrs(this.attribution))
    if (start === 0 && end === this.length) cpy._fingerprint = this._fingerprint
    return cpy
  }
}

/**
 * @template {DeltaConf} [Conf={}]
 * @internal not part of the consumer API — a content op; build deltas via {@link create}/{@link DeltaBuilder}.
 */
export class DeleteOp extends list.ListNode {
  /**
   * @param {number} len
   */
  constructor (len) {
    super()
    this.delete = len
    /**
     * @type {string|null}
     */
    this._fingerprint = null
  }

  /**
   * @return {'delete'}
   */
  get type () {
    return 'delete'
  }

  get length () {
    return this.delete
  }

  get fingerprint () {
    return this._fingerprint || (this._fingerprint = buffer.toBase64(encoding.encode(encoder => {
      encoding.writeVarUint(encoder, 2) // deleteOp type: 2
      encoding.writeVarUint(encoder, this.delete)
    })))
  }

  /**
   * Remove a part of the operation (similar to Array.splice)
   *
   * @param {number} _offset
   * @param {number} len
   */
  _splice (_offset, len) {
    this._fingerprint = null
    this.delete -= len
    return this
  }

  /**
   * @return {DeltaListOpJSON}
   */
  toJSON () {
    return { delete: this.delete }
  }

  /**
   * @param {DeleteOp<any>} other
   */
  [equalityTrait.EqualityTraitSymbol] (other) {
    return $deleteOp.check(other) && this.delete === other.delete
  }

  /**
   * @param {number} [start]
   * @param {number} [end]
   * @param {boolean} [_markAsDone] accepted for a uniform children-op `clone` signature; ignored (a
   * delete holds no nested content).
   * @return {DeleteOp<any>}
   */
  clone (start = 0, end = this.delete, _markAsDone = true) {
    const cpy = new DeleteOp(end - start)
    if (end - start === this.delete) cpy._fingerprint = this._fingerprint
    return cpy
  }
}

/**
 * @internal not part of the consumer API — a content op; build deltas via {@link create}/{@link DeltaBuilder}.
 */
export class RetainOp extends list.ListNode {
  /**
   * @param {number} retain
   * @param {Formats|null|undefined} format tri-state: `undefined` skip / `null` clear / object merge
   * @param {Attribution|null|undefined} attribution tri-state: `undefined` skip / `null` clear / object merge
   */
  constructor (retain, format, attribution) {
    super()
    /**
     * @readonly
     * @type {number}
     */
    this.retain = retain
    /**
     * @readonly
     * @type {Formats|null|undefined}
     */
    this.format = format
    /**
     * @readonly
     * @type {Attribution|null|undefined}
     */
    this.attribution = attribution
    /**
     * @type {string|null}
     */
    this._fingerprint = null
  }

  /**
   * @return {'retain'}
   */
  get type () {
    return 'retain'
  }

  get length () {
    return this.retain
  }

  get fingerprint () {
    return this._fingerprint || (this._fingerprint = buffer.toBase64(encoding.encode(encoder => {
      encoding.writeVarUint(encoder, 3) // retainOp type: 3
      encoding.writeVarUint(encoder, this.retain)
      encoding.writeAny(encoder, this.format)
      encoding.writeAny(encoder, this.attribution)
    })))
  }

  /**
   * Remove a part of the operation (similar to Array.splice)
   *
   * @param {number} _offset
   * @param {number} len
   */
  _splice (_offset, len) {
    // @ts-ignore
    this.retain -= len
    this._fingerprint = null
    return this
  }

  /**
   * @return {DeltaListOpJSON}
   */
  toJSON () {
    const { retain, format, attribution } = this
    // retain is an instruction op: emit `null` (clear) distinctly from `undefined` (skip / omitted)
    return object.assign({ type: /** @type {'retain'} */ ('retain'), retain }, format !== undefined ? { format } : {}, attribution !== undefined ? { attribution } : {})
  }

  /**
   * @param {RetainOp} other
   */
  [equalityTrait.EqualityTraitSymbol] (other) {
    return $retainOp.check(other) &&
      this.retain === other.retain &&
      (
        (object.isEmpty(this.format) && object.isEmpty(other.format)) || fun.equalityDeep(this.format, other.format)
      ) &&
      fun.equalityDeep(this.attribution, other.attribution)
  }

  /**
   * @param {number} [start]
   * @param {number} [end]
   * @param {boolean} [_markAsDone] accepted for a uniform children-op `clone` signature; ignored (retain
   * holds no nested deltas).
   * @return {RetainOp}
   */
  clone (start = 0, end = this.retain, _markAsDone = true) {
    const cpy = new RetainOp(end - start, _cloneAttrs(this.format), _cloneAttrs(this.attribution))
    if (end - start === this.retain) cpy._fingerprint = this._fingerprint
    return cpy
  }
}

/**
 * Delta that can be applied on a YType Embed
 *
 * @template {Delta<any>} [DTypes=DeltaAny]
 * @internal not part of the consumer API — a content op; build deltas via {@link create}/{@link DeltaBuilder}.
 */
export class ModifyOp extends list.ListNode {
  /**
   * @param {DTypes} delta
   * @param {Formats|null|undefined} format tri-state: `undefined` skip / `null` clear / object merge
   * @param {Attribution|null|undefined} attribution tri-state: `undefined` skip / `null` clear / object merge
   */
  constructor (delta, format, attribution) {
    super()
    /**
     * @readonly
     * @type {DTypes}
     */
    this.value = delta
    /**
     * @readonly
     * @type {Formats|null|undefined}
     */
    this.format = format
    /**
     * @readonly
     * @type {Attribution|null|undefined}
     */
    this.attribution = attribution
    /**
     * @type {string|null}
     */
    this._fingerprint = null
  }

  /**
   * @return {'modify'}
   */
  get type () {
    return 'modify'
  }

  get length () {
    return 1
  }

  /**
   * @type {DeltaBuilderAny}
   */
  get _modValue () {
    return modValue(this)
  }

  get fingerprint () {
    return this._fingerprint || (this._fingerprint = buffer.toBase64(encoding.encode(encoder => {
      encoding.writeVarUint(encoder, 4) // modifyOp type: 4
      encoding.writeVarString(encoder, this.value.fingerprint)
      encoding.writeAny(encoder, this.format)
      encoding.writeAny(encoder, this.attribution)
    })))
  }

  /* c8 ignore start */
  // ModifyOp has length 1, so callers never pass offset>0 or len>0 — splitHere
  // is a no-op for length-1 ops. Kept for the structural _splice contract.
  /**
   * Remove a part of the operation (similar to Array.splice)
   *
   * @param {number} _offset
   * @param {number} _len
   */
  _splice (_offset, _len) {
    return this
  }
  /* c8 ignore stop */

  /**
   * @return {DeltaListOpJSON}
   */
  toJSON () {
    const { value, attribution, format } = this
    // modify is an instruction op: emit `null` (clear) distinctly from `undefined` (skip / omitted)
    return object.assign(
      { type: /** @type {'modify'} */ ('modify'), value: value.toJSON() },
      format !== undefined ? { format } : {},
      attribution !== undefined ? { attribution } : {}
    )
  }

  /**
   * @param {ModifyOp<any>} other
   */
  [equalityTrait.EqualityTraitSymbol] (other) {
    return $modifyOp.check(other) &&
      this.value[equalityTrait.EqualityTraitSymbol](other.value) &&
      (
        (object.isEmpty(this.format) && object.isEmpty(other.format)) || fun.equalityDeep(this.format, other.format)
      ) &&
      fun.equalityDeep(this.attribution, other.attribution)
  }

  /**
   * @param {number} [_start] ignored (a modify is atomic); accepted for a uniform children-op signature.
   * @param {number} [_end] ignored.
   * @param {boolean} [markAsDone] freeze the cloned value (default); `false` shares it mutable — see
   * {@link InsertOp#clone}. The op itself is still a fresh node; only its `value` is consumed.
   * @return {ModifyOp<DTypes>}
   */
  clone (_start = 0, _end = 1, markAsDone = true) {
    // `markAsDone=false` is currently unreachable: only `_splitOp` passes `false`, and a length-1
    // modify is never split (apply's `move ? op : op.clone(0, 1, keep)` evaluates the clone only when
    // `keep` is true). Kept for the uniform children-op `clone` signature.
    /* c8 ignore next */
    const cpy = new ModifyOp(/** @type {DTypes} */ (markAsDone ? this.value.done() : this.value), _cloneAttrs(this.format), _cloneAttrs(this.attribution))
    cpy._fingerprint = this._fingerprint
    return cpy
  }
}

/**
 * @template {any} [V=any]
 * @template {string|number} [K=any]
 * @internal not part of the consumer API — an attribute op; build deltas via {@link create}/{@link DeltaBuilder}.
 */
export class SetAttrOp {
  /**
   * @param {K} key
   * @param {V} value
   * @param {Attribution?} attribution
   */
  constructor (key, value, attribution) {
    /**
     * @readonly
     * @type {K}
     */
    this.key = key
    /**
     * @readonly
     * @type {V}
     */
    this.value = value
    /**
     * @readonly
     * @type {Attribution?}
     */
    this.attribution = attribution
    /**
     * @type {string|null}
     */
    this._fingerprint = null
  }

  /**
   * @return {'insert'}
   */
  get type () { return 'insert' }

  /**
   * @type {DeltaBuilderAny}
   */
  get _modValue () {
    return modValue(this)
  }

  get fingerprint () {
    return this._fingerprint || (this._fingerprint = buffer.toBase64(encoding.encode(encoder => {
      encoding.writeVarUint(encoder, 5) // map insert type: 5
      encoding.writeAny(encoder, this.key)
      if ($deltaAny.check(this.value)) {
        encoding.writeUint8(encoder, 0)
        encoding.writeVarString(encoder, this.value.fingerprint)
      } else {
        encoding.writeUint8(encoder, 1)
        encoding.writeAny(encoder, /** @type {any} */ (this.value))
      }
      encoding.writeAny(encoder, this.attribution)
    })))
  }

  toJSON () {
    const v = this.value
    const attribution = this.attribution
    return object.assign(
      {
        type: this.type,
        value: $deltaAny.check(v) ? v.toJSON() : v
      },
      attribution != null ? { attribution } : {}
    )
  }

  /**
   * @param {SetAttrOp<V>} other
   */
  [equalityTrait.EqualityTraitSymbol] (other) {
    return $setAttrOp.check(other) && this.key === other.key && fun.equalityDeep(this.value, other.value) && fun.equalityDeep(this.attribution, other.attribution)
  }

  /**
   * Full (frozen) clone. A `move` apply reuses the source op instead of cloning, so there is no
   * shared-mutable variant — see {@link DeltaBuilder#apply}.
   *
   * @template {string|number} [K2=K]
   * @param {K2} [key] retarget the clone to another attribute key (a transformer projecting or
   * renaming an attribute); defaults to this op's key.
   * @return {SetAttrOp<V,K2>}
   */
  clone (key = /** @type {any} */ (this.key)) {
    const cpy = new SetAttrOp(key, _markMaybeDeltaAsDone(this.value), _cloneAttrs(this.attribution))
    if (/** @type {string|number} */ (key) === this.key) cpy._fingerprint = this._fingerprint // the key is part of the fingerprint
    return cpy
  }
}

/**
 * @template [V=any]
 * @template {string|number} [K=string|number]
 * @internal not part of the consumer API — an attribute op; build deltas via {@link create}/{@link DeltaBuilder}.
 */
export class DeleteAttrOp {
  /**
   * @param {K} key
   * @param {Attribution?} attribution
   */
  constructor (key, attribution) {
    /**
     * @type {K}
     */
    this.key = key
    this.attribution = attribution
    /**
     * @type {string|null}
     */
    this._fingerprint = null
  }

  /**
   * @type {'delete'}
   */
  get type () { return 'delete' }

  get value () { return undefined }

  get fingerprint () {
    return this._fingerprint || (this._fingerprint = buffer.toBase64(encoding.encode(encoder => {
      encoding.writeVarUint(encoder, 6) // map delete type: 6
      encoding.writeAny(encoder, this.key)
      encoding.writeAny(encoder, this.attribution)
    })))
  }

  /**
   * @return {DeltaAttrOpJSON}
   */
  toJSON () {
    const {
      type, attribution
    } = this
    return object.assign(
      { type },
      attribution != null ? { attribution } : {}
    )
  }

  /**
   * @param {DeleteAttrOp<V>} other
   */
  [equalityTrait.EqualityTraitSymbol] (other) {
    return $deleteAttrOp.check(other) && this.key === other.key && fun.equalityDeep(this.attribution, other.attribution)
  }

  /**
   * Full (frozen) clone; a `move` apply reuses the source op instead — see {@link DeltaBuilder#apply}.
   *
   * @template {string|number} [K2=K]
   * @param {K2} [key] retarget the clone to another attribute key; defaults to this op's key.
   * @return {DeleteAttrOp<V,K2>}
   */
  clone (key = /** @type {any} */ (this.key)) {
    const cpy = new DeleteAttrOp(key, _cloneAttrs(this.attribution))
    if (/** @type {string|number} */ (key) === this.key) cpy._fingerprint = this._fingerprint // the key is part of the fingerprint
    return cpy
  }
}

/**
 * @template {DeltaAny} [Modifier=DeltaAny]
 * @template {string|number} [K=string]
 * @internal not part of the consumer API — an attribute op; build deltas via {@link create}/{@link DeltaBuilder}.
 */
export class ModifyAttrOp {
  /**
   * @param {K} key
   * @param {Modifier} delta
   * @param {Attribution|null|undefined} attribution tri-state: `undefined` skip / `null` clear / object merge
   */
  constructor (key, delta, attribution) {
    /**
     * @readonly
     * @type {K}
     */
    this.key = key
    /**
     * @readonly
     * @type {Modifier}
     */
    this.value = delta
    /**
     * @readonly
     * @type {Attribution|null|undefined}
     */
    this.attribution = attribution
    /**
     * @type {string|null}
     */
    this._fingerprint = null
  }

  /**
   * @type {'modify'}
   */
  get type () { return 'modify' }

  get fingerprint () {
    return this._fingerprint || (this._fingerprint = buffer.toBase64(encoding.encode(encoder => {
      encoding.writeVarUint(encoder, 7) // map modify type: 7
      encoding.writeAny(encoder, this.key)
      encoding.writeVarString(encoder, this.value.fingerprint)
      encoding.writeAny(encoder, this.attribution)
    })))
  }

  /**
   * @return {DeltaBuilder<any>}
   */
  get _modValue () {
    return modValue(this)
  }

  /**
   * @return {DeltaAttrOpJSON}
   */
  toJSON () {
    const attribution = this.attribution
    return object.assign(
      {
        type: this.type,
        value: this.value.toJSON()
      },
      // modifyAttr applies its attribution as an instruction: emit `null` (clear) distinctly from skip
      attribution !== undefined ? { attribution } : {}
    )
  }

  /**
   * @param {ModifyAttrOp<Modifier>} other
   */
  [equalityTrait.EqualityTraitSymbol] (other) {
    return $modifyAttrOp.check(other) && this.key === other.key && this.value[equalityTrait.EqualityTraitSymbol](other.value) && fun.equalityDeep(this.attribution, other.attribution)
  }

  /**
   * Full (frozen) clone; a `move` apply reuses the source op instead — see {@link DeltaBuilder#apply}.
   *
   * @template {string|number} [K2=K]
   * @param {K2} [key] retarget the clone to another attribute key; defaults to this op's key.
   * @return {ModifyAttrOp<Modifier,K2>}
   */
  clone (key = /** @type {any} */ (this.key)) {
    const cpy = new ModifyAttrOp(key, /** @type {Modifier} */ (this.value.done()), _cloneAttrs(this.attribution))
    if (/** @type {string|number} */ (key) === this.key) cpy._fingerprint = this._fingerprint // the key is part of the fingerprint
    return cpy
  }
}

export const $insertOp = /** @type {s.Schema<InsertOp<any>>} */ (InsertOp.prototype.$type = /** @type {s.Schema<InsertOp<any>>} */ (s.$type('d:insertOp', InsertOp)))
export const $modifyOp = ModifyOp.prototype.$type = s.$type('d:modifyOp', ModifyOp)
export const $textOp = TextOp.prototype.$type = s.$type('d:textOp', TextOp)
export const $deleteOp = /** @type {s.Schema<DeleteOp<any>>} */ (DeleteOp.prototype.$type = s.$type('d:deleteOp', DeleteOp))
export const $retainOp = RetainOp.prototype.$type = s.$type('d:retainOp', RetainOp)
export const $anyOp = s.$union($insertOp, $deleteOp, $textOp, $modifyOp)

export const $setAttrOp = /** @type {s.Schema<SetAttrOp<any>>} */ (SetAttrOp.prototype.$type =/** @type {s.Schema<SetAttrOp<any>>} */ (s.$type('d:setAttrOp', SetAttrOp)))
export const $modifyAttrOp = /** @type {s.Schema<ModifyAttrOp<any>>} */ (ModifyAttrOp.prototype.$type = /** @type {s.Schema<ModifyAttrOp<any>>} */ (s.$type('d:modifyAttrOp', ModifyAttrOp)))
export const $deleteAttrOp = /** @type {s.Schema<DeleteAttrOp<any>>} */ (DeleteAttrOp.prototype.$type = /** @type {s.Schema<DeleteAttrOp<any>>} */ (s.$type('d:deleteAttrOp', DeleteAttrOp)))
export const $anyAttrOp = s.$union($setAttrOp, $deleteAttrOp, $modifyAttrOp)

/**
 * @template {fingerprintTrait.Fingerprintable} Content
 * @param {s.Schema<Content>} $content
 * @return {s.Schema<SetAttrOp<Content>>}
 */
export const $setAttrOpWith = $content => s.$custom(o => $setAttrOp.check(o) && $content.check(o.value))

/**
 * @template {fingerprintTrait.Fingerprintable} Content
 * @param {s.Schema<Content>} $content
 * @return {s.Schema<InsertOp<Content>>}
 */
export const $insertOpWith = $content => s.$custom(o => $insertOp.check(o) && o.insert.every(ins => $content.check(ins)))

/**
 * @template {DeltaAny} Modify
 * @param {s.Schema<Modify>} $content
 * @return {s.Schema<ModifyOp<Modify>>}
 */
export const $modifyOpWith = $content => s.$custom(o => $modifyOp.check(o) && $content.check(o.value))

/**
 * @template {DeltaAny} Modify
 * @param {s.Schema<Modify>} $content
 * @return {s.Schema<ModifyAttrOp<Modify>>}
 */
export const $modifyAttrOpWith = $content => s.$custom(o => $modifyAttrOp.check(o) && $content.check(/** @type {ModifyAttrOp<Modify>} */ (o).value))

/**
 * @template {{[Key in string|number]: any}} Attrs
 * @template {string|number} Key
 * @template {any} Val
 * @typedef {{ [K in (Key | keyof Attrs)]: (unknown extends Attrs[K] ? never : Attrs[K]) | (Key extends K ? Val : never) } & {}} AddToAttrs
 */

/**
 * @template {{[Key in string|number|symbol]: any}} Attrs
 * @template {{[Key in string|number|symbol]: any}} NewAttrs
 * @typedef {{ [K in (keyof NewAttrs | keyof Attrs)]: (unknown extends Attrs[K] ? never : Attrs[K]) | (unknown extends NewAttrs[K] ? never : NewAttrs[K]) }} MergeAttrs
 */

/**
 * A cursor/selection anchor stored inside a delta tree (see {@link import('./position.js').marksToPositions}).
 *
 * - `key` — the *terminal* step of the position: a content offset (number) or an attribute key
 *   (string) within the node that holds the mark.
 * - `id` — a unique, user-defined identifier.
 * - `assoc` — the gravity at a boundary (left `-1` / right `1`).
 * - `attrs` — optional user-supplied data carried with the mark **by reference**: the same
 *   object is shared across the mark, its `copy`/`clone`s, `toJSON`, and every `MarkPos` from
 *   `marksToPositions`, so the caller must treat it as immutable (do not mutate it after attaching).
 * - `transient` — the mark only rides through a mapping and must never be stored (see the field doc).
 *
 * A `Mark` is **immutable** — never mutate one in place; to "move" a mark, replace it with a fresh
 * `Mark` via {@link Mark#copy} (the {@link Marks} set keys by id, so re-adding the same id replaces
 * it). Immutability lets a `Mark` be shared freely across a delta and its clones. The class is not
 * exported — construct via {@link createMark} and validate via {@link $mark}.
 */
class Mark {
  /**
   * @param {number|string} key
   * @param {string} [id] unique id; defaults to a fresh GUID
   * @param {1|-1} [assoc] gravity at a boundary; defaults to right (`1`)
   * @param {object?} [attrs] optional user data, stored by reference; treat as immutable
   * @param {boolean} [transient] the mark must never be stored; see the field doc
   */
  constructor (key, id = rand.uuidv4(), assoc = 1, attrs = null, transient = false) {
    /**
     * @readonly
     * @type {number|string}
     */
    this.key = key
    /**
     * @readonly
     * @type {string}
     */
    this.id = id
    /**
     * @readonly
     * @type {1|-1}
     */
    this.assoc = assoc
    /**
     * @readonly
     * @type {object?}
     */
    this.attrs = attrs
    /**
     * A transient mark exists only to ride through a mapping (see
     * {@link import('./position.js').mapPositionsA}) and must never be stored: a consumer that
     * maintains persistent state (a document, a transformer's internal model of one) either filters
     * transient marks before applying a change to that state, or removes them when found on
     * final/settled state. How that is achieved is left to the implementation — the core does not
     * enforce it (today nothing in lib0 stores marks outside a document, so nothing needs to).
     *
     * @readonly
     * @type {boolean}
     */
    this.transient = transient
  }

  /**
   * A copy of this mark, optionally at a different `key` (used to "move" an otherwise-immutable mark).
   *
   * @param {number|string} [key]
   * @return {Mark}
   */
  copy (key = this.key) {
    return new Mark(key, this.id, this.assoc, this.attrs, this.transient)
  }

  /**
   * @return {MarkJSON}
   */
  toJSON () {
    /** @type {MarkJSON} */
    const json = this.attrs === null
      ? { id: this.id, key: this.key, assoc: this.assoc }
      : { id: this.id, key: this.key, assoc: this.assoc, attrs: this.attrs }
    if (this.transient) json.transient = true
    return json
  }

  /**
   * @param {Mark} other
   */
  [equalityTrait.EqualityTraitSymbol] (other) {
    return $mark.check(other) && this.id === other.id && this.key === other.key && this.assoc === other.assoc && this.transient === other.transient && fun.equalityDeep(this.attrs, other.attrs)
  }
}

export const $mark = /** @type {s.Schema<Mark>} */ (Mark.prototype.$type = s.$type('d:mark', Mark))

/**
 * Create a {@link Mark} (use this instead of `new Mark(...)`). `id` defaults to a fresh GUID, `assoc`
 * to right gravity (`1`), `attrs` to `null`, `transient` to `false`. A `Mark` is stored on a delta
 * node's own {@link Marks} set (see {@link DeltaBuilder#addMark}).
 *
 * @param {number|string} key
 * @param {string} [id]
 * @param {1|-1} [assoc]
 * @param {object?} [attrs]
 * @param {boolean} [transient] never to be stored (see {@link Mark#transient})
 * @return {Mark}
 */
export const createMark = (key, id, assoc, attrs, transient) => new Mark(key, id, assoc, attrs, transient)

/**
 * @typedef {Delta<any>} DeltaAny
 */

/**
 * @typedef {DeltaBuilder<any>} DeltaBuilderAny
 */

/**
 * @typedef {object} DeltaConf
 * @property {string} [DeltaConf.name]
 * @property {fingerprintTrait.Fingerprintable} [DeltaConf.children=never]
 * @property {boolean} [DeltaConf.text=never]
 * @property {{[K:string|number]:fingerprintTrait.Fingerprintable}} [DeltaConf.attrs={}]
 * @property {boolean} [DeltaConf.recursiveChildren=false]
 * @property {boolean} [DeltaConf.recursiveAttrs=false]
 */

/**
 * Coerce a computed conf to provably satisfy the `DeltaConf` constraint. Declaration emit re-checks
 * type arguments eagerly, so a deeply-computed conf (mapped/recursive) passed to `Delta<…>` /
 * `Transformer<…>` must be wrapped here: the conditional exposes `DeltaConf` as its upper bound,
 * letting the constraint resolve without forcing TS to expand the (self-referential) body.
 *
 * @template T
 * @typedef {T extends infer DC extends DeltaConf ? DC : never} AsDeltaConf
 */

/**
 * @template {DeltaConf} Conf
 * @typedef {Conf extends {name:infer Name} ? (unknown extends Name ? any : (Exclude<Name,undefined>)) : any} DeltaConfGetName
 */

/**
 * @template {DeltaConf} Conf
 * @typedef {(Conf extends {children:infer Children} ? (unknown extends Children ? any : Children) : never) | (Conf extends {recursiveChildren:true} ? Delta<Conf> : never)} DeltaConfGetChildren
 */

/**
 * @template {DeltaConf} Conf
 * @template {boolean} FixedConf
 * @typedef {FixedConf extends true ? DeltaConfGetChildren<Conf> : any } DeltaConfGetAllowedChildren
 */

/**
 * @template {DeltaConf} Conf
 * @typedef {0 extends (1 & Conf) ? string : (Conf extends {text:true} ? string : never)} DeltaConfGetText
 */

/**
 * @template {DeltaConf} Conf
 * @typedef {import('../ts.js').TypeIsAny<Conf, {[K:string|number]:any}, (Conf extends {attrs:infer Attrs} ? (Attrs extends undefined ? {} : Attrs) : {})>} DeltaConfGetAttrs
 */

/**
 * @template {DeltaConf} Conf
 * @template {boolean} FixedConf
 * @typedef {FixedConf extends true ? DeltaConfGetAttrs<Conf> : {[K:string|number]:any}} DeltaConfGetAllowedAttrs
 */

/**
 * @template {DeltaConf} Conf
 * @typedef {Conf extends {recursiveChildren:true} ? true : false} DeltaConfGetRecursiveChildren
 */

/**
 * @template {DeltaConf} Conf
 * @typedef {Conf extends {recursiveAttrs:true} ? true : false} DeltaConfigGetRecursiveAttrs
 */

/**
 * Transform Delta(Builder) to a normal delta: a `DeltaBuilder<Conf>` becomes `Delta<Conf>`, anything
 * else passes through.
 *
 * Infers the conf from `DeltaBuilder` on purpose, not from `Delta<infer Conf>`. lib0's own source
 * check never had a problem with the latter, but a consumer type-checks the emitted declaration
 * files, and there matching a builder against `Delta<infer Conf>` is structural: TypeScript walks
 * `Delta`'s members and recurses through {@link DeltaConfGetChildren}'s recursive branch until its
 * instantiation-depth limit, so every nested condensed builder (for example
 * `create().insert([create('c', {}, [create('p', {}, 'x')])])`) failed with TS2589 in
 * y-prosemirror and yjs. The structural walk also leaked the grandchild conf into the child
 * union; inferring from the builder keeps the nesting exact. dist-check/consumer.js pins both
 * against dist.
 *
 * @template V
 * @typedef {V extends never ? never : (import('../ts.js').TypeIsAny<V,any,V extends DeltaBuilder<infer Conf, any> ? Delta<Conf> : V>)} _SanifyDelta
 */

/**
 * @template {DeltaConf} Conf
 * @typedef {import('../ts.js').Prettify<{[K in keyof Conf]: K extends 'attrs' ? import('../ts.js').Prettify<{ [KA in keyof Conf[K]]: _SanifyDelta<Conf[K][KA]> },1> : (K extends 'children' ? _SanifyDelta<Conf[K]> : Conf[K]) }, 1>} PrettifyDeltaConf
 */

/**
 * @template {DeltaConf} D1
 * @template D2
 * @typedef {(import('../ts.js').TypeIsAny<D1, any, PrettifyDeltaConf<{[K in (keyof D1|keyof D2)]: K extends keyof D2 ? D2[K] : (K extends keyof D1 ? D1[K] : never)}>> & {}) extends infer DC extends DeltaConf ? DC : never} DeltaConfOverwrite
 */

/**
 * @template {string} Name
 * @template {{[K in string|number]:any}} Attrs
 * @template Children
 * @template {boolean} Text
 */
class DeltaData {
  /**
   * @param {string?} name
   * @param {s.Schema<Delta<any>>?} $schema
   */
  constructor (name, $schema) {
    this.name = /** @type {Name} */ (name)
    this.$schema = $schema
    /**
     * @type {{ [K in keyof Attrs]?: K extends string|number ? (SetAttrOp<Attrs[K],K>|DeleteAttrOp<Attrs[K],K>|(Attrs[K] extends never ? never : (Delta<any> extends Attrs[K] ? ModifyAttrOp<Extract<Attrs[K],Delta<any>>,K> : never))) : never }
     *       & { [Symbol.iterator]: () => Iterator<{ [K in keyof Attrs]: K extends string|number ? (SetAttrOp<Attrs[K],K>|DeleteAttrOp<Attrs[K],K>|(Attrs[K] extends never ? never : (Delta<any> extends Attrs[K] ? ModifyAttrOp<Extract<Attrs[K],Delta<any>>,K> : never))) : never }[keyof Attrs]> }
     * }
     */
    this.attrs = /** @type {any} */ ({
      * [Symbol.iterator] () {
        for (const k in this) {
          yield this[k]
        }
      }
    })

    /**
     * @type {list.List<
     *   | (Text extends true ? (RetainOp|TextOp|DeleteOp<any>) : never)
     *   | (RetainOp|InsertOp<Children>|DeleteOp<any>|(Delta<any> extends Children ? ModifyOp<Extract<Children,Delta<any>>> : never))
     * >}
     */
    this.children = /** @type {any} */ (list.create())
    /**
     * All child-ops sizes combined. Note that delete-ops also have a length
     */
    this.childCnt = 0
    /**
     * @type {any}
     */
    this.origin = null
    /**
     * @type {string|null}
     */
    this._fingerprint = null
    this.isDone = false
    /**
     * Is the final document and does not contain delete, modify, or retain ops.
     */
    this.isFinal = false
    /**
     * Leaf {@link Mark marks} whose cursor sits in THIS node (in a settled delta), or the marks to ADD
     * to the target node (in a change delta). Local/ephemeral cursor state — NOT part of the document
     * fingerprint or equality. `null` when there are none. See {@link Marks}.
     *
     * @type {Marks?}
     */
    this.marks = null
    /**
     * Mark ids to DELETE (tombstones). Only present in a (non-settled) change delta — like a
     * `DeleteAttrOp`, a deletion is dropped from a final delta (a materialized document carries none, at
     * any depth). `null` when there are none. Marks follow *last-writer-wins*, treating each op as an
     * absolute per-id assignment (present vs absent): adding a mark cancels a pending delete of the same
     * id ({@link addMarkTo}) and a delete cancels a present add ({@link deleteMarkTo}), so a node never
     * holds both an add and a delete for one id. {@link deleteMarkTo} is the sole writer of this set
     * (and `addMarkTo`'s mirror); see {@link applyMarkOps}.
     *
     * @type {Set<string>?}
     */
    this.deleteMarks = null
    /**
     * Conservative "this subtree might hold a {@link Mark}" flag (own {@link DeltaData#marks} or any
     * descendant's). Set `true` when a mark is introduced and OR-propagated up to every ancestor; it is
     * **never** decremented (removal leaves it conservatively `true`). {@link
     * import('./position.js').marksToPositions} is the sole resetter: it prunes subtrees where this is
     * `false` (a guaranteed-empty subtree) and lazily clears a stale `true` to `false` when a descended
     * subtree turns out to hold none. `false` therefore *guarantees* no marks; `true` only *maybe*.
     */
    this.maybeHasMarks = false
  }
}

/**
 * @template {DeltaConf} [Conf={}]
 * @extends {DeltaData<
 *   DeltaConfGetName<Conf>,
 *   DeltaConfGetAttrs<Conf>,
 *   DeltaConfGetChildren<Conf>,
 *   Conf extends {text:true} ? true : false
 * >}
 */
export class Delta extends DeltaData {
  /**
   * @type {string}
   */
  get fingerprint () {
    return this._fingerprint || (this._fingerprint = buffer.toBase64(rabin.fingerprint(rabin.StandardIrreducible32, encoding.encode(encoder => {
      encoding.writeUint32(encoder, 0xf2ae5680) // "magic number" that ensures that different types of content don't yield the same fingerprint
      encoding.writeAny(encoder, this.name)
      /**
       * @type {Array<number|string>}
       */
      const keys = []
      for (const attr of this.attrs) {
        keys.push(attr.key)
      }
      keys.sort((a, b) => {
        const aIsString = s.$string.check(a)
        const bIsString = s.$string.check(b)
        // numbers first
        // in ascending order
        return (aIsString && bIsString)
          ? a.localeCompare(b)
          : (aIsString ? 1 : (bIsString ? -1 : (a - b)))
      })
      encoding.writeVarUint(encoder, keys.length)
      for (const key of keys) {
        encoding.writeVarString(encoder, /** @type {any} */ (this.attrs[/** @type {keyof typeof this.attrs} */ (key)]).fingerprint)
      }
      // trailing plain retains are not content: `done()` trims them, and the fingerprint must not
      // change when it does — so only the children up to the last content op are fingerprinted
      const cs = this.children
      let n = _contentLen(cs)
      encoding.writeVarUint(encoder, n)
      // non-null for the first `n` ops by construction (see _contentLen)
      for (let child = /** @type {ChildrenOpAny} */ (cs.start); n > 0; n--, child = /** @type {ChildrenOpAny} */ (child.next)) {
        encoding.writeVarString(encoder, child.fingerprint)
      }
    }))))
  }

  [fingerprintTrait.FingerprintTraitSymbol] () {
    return this.fingerprint
  }

  isEmpty () {
    // a children list holding only plain retains is empty (positional only — see _isPlainRetain)
    return object.isEmpty(this.attrs) && _contentLen(this.children) === 0 && (this.marks === null || this.marks.size === 0) && (this.deleteMarks === null || this.deleteMarks.size === 0)
  }

  /**
   * @return {DeltaJSON}
   */
  toJSON () {
    const name = this.name
    /**
     * @type {any}
     */
    const attrs = {}
    /**
     * @type {any}
     */
    const children = []
    for (const attr of this.attrs) {
      attrs[attr.key] = attr.toJSON()
    }
    this.children.forEach(val => {
      children.push(val.toJSON())
    })
    return object.assign(
      { type: /** @type {'delta'} */ ('delta') },
      (name != null ? { name } : {}),
      (object.isEmpty(attrs) ? {} : { attrs }),
      (children.length > 0 ? { children } : {}),
      (this.marks !== null && this.marks.size > 0 ? { marks: [...this.marks].sort(compareMarksById).map(m => m.toJSON()) } : {}),
      (this.deleteMarks !== null && this.deleteMarks.size > 0 ? { deleteMarks: [...this.deleteMarks].sort() } : {})
    )
  }

  /**
   * @param {Delta<any>} other
   * @return {boolean}
   */
  equals (other) {
    return this[equalityTrait.EqualityTraitSymbol](other)
  }

  /**
   * @param {any} other
   * @return {boolean}
   */
  [equalityTrait.EqualityTraitSymbol] (other) {
    // marks are local/ephemeral cursor state and intentionally NOT part of document identity
    if (this.name !== other.name || !fun.equalityDeep(this.attrs, other.attrs)) return false
    // only the content ops count (a trailing plain-retain run is not content — see _isPlainRetain);
    // comparing the counts first also makes two retain-only lists equal. `childCnt` (Σ op.length)
    // needs no separate check: every content op's equality covers its length-bearing field.
    const n = _contentLen(this.children)
    if (n !== _contentLen(other.children)) return false
    // non-null for the first `n` ops by construction (see _contentLen)
    let a = /** @type {ChildrenOpAny} */ (this.children.start)
    let b = /** @type {ChildrenOpAny} */ (other.children.start)
    for (let i = 0; i < n; i++, a = /** @type {ChildrenOpAny} */ (a.next), b = /** @type {ChildrenOpAny} */ (b.next)) {
      if (!fun.equalityDeep(a, b)) return false
    }
    return true
  }

  // toString () {
  //   /**
  //    * @type {Array<[string|number,string]>}
  //    */
  //   const attrs = []
  //   for (const attr of this.attrs) {
  //     attrs.push([attr.key, $deleteAttrOp.check(attr) ? 'delete' : attr.type + ' ' + (attr.value?.toString() ?? attr.value)])
  //   }
  //   attrs.sort((a, b) => a[0].toString() < b[0].toString() ? -1 : 1)
  //   const attrsString = attrs.map(attr => `${attr[0]} = ${attr[1]}`).join(', ')
  //   if (this.childCnt === 0) return `<${this.name ?? ''} ${attrsString}/>`
  //   this.children.toArray() drnt
  //   return '<>'
  // }
  //
  /**
   * Mark this delta as done and perform some cleanup (e.g. remove appended retains without
   * formats&attributions). In the future, there might be additional merge operations that can be
   * performed to result in smaller deltas. Set `markAsDone=false` to only perform the cleanup.
   *
   * @return {Delta<Conf>}
   */
  done (markAsDone = true) {
    if (!this.isDone) {
      this.isDone = markAsDone
      // trim the trailing plain-retain run — fingerprint-neutral, as fingerprint/equality never counted
      // it (see _contentLen); any other cleanup added here must null `_fingerprint`
      const cs = this.children
      for (let end = cs.end; end !== null && _isPlainRetain(end); end = cs.end) {
        this.childCnt -= end.length
        list.popEnd(cs)
      }
    }
    return this
  }
}

/**
 * @template {DeltaConf} Conf
 * @param {Delta<Conf>} d
 * @param {number} start
 * @param {number} end
 * @param {ChildrenOpAny?} currNode - start slicing at this node (instead of d.children.start)
 * @return {DeltaBuilder<Conf>}
 */
export const slice = (d, start = 0, end = d.childCnt, currNode = d.children.start) => {
  const cpy = /** @type {DeltaAny} */ (new DeltaBuilder(d.name, d.$schema))
  cpy.origin = d.origin
  const sliceStart = start // `start` is mutated by the walk below; keep the original for clamping marks
  // copy attrs
  for (const op of d.attrs) {
    // @ts-ignore
    cpy.attrs[op.key] = /** @type {any} */ (op.clone())
  }
  // copy children
  const slicedLen = end - start
  let remainingLen = slicedLen
  let currNodeOffset = 0
  while (start > 0 && currNode != null) {
    if (currNode.length <= start) {
      start -= currNode.length
      currNode = currNode.next
    } else {
      currNodeOffset = start
      start = 0
    }
  }
  if (currNodeOffset > 0 && currNode) {
    const ncpy = currNode.clone(currNodeOffset, currNodeOffset + math.min(remainingLen, currNode.length - currNodeOffset))
    list.pushEnd(cpy.children, ncpy)
    remainingLen -= ncpy.length
    currNode = currNode.next
  }
  while (currNode != null && currNode.length <= remainingLen) {
    list.pushEnd(cpy.children, currNode.clone())
    remainingLen -= currNode.length
    currNode = currNode.next
  }
  if (currNode != null && remainingLen > 0) {
    list.pushEnd(cpy.children, currNode.clone(0, remainingLen))
    remainingLen -= math.min(currNode.length, remainingLen)
  }
  cpy.childCnt = slicedLen - remainingLen
  // copy marks (only if the subtree maybe has any). A full clone (the `clone` path) copies this node's
  // own marks verbatim — a change delta's number-keyed root marks point into the TARGET's coordinate
  // and may sit beyond the change's own (often empty) content, so they must not be clamped. A genuine
  // partial slice instead clamps number-keyed marks to the sliced range (rebasing `key -= start`).
  // String/attr marks ride with the copied attrs; child marks ride on the cloned children. The
  // `maybeHasMarks` flag is copied (conservative); a change-only deleteMarks list is copied wholesale.
  if (d.maybeHasMarks) {
    if (d.marks !== null) {
      const fullCopy = sliceStart === 0 && end >= d.childCnt
      const cpyMarks = new Marks()
      for (const m of d.marks) {
        if (fullCopy || typeof m.key !== 'number') {
          cpyMarks.add(m)
        } else if (m.key >= sliceStart && m.key <= end) {
          cpyMarks.add(m.copy(m.key - sliceStart))
        }
      }
      if (cpyMarks.size > 0) cpy.marks = cpyMarks
    }
    cpy.maybeHasMarks = true
  }
  // a bulk copy of an already-disjoint source onto a fresh `cpy` (not a conflict-resolving write, so it
  // bypasses the deleteMarkTo/addMarkTo primitives): `d`'s marks/deleteMarks are disjoint by id, and
  // clamping only ever drops marks, so `cpy` stays disjoint
  if (d.deleteMarks !== null) cpy.deleteMarks = new Set(d.deleteMarks)
  // @ts-ignore
  return cpy
}

/**
 * @template {DeltaAny} D
 * @param {D} d
 * @return {D extends Delta<infer Conf> ? DeltaBuilder<Conf> : never}
 */
export const clone = d => /** @type {any} */ (slice(d, 0, d.childCnt))

/**
 * Deep-clone a value that may be a nested delta: a fresh {@link cloneDeep} of it when it is a delta,
 * otherwise the value unchanged (plain JSON content is treated as immutable and shared).
 *
 * @param {any} v
 * @return {any}
 */
const _cloneMaybeDeltaDeep = v => $deltaAny.check(v) ? _cloneDeepNested(v) : v

/**
 * Carry `src`'s cached fingerprint over to `cpy`, a content-identical copy of it. Only valid when
 * nothing about the copy differs from the source in fingerprinted fields (a full-range clone, the same
 * key, nested values cloned rather than `done()`-trimmed).
 *
 * @template {{ _fingerprint: string|null }} T
 * @param {{ _fingerprint: string|null }} src
 * @param {T} cpy
 * @return {T}
 */
const _copyFp = (src, cpy) => {
  cpy._fingerprint = src._fingerprint
  return cpy
}

/**
 * {@link cloneDeep} for a *nested* delta: content-identical, so the cached fingerprint rides along
 * (only the root returned by the exported `cloneDeep` starts without one).
 *
 * @template {DeltaConf} Conf
 * @param {Delta<Conf>} d
 * @return {DeltaBuilder<Conf>}
 */
const _cloneDeepNested = d => _copyFp(d, cloneDeep(d))

/**
 * Deep-clone one content (child) op for {@link cloneDeep}: a fresh op whose nested deltas (an insert's
 * delta content, a modify's value) are themselves deep-cloned. `format`/`attribution` objects are
 * retained (shared) — they are always copied before being mutated (see {@link cloneDeep}). Ops without a
 * nested delta (`text`/`retain`/`delete`) use `op.clone()`. Every copy is content-identical, so the
 * cached fingerprint is carried over.
 *
 * @param {ChildrenOpAny} op
 * @return {ChildrenOpAny}
 */
const _cloneChildOpDeep = op =>
  $insertOp.check(op)
    ? _copyFp(op, new InsertOp(op.insert.map(_cloneMaybeDeltaDeep), op.format, op.attribution))
    : ($modifyOp.check(op)
        ? _copyFp(op, new ModifyOp(_cloneDeepNested(op.value), op.format, op.attribution))
        : op.clone())

/**
 * Deep-clone one attribute op for {@link cloneDeep} — the attribute-dimension mirror of
 * {@link _cloneChildOpDeep}: a `setAttr`'s value and a `modifyAttr`'s value may be nested
 * deltas and are deep-cloned.
 *
 * @param {AttrOpAny} op
 * @return {AttrOpAny}
 */
const _cloneAttrOpDeep = op =>
  $setAttrOp.check(op)
    ? _copyFp(op, new SetAttrOp(op.key, _cloneMaybeDeltaDeep(op.value), op.attribution))
    : ($modifyAttrOp.check(op)
        ? _copyFp(op, new ModifyAttrOp(op.key, _cloneDeepNested(op.value), op.attribution))
        // the only remaining attr op is a deleteAttr
        : _copyFp(op, new DeleteAttrOp(op.key, op.attribution)))

/**
 * A **deep** clone of `d`: like {@link clone}, but every nested delta — an insert's delta content, a
 * `modify`/`modifyAttr` value, a delta-valued attribute — is itself recursively cloned into a fresh,
 * **mutable** node, instead of being frozen (`done`) and shared as {@link clone} does. The result and
 * its whole subtree are therefore independently editable. Cached fingerprints of the ops and nested
 * deltas ride along (they are content-identical); only the returned root starts without one.
 *
 * ## What is cloned, and when to reach for this
 *
 * A delta owns two kinds of nested state, treated differently on purpose:
 * - **Ops and (nested) deltas are cloned.** They carry structure edited *in place* — `apply`/`rebase`
 *   splice op lists and mutate op fields — so a shared copy would be corrupted by a later edit of either
 *   side.
 * - **`format`/`attribution` objects and plain JSON insert values are retained (shared).** They are
 *   always *copied before being changed* (a format/attribution update allocates a new object — see
 *   {@link mergeAttr}), so sharing them is safe and avoids needless allocation.
 *
 * {@link clone} suffices for the common case because a delta is only ever edited through `apply`/`rebase`,
 * which re-clone a `done` nested delta the first time they touch it (see {@link InsertOp#_modValue}) — the
 * shared, frozen children are never mutated in place. Reach for `cloneDeep` when a nested delta will be
 * **manipulated directly** instead of via `apply`/`rebase` — most notably by a
 * {@link import('./transformer/core.js').Transformer transformer}, which walks and mutates a delta's
 * ops/children in place. A `done` (frozen) delta must always be cloned before such direct manipulation
 * (a plain `apply`/`rebase` handles the freeze itself, so it needs no pre-clone).
 *
 * @template {DeltaConf} Conf
 * @param {Delta<Conf>} d
 * @return {DeltaBuilder<Conf>}
 */
export const cloneDeep = d => {
  const cpy = /** @type {DeltaAny} */ (new DeltaBuilder(d.name, d.$schema))
  cpy.origin = d.origin
  cpy.isFinal = d.isFinal
  for (const op of d.attrs) {
    // @ts-ignore dynamic attr key (the same limitation `slice` suppresses when copying attrs)
    cpy.attrs[op.key] = _cloneAttrOpDeep(op)
  }
  for (const op of d.children) {
    list.pushEnd(cpy.children, _cloneChildOpDeep(op))
  }
  cpy.childCnt = d.childCnt
  // marks ride along verbatim — a full clone never clamps number-keyed root marks, and Mark objects are
  // immutable so they are shared (mirrors slice's full-copy path); a change delta's deleteMarks is copied.
  if (d.maybeHasMarks) {
    if (d.marks !== null) {
      const cpyMarks = new Marks()
      for (const m of d.marks) cpyMarks.add(m)
      if (cpyMarks.size > 0) cpy.marks = cpyMarks
    }
    cpy.maybeHasMarks = true
  }
  if (d.deleteMarks !== null) cpy.deleteMarks = new Set(d.deleteMarks)
  return /** @type {any} */ (cpy)
}

/**
 * A *shallow* clone of `d`: a fresh {@link DeltaBuilder} carrying `d`'s name, attribute ops, and own
 * (root) marks — but **no children**. Content-transforming
 * {@link import('./transformer/core.js').Transformer transformers} (e.g. `children`, `value`) rebuild
 * the child list themselves yet must keep this node's own marks; the marks of nested children ride
 * along on the children the caller rebuilds. The `maybeHasMarks` flag is copied from `d` (conservative)
 * and the builder OR-propagates the flag as the caller appends children. Because mark-carrying lives
 * here in the shared primitive, a transformer that builds its output via {@link cloneShallow} cannot
 * silently drop the node's marks.
 *
 * @template {DeltaAny} D
 * @param {D} d
 * @return {D extends Delta<infer Conf> ? DeltaBuilder<Conf> : never}
 * @internal transformer plumbing — drops children; consumers use {@link clone}.
 */
export const cloneShallow = d => {
  const cpy = /** @type {DeltaAny} */ (new DeltaBuilder(d.name, d.$schema))
  cpy.origin = d.origin
  for (const op of d.attrs) {
    // @ts-ignore (dynamic attr key — the same limitation slice suppresses when copying attrs)
    cpy.attrs[op.key] = /** @type {any} */ (op.clone())
  }
  // root marks / deleteMarks (a delete-mark-only change has no marks but must still ride); `cpy` is
  // fresh, so merging onto it is a plain copy
  if (d.marks !== null || d.deleteMarks !== null) mergeRootMarks(cpy, d)
  // carry the conservative flag (covers attr-value marks even when this node has no root marks of its
  // own); the builder OR-propagates it further as the caller appends the rebuilt children
  cpy.maybeHasMarks = d.maybeHasMarks
  // @ts-ignore
  return cpy
}

/**
 * Make an op's modify `value` mutable for in-place editing: clear the op's cached fingerprint and, when
 * the value is a `done` delta, replace it with a mutable clone. Shared by the `_modValue` getters of
 * {@link ModifyOp}, {@link ModifyAttrOp}, and {@link SetAttrOp}; the `$deltaAny` guard lets a scalar
 * `SetAttrOp` value pass through untouched.
 *
 * @param {{ value: any, _fingerprint: string|null }} op
 * @return {any}
 */
const modValue = op => {
  op._fingerprint = null
  const v = op.value
  if ($deltaAny.check(v) && v.isDone) op.value = clone(v)
  return op.value
}

/*
 * --- @internal low-level mutation API for a delta's children list ---
 *
 * These are the ONLY sanctioned way to structurally edit `d.children` in place. Each keeps the cached
 * invariant `d.childCnt === Σ op.length` correct and resets the op's `_fingerprint`. Never mutate
 * `op.retain` / `op.delete` / `op.insert`, call `list.*`, or touch `d.childCnt` directly from outside
 * delta.js — go through these so the count (which `slice`/`apply` position by) never drifts.
 *
 * Cursor pattern (used by {@link DeltaBuilder#apply}, {@link DeltaBuilder#rebase}, and
 * `transformer/conform.js`): walk the target with `(op, offset)`; {@link _splitChildAt} before inserting
 * in the middle of an op so the cursor sits on a boundary; {@link _mergeChildWithPrev} after edits to
 * coalesce adjacent same-kind runs. `_growRun` / `_spliceInsert` extend an existing run *without* a split,
 * which keeps same-kind insertion (e.g. typing into a pass-through run) non-fragmenting.
 */

/**
 * Coalesce `op` into its previous sibling when they are the same kind with equal format/attribution,
 * removing `op`. `d.childCnt` is unchanged (the length moves to the previous op). Returns `true` when it
 * merged — so a caller holding a cursor on `op` can move it onto the surviving previous op.
 *
 * @param {DeltaBuilderAny} d
 * @param {InsertOp<any>|RetainOp|DeleteOp<any>|TextOp|ModifyOp<any>} op
 * @return {boolean}
 */
export const _mergeChildWithPrev = (d, op) => {
  const prevOp = op.prev
  if (
    prevOp?.constructor !== op.constructor ||
    $modifyOp.check(op) ||
    (
      !$deleteOp.check(op) && (
        !fun.equalityDeep(op.format, /** @type {InsertOp<any>} */ (prevOp).format) ||
        !fun.equalityDeep(op.attribution, /** @type {InsertOp<any>} */ (prevOp).attribution)
      )
    )
  ) {
    // constructor mismatch or format/attribution mismatch
    return false
  }
  // can be merged
  if ($insertOp.check(op)) {
    /** @type {InsertOp<any>} */ (prevOp).insert.push(...op.insert)
  } else if ($retainOp.check(op)) {
    // @ts-ignore
    /** @type {RetainOp} */ (prevOp).retain += op.retain
  } else if ($deleteOp.check(op)) {
    /** @type {DeleteOp<any>} */ (prevOp).delete += op.delete
  } else if ($textOp.check(op)) {
    // @ts-ignore
    /** @type {TextOp} */ (prevOp).insert += op.insert
  /* c8 ignore start */
  } else {
    // unreachable: the constructor check at the top of the function already
    // limits `op` to one of the four kinds tested above
    error.unexpectedCase()
  }
  prevOp._fingerprint = null
  /* c8 ignore stop */
  list.remove(d.children, op)
  return true
}

/**
 * Split `op` at `offset`, leaving `op` = `[0, offset)` and a fresh right node `[offset, op.length)`
 * inserted immediately after it; returns the right node. `d.childCnt` is unchanged (net-zero). The clone
 * uses `markAsDone=false` because the right node is `_splice`d out of `op` here, so it is the sole owner.
 * Caller guarantees `0 < offset < op.length`.
 *
 * @param {DeltaBuilderAny} d
 * @param {ChildrenOpAny} op
 * @param {number} offset
 * @return {ChildrenOpAny}
 */
export const _splitChildAt = (d, op, offset) => {
  const right = /** @type {ChildrenOpAny} */ (op.clone(offset, op.length, false))
  op._splice(offset, op.length - offset)
  list.insertBetween(d.children, op, op.next || null, right)
  return right
}

/**
 * Insert `op` into `d.children` immediately before `ref` (or push at the end when `ref == null`);
 * `d.childCnt += op.length`. Does not coalesce — call {@link _mergeChildWithPrev} afterwards if needed.
 *
 * @param {DeltaBuilderAny} d
 * @param {ChildrenOpAny?} ref
 * @param {ChildrenOpAny} op
 * @return {ChildrenOpAny}
 */
export const _insertChild = (d, ref, op) => {
  list.insertBetween(d.children, ref == null ? d.children.end : ref.prev, ref, op)
  d.childCnt += op.length
  return op
}

/**
 * Remove child `op` entirely; `d.childCnt -= op.length`.
 *
 * @param {DeltaBuilderAny} d
 * @param {ChildrenOpAny} op
 */
export const _removeChild = (d, op) => {
  d.childCnt -= op.length
  list.remove(d.children, op)
}

/**
 * Drop `len` positions from `op` starting at `offset` (`op._splice`); `d.childCnt -= len`. Does not remove
 * an emptied op — the caller decides (call {@link _removeChild} when `op.length === 0`), because removal is
 * usually entangled with the caller's own cursor advance. Retain/Delete ignore `offset` (uniform runs);
 * Insert drops `insert[offset, offset+len)`.
 *
 * @param {DeltaBuilderAny} d
 * @param {ChildrenOpAny} op
 * @param {number} offset
 * @param {number} len
 */
export const _shrinkChild = (d, op, offset, len) => {
  op._splice(offset, len)
  d.childCnt -= len
}

/**
 * Grow a Retain or Delete run `op` by `n` positions in place (the invariant-safe form of `op.retain += n`);
 * `d.childCnt += n`. Use to extend an existing same-kind run without a split.
 *
 * @param {DeltaBuilderAny} d
 * @param {RetainOp|DeleteOp<any>} op
 * @param {number} n
 */
export const _growRun = (d, op, n) => {
  // reason: RetainOp.retain / DeleteOp.delete are @readonly for consumers; the @internal mutators grow them in place, just as _splice shrinks them
  // @ts-ignore
  if ($retainOp.check(op)) op.retain += n; else op.delete += n
  op._fingerprint = null
  d.childCnt += n
}

/**
 * Splice `elems` into an Insert run `op` at `offset` in place; `d.childCnt += elems.length`.
 *
 * @param {DeltaBuilderAny} d
 * @param {InsertOp<any>} op
 * @param {number} offset
 * @param {Array<any>} elems
 */
export const _spliceInsert = (d, op, offset, elems) => {
  op.insert.splice(offset, 0, ...elems)
  op._fingerprint = null
  d.childCnt += elems.length
}

/**
 * Ensures that the delta can be edited. clears _fingerprint cache.
 *
 * @param {DeltaBuilderAny} d
 */
const modDeltaCheck = d => {
  if (d.isDone) {
    /**
     * You tried to modify a delta after it has been marked as "done".
     */
    throw error.create("Readonly Delta can't be modified")
  }
  d._fingerprint = null
}

/**
 * @template {DeltaConf} [Conf={}]
 * @template {boolean} [FixedConf=false]
 * @extends {Delta<Conf>}
 */
export class DeltaBuilder extends Delta {
  /**
   * @param {string?} name
   * @param {s.Schema<Delta<Conf>>?} $schema
   */
  constructor (name, $schema) {
    super(name, $schema)
    /**
     * @type {Formats?}
     */
    this._usedFormats = null
    /**
     * @type {Attribution?}
     */
    this._usedAttribution = null
    /**
     * Resolved data-companion cache for the `used*` contexts ({@link resolveUsedData}): what data
     * ops inherit instead of the raw context. `undefined` = stale, recomputed on the next data-op
     * consumption ({@link DeltaBuilder#_getUsedFormatsData}) — {@link DeltaBuilder#useFormats} & co
     * clear it whenever they swap the context (which is why the public slots are read-only).
     *
     * @type {Formats|null|undefined}
     */
    this._usedFormatsData = undefined
    /**
     * @type {Attribution|null|undefined}
     */
    this._usedAttributionData = undefined
  }

  /**
   * The ambient formats context inherited by subsequent ops. Read-only: set it via
   * {@link DeltaBuilder#useFormats} / {@link DeltaBuilder#updateUsedFormats}, which also invalidate
   * the cached data companion — a direct overwrite would leave it stale.
   *
   * @return {Formats?}
   */
  get usedFormats () { return this._usedFormats }

  /**
   * The ambient attribution context inherited by subsequent ops. Read-only — see
   * {@link DeltaBuilder#usedFormats}; set it via {@link DeltaBuilder#useAttribution} /
   * {@link DeltaBuilder#updateUsedAttribution}.
   *
   * @return {Attribution?}
   */
  get usedAttribution () { return this._usedAttribution }

  /**
   * Set the ambient attribution context inherited by subsequent ops (`undefined`/empty dim args).
   * The object is interned — instruction ops store it, and data ops store its resolved companion
   * ({@link resolveUsedData}), BY REFERENCE. Never mutate it after setting; pass a fresh object
   * instead (same invariant as the objects stored on ops).
   *
   * @param {Attribution?} attribution
   */
  useAttribution (attribution) {
    modDeltaCheck(this)
    if (this._usedAttribution !== attribution) {
      this._usedAttribution = attribution
      this._usedAttributionData = undefined // clear the data companion — recomputed on next consumption
    }
    return this
  }

  /**
   * Set the ambient formats context inherited by subsequent ops (`undefined`/empty dim args). The
   * object is interned — see {@link DeltaBuilder#useAttribution}: never mutate it after setting.
   *
   * @param {Formats?} attributes
   * @return {this}
   */
  useFormats (attributes) {
    modDeltaCheck(this)
    if (this._usedFormats !== attributes) {
      this._usedFormats = attributes
      this._usedFormatsData = undefined // clear the data companion — recomputed on next consumption
    }
    return this
  }

  /**
   * The `usedFormats` context resolved for data-op consumption ({@link resolveUsedData}): the slot
   * object itself when it carries no `{k:null}` removals, one shared stripped copy otherwise.
   * Cached until the context is swapped — no per-op traversal.
   *
   * @return {Formats?}
   */
  _getUsedFormatsData () {
    const d = this._usedFormatsData
    return d !== undefined ? d : (this._usedFormatsData = /** @type {Formats?} */ (resolveUsedData(this._usedFormats, false)))
  }

  /**
   * See {@link DeltaBuilder#_getUsedFormatsData} — the `usedAttribution` analogue (its `format` key
   * resolves one level deeper).
   *
   * @return {Attribution?}
   */
  _getUsedAttributionData () {
    const d = this._usedAttributionData
    return d !== undefined ? d : (this._usedAttributionData = /** @type {Attribution?} */ (resolveUsedData(this._usedAttribution, true)))
  }

  /**
   * @param {string} name
   * @param {any} value
   */
  updateUsedFormats (name, value) {
    modDeltaCheck(this)
    if (value == null) {
      this._usedFormats = object.assign({}, this._usedFormats)
      delete this._usedFormats?.[name]
      if (object.isEmpty(this._usedFormats)) {
        this._usedFormats = null
      }
      this._usedFormatsData = undefined // clear the data companion — recomputed on next consumption
    } else if (!fun.equalityDeep(this._usedFormats?.[name], value)) {
      this._usedFormats = object.assign({}, this._usedFormats)
      this._usedFormats[name] = value
      this._usedFormatsData = undefined // clear the data companion — recomputed on next consumption
    }
    return this
  }

  /**
   * @template {keyof Attribution} NAME
   * @param {NAME} name
   * @param {Attribution[NAME]?} value
   */
  updateUsedAttribution (name, value) {
    modDeltaCheck(this)
    if (value == null) {
      this._usedAttribution = object.assign({}, this._usedAttribution)
      delete this._usedAttribution?.[name]
      if (object.isEmpty(this._usedAttribution)) {
        this._usedAttribution = null
      }
      this._usedAttributionData = undefined // clear the data companion — recomputed on next consumption
    } else if (!fun.equalityDeep(this._usedAttribution?.[name], value)) {
      this._usedAttribution = object.assign({}, this._usedAttribution)
      this._usedAttribution[name] = value
      this._usedAttributionData = undefined // clear the data companion — recomputed on next consumption
    }
    return this
  }

  /**
   * @template {(FixedConf extends true ? never : (Array<any>|string)) | (DeltaConfGetChildren<Conf> extends infer Children ? (Children extends never ? never : Array<Children>) : never) | DeltaConfGetText<Conf>} NewContent
   * @param {NewContent} insert
   * @param {Formats?} [formatting]
   * @param {Attribution?} [attribution]
   * @return {DeltaBuilder<FixedConf extends true ? Conf : DeltaConfOverwrite<Conf,
   * (Exclude<NewContent,string> extends never ? {} : {
   *   children: Exclude<NewContent,string>[number]|DeltaConfGetChildren<Conf>
   * }) & (Extract<NewContent,string> extends never ? {} : { text: true })>, FixedConf>}
   */
  insert (insert, formatting, attribution) {
    modDeltaCheck(this)
    const mergedFormats = combineData(this._getUsedFormatsData(), formatting, false)
    const mergedAttribution = combineData(this._getUsedAttributionData(), attribution, true)
    /**
     * @param {TextOp | InsertOp<any>} lastOp
     */
    const checkMergedEquals = lastOp => (mergedFormats === lastOp.format || fun.equalityDeep(mergedFormats, lastOp.format)) && (mergedAttribution === lastOp.attribution || fun.equalityDeep(mergedAttribution, lastOp.attribution))
    const end = this.children.end
    if (s.$string.check(insert)) {
      if ($textOp.check(end) && checkMergedEquals(end)) {
        // @ts-ignore
        end.insert += insert
        end._fingerprint = null
      } else if (insert.length > 0) {
        list.pushEnd(this.children, new TextOp(insert, mergedFormats, mergedAttribution))
      }
      this.childCnt += insert.length
    } else if (arr.isArray(insert)) {
      if ($insertOp.check(end) && checkMergedEquals(end)) {
        // @ts-ignore
        end.insert.push(...insert)
        end._fingerprint = null
      } else if (insert.length > 0) {
        list.pushEnd(this.children, new InsertOp(insert.slice() /* ensures that we don't reuse an existing array */, mergedFormats, mergedAttribution))
      }
      this.childCnt += insert.length
      // inserting pre-marked content brings its subtree marks along (as apply's insert path does)
      this.maybeHasMarks ||= elemsMaybeHaveMarks(insert)
    }
    return /** @type {any} */ (this)
  }

  /**
   * @template {Extract<DeltaConfGetAllowedChildren<Conf, FixedConf>,Delta<any>|DeltaData<any,any,any,any>|DeltaBuilder<any>>} NewContent
   * @param {NewContent} modify
   * @param {Formats?} [formatting] tri-state: omit/`undefined` skip, `null` clear, `{k:v}`/`{k:null}` set/remove
   * @param {Attribution?} [attribution] tri-state: omit/`undefined` skip, `null` clear, `{k:v}`/`{k:null}` set/remove
   * @return {DeltaBuilder<DeltaConfOverwrite<Conf, {children: DeltaConfGetChildren<Conf>|NewContent}>, FixedConf>}
   */
  modify (modify, formatting, attribution) {
    modDeltaCheck(this)
    const mergedFormats = combineInstr(this.usedFormats, formatting, false)
    const mergedAttribution = combineInstr(this.usedAttribution, attribution, true)
    list.pushEnd(this.children, new ModifyOp(modify, mergedFormats, mergedAttribution))
    this.childCnt += 1
    // the modify target carries marks in its subtree (own root marks + nested) - flag conservatively
    this.maybeHasMarks ||= modify.maybeHasMarks
    return /** @type {any} */ (this)
  }

  /**
   * Add a cursor/selection {@link Mark} at `pos` — a {@link import('./position.js').Pos}: a `path` of
   * content-index / attribute-key steps plus an `assoc` and optional immutable `attrs` (user data
   * carried with the mark). Returns `this` for chaining (like every other builder method). Pass an
   * explicit `id` to reference the mark later — re-adding the same `id` replaces it (e.g. to move a
   * cursor or update its `attrs`) and {@link DeltaBuilder#removeMark} deletes it; an anonymous one-shot
   * mark gets a fresh GUID otherwise.
   *
   * @param {import('./position.js').Pos} pos
   * @param {string} [id]
   * @param {boolean} [transient] flag the mark as never-to-be-stored (a one-shot mapping probe, see
   * {@link Mark#transient} and {@link import('./position.js').mapPositionsA})
   * @return {this}
   */
  addMark (pos, id = rand.uuidv4(), transient = false) {
    // apply with the default `final = this.isFinal` (do NOT force `{ final: true }`): a fresh change
    // builder is non-final so a sibling `removeMark` stays transmittable; a final doc collects nothing
    this.apply(/** @type {Delta<Conf>} */ (markChange(pos, id, false, transient)))
    return this
  }

  /**
   * Remove the mark with `id`, located via `pos` (the position it was added at). On a settled document
   * that holds the mark this removes it in place; on a fresh/markless builder it records a transmittable
   * `deleteMarks` change (symmetric to {@link DeltaBuilder#addMark}), so
   * `delta.create().removeMark(pos, id)` yields the delete-mark change to apply / rebase / transmit.
   *
   * @param {import('./position.js').Pos} pos
   * @param {string} id
   * @return {this}
   */
  removeMark (pos, id) {
    // apply with the default `final = this.isFinal` (do NOT force `{ final: true }`): on a non-final
    // change builder this records a transmittable `deleteMarks`; on a final doc it only removes in place
    this.apply(/** @type {Delta<Conf>} */ (markChange(pos, id, true)))
    return this
  }

  /**
   * @param {number} len
   * @param {Formats?} [format]
   * @param {Attribution?} [attribution]
   */
  retain (len, format, attribution) {
    modDeltaCheck(this) // this clears _fingerprint
    const mergedFormats = combineInstr(this.usedFormats, format, false)
    const mergedAttribution = combineInstr(this.usedAttribution, attribution, true)
    const lastOp = /** @type {RetainOp|InsertOp<any>} */ (this.children.end)
    if ($retainOp.check(lastOp) && fun.equalityDeep(mergedFormats, lastOp.format) && fun.equalityDeep(mergedAttribution, lastOp.attribution)) {
      // @ts-ignore
      lastOp.retain += len
      lastOp._fingerprint = null
    } else if (len > 0) {
      list.pushEnd(this.children, new RetainOp(len, mergedFormats, mergedAttribution))
    }
    this.childCnt += len
    return this
  }

  /**
   * @param {number} len
   */
  delete (len) {
    modDeltaCheck(this)
    const lastOp = /** @type {DeleteOp<any>|InsertOp<any>} */ (this.children.end)
    if ($deleteOp.check(lastOp)) {
      lastOp.delete += len
      lastOp._fingerprint = null
    } else if (len > 0) {
      list.pushEnd(this.children, new DeleteOp(len))
    }
    this.childCnt += len
    return this
  }

  /**
   * @template {Extract<keyof DeltaConfGetAllowedAttrs<Conf, FixedConf>,string|number>} Key
   * @template {DeltaConfGetAllowedAttrs<Conf, FixedConf>[Key]} Val
   * @param {Key} key
   * @param {Val} val
   * @param {Attribution?} [attribution] provenance for this attr (data: object or `null`/none)
   * @return {DeltaBuilder<DeltaConfOverwrite<Conf,{attrs:AddToAttrs<DeltaConfGetAttrs<Conf>,Key,Val>}>, FixedConf>}
   */
  setAttr (key, val, attribution) {
    modDeltaCheck(this)
    // @ts-ignore
    this.attrs[key] /** @type {any} */ =
      (new SetAttrOp(/** @type {any} */ (key), val, combineData(this._getUsedAttributionData(), attribution, true)))
    // a delta-valued attribute carries marks in its subtree - flag conservatively (never decremented)
    if ($deltaAny.check(val)) this.maybeHasMarks ||= val.maybeHasMarks
    return /** @type {any} */ (this)
  }

  /**
   * @template {DeltaConfGetAllowedAttrs<Conf, FixedConf>} NewAttrs
   * @param {NewAttrs} attrs
   * @param {Attribution?} [attribution] provenance applied to each set attr (data: object or `null`/none)
   * @return {DeltaBuilder<DeltaConfOverwrite<
   *   Conf,
   *   { attrs: MergeAttrs<DeltaConfGetAttrs<Conf>,NewAttrs> }
   *   >, FixedConf>
   * }
   */
  setAttrs (attrs, attribution) {
    modDeltaCheck(this)
    for (const k in attrs) {
      this.setAttr(/** @type {any} */ (k), /** @type {any} */ (attrs)[/** @type {any} */ (k)], attribution)
    }
    return /** @type {any} */ (this)
  }

  /**
   * @template {Extract<keyof DeltaConfGetAllowedAttrs<Conf, FixedConf>,string|number>} Key
   * @param {Key} key
   * @param {Attribution?} [attribution] provenance for the deletion (data: object or `null`/none)
   * @return {DeltaBuilder<DeltaConfOverwrite<Conf, {
   *   attrs: AddToAttrs<DeltaConfGetAttrs<Conf>,Key,never>
   * }>, FixedConf>}
   */
  deleteAttr (key, attribution) {
    modDeltaCheck(this)
    // dropping a delta-valued attribute drops its subtree's marks; the conservative `maybeHasMarks` flag
    // is left as-is (never decremented - marksToPositions self-corrects it)
    // @ts-ignore
    this.attrs[key] /** @type {any} */ =
      (new DeleteAttrOp(/** @type {any} */ (key), combineData(this._getUsedAttributionData(), attribution, true)))
    return /** @type {any} */ (this)
  }

  /**
   * @template {DeltaConfGetAllowedAttrs<Conf, FixedConf> extends infer As ? { [K in keyof As]: Extract<As[K],DeltaAny> extends never ? never : K }[keyof As] : never} Key
   * @template {Extract<DeltaConfGetAllowedAttrs<Conf, FixedConf>[Key],DeltaAny>} D
   * @param {Key} key
   * @param {D} modify
   * @param {Attribution?} [attribution] tri-state instruction merged onto the target attr op: omit/`undefined` skip, `null` clear, `{k:v}`/`{k:null}` set/remove
   * @return {DeltaBuilder<DeltaConfOverwrite<Conf,{attrs:AddToAttrs<DeltaConfGetAttrs<Conf>,Key,D>}>, FixedConf>}
   */
  modifyAttr (key, modify, attribution) {
    modDeltaCheck(this)
    const mergedAttribution = combineInstr(this.usedAttribution, attribution, true)
    this.attrs[key] = /** @type {any} */ (new ModifyAttrOp(key, modify, mergedAttribution))
    // the modify value carries marks in its subtree - flag conservatively (never decremented)
    this.maybeHasMarks ||= /** @type {DeltaAny} */ (modify).maybeHasMarks
    return /** @type {any} */ (this)
  }

  /**
   * Apply other delta on this op. The result is the merged op of both of them.
   *
   * a.apply(b.apply(c))
   *
   * is equivalent to
   *
   * a.apply(b).apply(c)
   *
   * If `final = true`, we consider this delta the final state and drop deleteAttrOps from
   * attributes. (E.g. if `otherOp` deletes an attribute, this op will simply not have the
   * attribute). Any kind of `delete` op might be considered a bug. A final delta is not idempotent.
   *
   * @param {Delta<Conf>?} other
   * @param {{ final?: boolean, move?: boolean }} [opts] `move`: the caller donates `other` (it is not
   * read again), so its content is consumed (shared mutable) rather than frozen-cloned — see {@link
   * InsertOp#clone}. UNSAFE if the caller keeps using `other`.
   * @return {DeltaBuilder<Conf>}
   */
  apply (other, { final = this.isFinal, move = false } = {}) {
    if (other == null) return this
    modDeltaCheck(this)
    this.$schema?.expect(other)
    // `move`: the caller donates `other` (it must not be read again), so content is consumed (shared
    // mutable) instead of frozen-cloned — no `done()` and no re-clone on a later modify. UNSAFE if the
    // caller keeps using `other`. `!move` is the default everywhere (today's behavior).
    const keep = !move
    // apply attrs
    for (const op of other.attrs) {
      // @ts-ignore
      const c = /** @type {SetAttrOp<any,any>|DeleteAttrOp<any>|ModifyAttrOp<any,any>} */ (this.attrs[op.key])
      if ($modifyAttrOp.check(op)) {
        if ($deltaAny.check(c?.value)) {
          const tgt = c._modValue
          tgt.apply(op.value, { final, move })
          this.maybeHasMarks ||= tgt.maybeHasMarks // flag the attr subtree's marks up
          // modifyAttr carries a tri-state attribution update for the target attr op (merge/clear/skip)
          if (applyDim(c, 'attribution', op.attribution)) {
            /** @type {any} */ (c)._fingerprint = null
          }
        } else {
          // then this is a simple modify (the attribute did not previously hold a delta)
          // @ts-ignore
          this.attrs[op.key] = move ? op : op.clone()
          this.maybeHasMarks ||= op.value.maybeHasMarks // flag any marks the new modify value carries
        }
      } else if ($setAttrOp.check(op)) {
        // a delta-valued attribute carries marks in its subtree - flag conservatively (never decremented)
        if ($deltaAny.check(op.value)) this.maybeHasMarks ||= op.value.maybeHasMarks
        // @ts-ignore
        this.attrs[op.key] = move ? op : op.clone()
      } else if ($deleteAttrOp.check(op)) {
        // removing a delta-valued attribute drops its subtree's marks; the conservative `maybeHasMarks`
        // flag is left as-is (never decremented - marksToPositions self-corrects it)
        if (final) {
          // @ts-ignore
          delete this.attrs[op.key]
        } else {
          // @ts-ignore
          this.attrs[op.key] = move ? op : op.clone()
        }
      }
    }
    // apply children
    /**
     * @type {ChildrenOpAny?}
     */
    let opsI = this.children.start
    if ($deleteOp.check(opsI)) {
      opsI = opNextUndeleted(opsI)
    }
    let offset = 0
    /**
     * At the end, we will try to merge this op, and op.next op with their respective previous op.
     *
     * Hence, anytime an op is cloned, deleted, or inserted (anytime list.* api is used) we must add
     * an op to maybeMergeable.
     *
     * @type {Array<InsertOp<any>|RetainOp|DeleteOp<any>|TextOp|ModifyOp<any>>}
     */
    const maybeMergeable = []
    /**
     * Schedule an op to be merged later
     * @param {ChildrenOpAny?} op
     * @return {ChildrenOpAny}
     */
    const scheduleForMerge = op => {
      op && maybeMergeable.push(op)
      return /** @type {any} */ (op)
    }

    /**
     * Split opsI at the current offset - ensuring that we can insert safely at the current position
     */
    const splitHere = () => {
      if (offset > 0 && opsI != null) {
        opsI = scheduleForMerge(_splitChildAt(this, opsI, offset))
        offset = 0
      }
    }
    /**
     * @param {ChildrenOpAny} op
     */
    const insertClonedOp = op => {
      splitHere()
      // `move` re-parents the source op directly (it is donated); else clone with `keep`. Freshly
      // built `new DeleteOp(...)` callers are unaliased, so moving them is likewise safe.
      _insertChild(this, opsI, scheduleForMerge(move ? op : op.clone(0, op.length, keep)))
      // inserting pre-marked content (e.g. a node that already carries a cursor) brings its marks along
      if ($insertOp.check(op)) this.maybeHasMarks ||= elemsMaybeHaveMarks(op.insert)
    }
    // manual iteration (not `for..of`): a `move` apply re-parents the source op into `this.children`,
    // rewiring its `.next`; capture the successor first so the walk stays on `other`'s chain.
    for (let op = /** @type {ChildrenOpAny?} */ (other.children.start), nextOp = op; op != null; op = nextOp) {
      nextOp = op.next
      // defensive: the per-branch logic below resets opsI/offset whenever it
      // consumes an op exactly. This guard catches any path that forgets to.
      /* c8 ignore start */
      if (opsI?.length === offset) {
        opsI = opNextUndeleted(opsI)
        offset = 0
      }
      /* c8 ignore stop */
      if ($textOp.check(op) || $insertOp.check(op)) {
        insertClonedOp(op)
      } else if ($retainOp.check(op)) {
        let retainLen = op.length
        // split the current op when this retain carries a format and/or attribution instruction
        // (`!== undefined`) that differs from what the existing op holds — both unified tri-state. The
        // split must fire on any differing instruction (incl. a `{k:null}` removal or a `null` clear), not
        // just one that would change a *data* target: it marks the boundary between the already-advanced
        // (skipped) prefix and the region this op formats, so the update can't bleed into the prefix.
        if (offset > 0 && opsI != null && !$deleteOp.check(opsI) && (
          (op.format !== undefined && !fun.equalityDeep(/** @type {InsertOp<any>|RetainOp|ModifyOp} */ (opsI).format, op.format)) ||
          (op.attribution !== undefined && !fun.equalityDeep(/** @type {any} */ (opsI).attribution, op.attribution))
        )) {
          // need to split current op
          splitHere()
        }
        while (opsI != null && opsI.length - offset <= retainLen) {
          if (op.format !== undefined || op.attribution !== undefined) {
            updateOpFormat(opsI, op.format, op.attribution)
            scheduleForMerge(opsI)
          }
          retainLen -= opsI.length - offset
          opsI = opNextUndeleted(opsI)
          offset = 0
        }
        if (opsI != null) {
          if ((op.format !== undefined || op.attribution !== undefined) && retainLen > 0) {
            // accumulate onto the existing offset — the else-branch below uses
            // `offset += retainLen`, and we must agree with it when prior
            // iterations have advanced offset into opsI without splitting (e.g.
            // a format-less retain followed by a same-format retain).
            offset += retainLen
            splitHere()
            updateOpFormat(/** @type {ChildrenOpAny} */ (opsI.prev), op.format, op.attribution)
            scheduleForMerge(opsI.prev)
          } else {
            offset += retainLen
          }
        } else if (retainLen > 0) {
          // append the retain beyond existing content. On a NON-final/instruction apply keep it verbatim —
          // a `null` (clear) format/attribution here is a meaningful change op (e.g. a format/attribution
          // diff applied onto an as-yet-empty change delta). On a FINAL (materializing) apply the
          // removal/clear has nothing to act on, so resolve it away ({@link resolveBeyond}): a `{k:null}`
          // removal or `null` clear collapses to `undefined`, a set survives. We still append the (possibly
          // now-bare) retain so a following op stays positioned; done() trims it iff it is trailing.
          const fmt = final ? resolveBeyond(op.format, false) : op.format
          const attr = final ? resolveBeyond(op.attribution, true) : op.attribution
          _insertChild(this, null, scheduleForMerge(new RetainOp(retainLen, fmt, attr)))
        }
      } else if ($deleteOp.check(op)) {
        let remainingLen = op.delete
        while (remainingLen > 0) {
          if (opsI == null) {
            _insertChild(this, null, scheduleForMerge(new DeleteOp(remainingLen)))
            break
          } else if ($retainOp.check(opsI)) { // retain ⇒ splice retain, insert delete
            const delLen = math.min(opsI.length - offset, remainingLen)
            _shrinkChild(this, opsI, offset, delLen)
            if (opsI.length === 0) {
              _removeChild(this, opsI)
              opsI = opNextUndeleted(opsI)
              offset = 0
            }
            if (opsI?.length === offset) {
              opsI = opNextUndeleted(opsI)
              offset = 0
            }
            insertClonedOp(new DeleteOp(delLen))
            remainingLen -= delLen
          } else if ($modifyOp.check(opsI)) { // modify ⇒ delete the source position it stands for
            // a modify op addresses an underlying source position (retain + change), so deleting it
            // must emit a DeleteOp for that position - exactly like the retain case above. (The
            // insert/embed branch below instead just cancels the inserted content, because that
            // content is new and has no source position to delete.)
            _removeChild(this, opsI)
            opsI = opNextUndeleted(opsI)
            offset = 0
            insertClonedOp(new DeleteOp(1))
            remainingLen -= 1
          } else if (!$deleteOp.check(opsI)) { // insert / embed ⇒ replace
            // case1: delete o fully
            // case2: delete some part of beginning
            // case3: delete some part of end
            // case4: delete some part of center
            const delLen = math.min(opsI.length - offset, remainingLen)
            // deleting content drops any marks in the removed embeds; the conservative `maybeHasMarks`
            // flag is left as-is (never decremented - marksToPositions self-corrects it)
            if (opsI.length === delLen) {
              // case 1: delete opsI fully
              _removeChild(this, opsI)
              scheduleForMerge(opsI = opNextUndeleted(opsI))
              offset = 0
            } else if (offset === 0) {
              // case 2: delete from the beginning
              _shrinkChild(this, opsI, offset, delLen)
            } else if (offset + delLen === opsI.length) {
              // case 3: delete to the end
              _shrinkChild(this, opsI, offset, delLen)
              opsI = opNextUndeleted(opsI)
              offset = 0
            } else {
              // case 4: delete from the center
              _shrinkChild(this, opsI, offset, delLen)
            }
            remainingLen -= delLen
          /* c8 ignore start */
          } else {
            // unreachable: opsI was already typed as retain | non-delete-content | delete above
            error.unexpectedCase()
          }
          /* c8 ignore stop */
        }
      } else if ($modifyOp.check(op)) {
        if (opsI != null && (op.format !== undefined || op.attribution !== undefined) && (!$deleteOp.check(opsI) && !$retainOp.check(opsI))) { // retain handles splitting seperately, without copying attrs
          splitHere()
          if (opsI.length > 1) {
            offset = 1
            splitHere()
            opsI = /** @type {InsertOp<any>} */ (opsI.prev)
          }
          // at this point, opsI is guaranteed to be !deleteOp && !retainOp and of length 1
          updateOpFormat(opsI, op.format, op.attribution)
          scheduleForMerge(opsI)
        }
        if (opsI == null) {
          _insertChild(this, null, move ? op : op.clone(0, 1, keep))
          this.maybeHasMarks ||= /** @type {DeltaAny} */ (op.value).maybeHasMarks // a freshly-inserted modify brings its value's root marks
        } else if ($modifyOp.check(opsI)) {
          const tgt = opsI._modValue
          tgt.apply(/** @type {any} */ (op.value), { final, move })
          this.maybeHasMarks ||= tgt.maybeHasMarks // flag the child subtree's marks up (incl. the value's own root marks)
          opsI = opNextUndeleted(opsI)
        } else if ($insertOp.check(opsI)) {
          const tgt = opsI._modValue(offset)
          tgt.apply(op.value, { final, move })
          this.maybeHasMarks ||= tgt.maybeHasMarks // flag the child subtree's marks up (incl. the value's own root marks)
          if (opsI.length === ++offset) {
            opsI = opNextUndeleted(opsI)
            offset = 0
          }
        } else if ($retainOp.check(opsI)) {
          splitHere()
          const insertModOp = scheduleForMerge(move ? op : op.clone(0, 1, keep))
          this.maybeHasMarks ||= /** @type {DeltaAny} */ (op.value).maybeHasMarks // the inserted modify brings its value's root marks
          ;(opsI.format !== undefined || opsI.attribution !== undefined) && updateOpFormat(insertModOp, opsI.format, opsI.attribution)
          // the modify replaces one retain position with a length-1 modify (net childCnt: +1 here, -1 below)
          _insertChild(this, opsI, insertModOp)
          if (opsI.length === 1) {
            _removeChild(this, opsI)
            opsI = opNextUndeleted(opsI)
            offset = 0
          } else {
            _shrinkChild(this, opsI, 0, 1)
            scheduleForMerge(opsI)
          }
        /* c8 ignore start */
        } else {
          // remaining branches: opsI is deleteOp or something unknown
          // both branches are unreachable today: opNextUndeleted skips
          // delete ops, so opsI is never a delete during iteration; and the four
          // branches above exhaust the other op kinds. The deleteOp branch is
          // kept as a defensive no-op (drops a modify that lands in a deleted
          // region) rather than a throw.
          error.unexpectedCase()
        }
      } else {
        error.unexpectedCase()
      }
      /* c8 ignore stop */
    }
    // iterate backwards, to ensure that we merge all content
    for (let i = maybeMergeable.length - 1; i >= 0; i--) {
      const op = maybeMergeable[i]
      // check if this is still integrated
      if (op.prev != null ? op.prev.next === op : this.children.start === op) {
        op.prev && _mergeChildWithPrev(this, op)
        op.next && _mergeChildWithPrev(this, op.next)
      }
    }
    // marks: shift this node's own leaf marks by the change, then fold in root-level mark adds/deletes
    // (child subtree marks already flagged `maybeHasMarks` incrementally above)
    shiftMarksByChange(this, other)
    // pass `final` so a materializing apply (e.g. a deltaRDT's final state) applies deletes in place
    // but collects no pending `deleteMarks`; a non-final change delta still records them (transmittable)
    applyMarkOps(this, other.marks, other.deleteMarks, final)
    return this
  }

  /**
   * Rebase this op against a concurrent op. We can apply this op on a doc that already applied
   * `other` op.
   *
   * @param {DeltaAny} other
   * @param {boolean} priority
   */
  rebase (other, priority) {
    modDeltaCheck(this)
    /**
     * Rebase attributes
     *
     * - insert vs delete ⇒ insert takes precedence
     * - insert vs modify ⇒ insert takes precedence
     * - insert vs insert ⇒ priority decides
     * - delete vs modify ⇒ delete takes precedence
     * - delete vs delete ⇒ current delete op is removed because item has already been deleted
     * - modify vs modify ⇒ rebase using priority
     */
    for (const op of this.attrs) {
      if ($setAttrOp.check(op)) {
        if ($setAttrOp.check(other.attrs[op.key]) && !priority) {
          // @ts-ignore
          delete this.attrs[op.key]
        }
      } else if ($deleteAttrOp.check(op)) {
        // @ts-ignore
        const otherOp = other.attrs[/** @type {any} */ (op.key)]
        if ($setAttrOp.check(otherOp)) {
          // @ts-ignore
          delete this.attrs[otherOp.key]
        }
      } else if ($modifyAttrOp.check(op)) {
        const otherOp = other.attrs[/** @type {any} */ (op.key)]
        if (otherOp == null) {
          // nop
        } else if ($modifyAttrOp.check(otherOp)) {
          op._modValue.rebase(otherOp.value, priority)
        } else {
          // @ts-ignore
          delete this.attrs[otherOp.key]
        }
      /* c8 ignore start */
      } else {
        // unreachable: attr ops are exhaustively setAttr | deleteAttr | modifyAttr
        error.unexpectedCase()
      }
      /* c8 ignore stop */
    }
    /**
     * Rebase children.
     *
     * Precedence: insert with higher priority comes first. Op with less priority is transformed to
     * be inserted later.
     *
     * @todo always check if inser OR text
     */
    /**
     * @type {ChildrenOpAny?}
     */
    let currChild = this.children.start
    let currOffset = 0
    /**
     * @type {ChildrenOpAny?}
     */
    let otherChild = other.children.start
    let otherOffset = 0
    while (currChild != null && otherChild != null) {
      if ($insertOp.check(currChild) || $textOp.check(currChild)) {
        /**
         * Transforming *insert*. If other is..
         * - insert: transform based on priority
         * - retain/delete/modify: transform next op against other
         */
        if ($insertOp.check(otherChild) || $textOp.check(otherChild)) {
          if (!priority) {
            _insertChild(this, currChild, new RetainOp(otherChild.length, undefined, undefined))
            // curr is transformed against other, transform curr against next
            otherOffset = otherChild.length
          } else {
            // curr stays as is, transform next op
            currOffset = currChild.length
          }
        } else { // otherChild = delete | retain | modify - curr stays as is, transform next op
          currOffset = currChild.length
        }
      } else if ($modifyOp.check(currChild)) {
        /**
         * Transforming *modify*. If other is..
         * - insert: adjust position
         * - modify: rebase curr modify on other modify
         * - delete: remove modify
         * - retain: adjust offset
         */
        if ($insertOp.check(otherChild) || $textOp.check(otherChild)) {
          // @todo: with all list changes (retain insertions, removal), try to merge the surrounding
          // ops later
          _insertChild(this, currChild, new RetainOp(otherChild.length, undefined, undefined))
          // curr is transformed against other, transform curr against next
          otherOffset = otherChild.length
        } else {
          if ($modifyOp.check(otherChild)) {
            // _modValue (not .value) — ModifyOp.clone() marks its inner delta
            // as `done`, so a cloned ModifyOp can only be rebased after the
            // _modValue getter lazy-clones it back to mutable. This recursion also reconciles the
            // value's own root marks (via rebaseRootMarks), so a nested mark needs no special handling.
            currChild._modValue.rebase(otherChild.value, priority)
          } else if ($deleteOp.check(otherChild)) {
            _removeChild(this, currChild)
          }
          currOffset += 1
          otherOffset += 1
        }
      } else { // DeleteOp | RetainOp
        const maxCommonLen = math.min(currChild.length - currOffset, otherChild.length - otherOffset)
        /**
         * Transforming *retain* OR *delete*. If other is..
         * - retain / modify: adjust offsets
         * - delete: shorten curr op
         * - insert: split curr op and insert retain
         */
        if ($retainOp.check(otherChild) || $modifyOp.check(otherChild)) {
          // Format reconciliation over the [currOffset..currOffset+maxCommonLen] overlap. Split currChild
          // around the overlap so the prefix/suffix keep their original format and only the middle piece
          // changes. Three rules:
          //  1. A blanket `null` clear ALWAYS wins, priority-independent (this is what lets it converge — both
          //     rebase orderings agree the overlap ends up cleared, at the cost of dropping a concurrent set;
          //     hence `null` is a local-only utility, see the `Formats` docs). currChild's own
          //     `null` clear is left untouched (it wins); if *other* clears, currChild concedes its whole format.
          //  2. Otherwise (both per-key objects) priority decides: priority=true is a no-op (currChild wins);
          //     for !priority currChild concedes any key otherChild also writes (a `{k:null}` removal is just
          //     such a key, so per-key removals reconcile here and converge without data loss — the recommended
          //     way to clear).
          //  3. otherChild `undefined` (skip) never changes currChild.
          if ($retainOp.check(currChild) && currChild.format !== null) {
            let changed = false
            /** @type {Formats|null|undefined} */
            let newFormat
            if (otherChild.format === null) {
              // other clears all → clear wins → currChild concedes its entire format
              if (currChild.format !== undefined) { newFormat = undefined; changed = true }
            } else if (!priority && currChild.format != null && otherChild.format != null) {
              /** @type {Formats} */
              const stripped = {}
              for (const k in currChild.format) {
                if (k in otherChild.format) changed = true
                else stripped[k] = currChild.format[k]
              }
              if (changed) newFormat = object.isEmpty(stripped) ? undefined : stripped
            }
            if (changed) {
              // split off the suffix [currOffset+maxCommonLen..length] if any
              if (currOffset + maxCommonLen < currChild.length) {
                _splitChildAt(this, currChild, currOffset + maxCommonLen)
              }
              // split off the prefix [0..currOffset] if any: currChild becomes the [currOffset..] suffix
              if (currOffset > 0) {
                currChild = _splitChildAt(this, currChild, currOffset)
                currOffset = 0
              }
              // currChild now spans exactly the overlap. A conceded format is `undefined` (skip / no format
              // change), NOT `null` (which would CLEAR the format on apply).
              /** @type {any} */ (currChild).format = newFormat
              currChild._fingerprint = null
            }
          }
          currOffset += maxCommonLen
          otherOffset += maxCommonLen
        } else if ($deleteOp.check(otherChild)) {
          // currChild is Retain | Delete here (outer branch); shrink it by the overlap
          _shrinkChild(this, currChild, currOffset, maxCommonLen)
          // advance other so subsequent currChild ops see what comes AFTER this
          // delete; without this we'd loop against the same delete forever and
          // never reach other's later inserts.
          otherOffset += maxCommonLen
        } else { // insert/text.check(otherChild)
          if (currOffset > 0) {
            // split currChild so the cursor sits on the boundary; continue on the [currOffset..] suffix
            currChild = _splitChildAt(this, currChild, currOffset)
            currOffset = 0
          }
          _insertChild(this, currChild, new RetainOp(otherChild.length, undefined, undefined))
          otherOffset = otherChild.length
        }
      }
      if (currOffset >= currChild.length) {
        currChild = currChild.next
        currOffset = 0
      }
      if (otherOffset >= otherChild.length) {
        otherChild = otherChild.next
        otherOffset = 0
      }
    }
    // marks: reconcile this node's own root mark ops against the concurrent change (also shifting and,
    // where an anchor was deleted, converting an add to a delete). No count to maintain: `maybeHasMarks`
    // is conservative (rebase only drops/reconciles marks, never adds), so a stale `true` is harmless
    // and marksToPositions self-corrects it.
    if (this.marks !== null || this.deleteMarks !== null) {
      rebaseRootMarks(this, other, priority)
    }
    return this
  }

  /**
   * Same as doing `delta.rebase(other.inverse())`, without creating a temporary delta.
   *
   * @param {DeltaAny} other
   * @param {boolean} priority
   */
  rebaseOnInverse (other, priority) {
    modDeltaCheck(this)
    // @todo
    console.info('method rebaseOnInverse unimplemented')
    return this
  }

  /**
   * Append child ops from one op to the other.
   *
   *     delta.create().insert('a').append(delta.create().insert('b')) // => insert "ab"
   *
   * @template {DeltaConf} OtherDeltaConf
   * @param {Delta<OtherDeltaConf>} other
   * @return {DeltaBuilder<FixedConf extends true ? Conf : DeltaConfOverwrite<
   *   Conf,
   *   (DeltaConfGetChildren<OtherDeltaConf> extends never ? {} : { children: DeltaConfGetChildren<Conf> | DeltaConfGetChildren<OtherDeltaConf> })
   *   & (DeltaConfGetText<OtherDeltaConf> extends string ? { text: true } : never)
   * >, FixedConf>}
   */
  append (other) {
    modDeltaCheck(this)
    const prevLast = this.children.end
    // @todo Investigate. Above is a typescript issue. It is necessary to cast OtherDelta to a Delta first before
    // inferring type, otherwise Children will contain Text.
    for (const child of other.children) {
      _insertChild(this, null, child.clone())
    }
    // appended children may carry marks in their subtrees - flag conservatively
    this.maybeHasMarks ||= other.maybeHasMarks
    prevLast?.next && _mergeChildWithPrev(this, prevLast.next)
    // @ts-ignore
    return this
  }
}

/**
 * Apply one tri-state dimension update (`format` or `attribution`) to `op[field]` and report whether it
 * changed. `undefined` = skip; `null` = clear all keys; an object merges per-key (`{k:v}` sets, `{k:null}`
 * removes key `k`). `format` and `attribution` are handled identically (the unified model).
 *
 * On an **instruction** op (`retain`/`modify`) the merge is kept verbatim — a `{k:null}` removal and a
 * `null` clear must survive so they still apply when this op is later consumed (an empty merge result `{}`
 * is a no-op → `undefined`/skip). On a **data** op (`insert`/`text`/attr op) the merge is resolved against
 * the stored value (empty → `null`/none). (Limitation: a `null` clear already present on an instruction op
 * does not compose with a subsequent set into a single op — only the set survives. The diff never produces
 * this; it would need arbitrary clear-then-set change composition.)
 *
 * @param {_OpAny} op
 * @param {'format'|'attribution'} field
 * @param {{[k:string]:any}|null|undefined} update
 * @return {boolean} whether `op[field]` was touched
 */
const applyDim = (op, field, update) => {
  if (update === undefined) return false
  if (update === null) {
    /** @type {any} */ (op)[field] = null // clear all
  } else if ($retainOp.check(op) || $modifyOp.check(op)) {
    // instruction op: keep the merge verbatim (preserves `{k:null}` removals to apply later). Attribution
    // additionally merges its nested `format` key one level (see {@link mergeAttr}); format stays shallow.
    const cur = /** @type {any} */ (op)[field]
    const merged = field === 'attribution' ? mergeAttr(cur, update, false) : object.assign({}, cur, update)
    // the empty-merge guard is currently unreachable: a stored `update` is canonical (the builder's
    // combine strips `{k:undefined}` keys and drops empty objects to `undefined`, which `applyDim`
    // already returned on), and a verbatim (non-resolving) merge never removes keys — so `merged`
    // keeps at least one key of `update`. Kept to document that an empty result means skip.
    /* c8 ignore next */
    ;/** @type {any} */ (op)[field] = object.isEmpty(merged) ? undefined : merged
  } else if (field === 'attribution') {
    // data op, attribution: resolve per key; the nested `format` key merges one level (see {@link mergeAttr})
    const merged = mergeAttr(/** @type {any} */ (op)[field], update, true)
    ;/** @type {any} */ (op)[field] = object.isEmpty(merged) ? null : merged
  } else {
    // data op (format): resolve per-key against the stored value (`{k:v}` sets, `{k:null}` removes, `{k:undefined}` skips)
    let f = /** @type {any} */ (op)[field]
    for (const k in update) {
      const v = update[k]
      // the skip is currently unreachable: a stored `update` never carries a `{k:undefined}` key
      // (the builder's combine strips them). Kept for the unified tri-state contract.
      /* c8 ignore next */
      if (v === undefined) continue // skip this key
      if (v === null) { // remove this key (no-op when the stored value is already `null`)
        if (f !== null) {
          const { [k]: _, ...rest } = f
          f = object.isEmpty(rest) ? null : rest
        }
      } else { // set this key
        f = object.assign({}, f, { [k]: v })
      }
    }
    /** @type {any} */ (op)[field] = f
  }
  return true
}

/**
 * Compute the tri-state update that turns stored value `aVal` into `bVal` (used by {@link diff} for both
 * `format` and `attribution`): `undefined` when unchanged (skip), `null` when fully cleared, else a
 * per-key object (`{k:v}` for changed/added keys, `{k:null}` for removed keys). With `deep` (attribution),
 * the nested `format` key is diffed one level deeper so it round-trips through {@link mergeAttr}'s per-inner-key
 * merge instead of a wholesale replace.
 *
 * @param {{[k:string]:any}|null|undefined} aVal
 * @param {{[k:string]:any}|null|undefined} bVal
 * @param {boolean} deep `true` for `attribution` (recurse its `format` key), `false` for `format` (shallow)
 * @return {{[k:string]:any}|null|undefined}
 */
const diffDim = (aVal, bVal, deep) => {
  if (fun.equalityDeep(aVal, bVal)) return undefined
  if ((bVal === null || bVal === undefined || object.isEmpty(bVal)) && (deep || aVal == null)) {
    return null // fully cleared: attribution (`deep`) clears wholesale; format with no prior keys has nothing to enumerate
  }
  /** @type {{[k:string]:any}} */
  const u = {}
  for (const k in bVal) {
    if (!fun.equalityDeep(bVal[k], aVal?.[k] ?? null)) {
      // attribution's nested `format` diffs one level deeper (mirrors mergeAttr); everything else is wholesale
      u[k] = deep && k === 'format' && s.$objectAny.check(bVal[k]) && s.$objectAny.check(aVal?.[k])
        ? diffDim(aVal[k], bVal[k], false)
        : bVal[k]
    }
  }
  // removed keys. For `format` (shallow) a full clear enumerates one `{k:null}` per prior key instead of a
  // wholesale `format:null` — individual clears rebase without clobbering concurrently-added formats.
  // (`for..in` is a no-op when `aVal` is nullish; `k in bVal` is guarded against a nullish `bVal`.)
  for (const k in aVal) { if (bVal == null || !(k in bVal)) u[k] = null }
  return u
}

/**
 * Apply a `format` and/or `attribution` update to an op (both unified tri-state — see {@link applyDim}).
 * `format`/`attribution` are fingerprinted, so any change nulls the op's `_fingerprint`.
 *
 * @param {ChildrenOpAny} op
 * @param {{[k:string]:any}|null|undefined} formatUpdate
 * @param {{[k:string]:any}|null|undefined} attributionUpdate
 */
const updateOpFormat = (op, formatUpdate, attributionUpdate) => {
  // defensive: apply's walk never passes a delete op (the initial `opsI` skips leading deletes and
  // every advance goes through `opNextUndeleted`)
  /* c8 ignore next */
  if ($deleteOp.check(op)) return
  const changedF = applyDim(op, 'format', formatUpdate)
  const changedA = applyDim(op, 'attribution', attributionUpdate)
  if (changedF || changedA) {
    /** @type {any} */ (op)._fingerprint = null
  }
}

/**
 * @param {ChildrenOpAny?} op
 */
const opNextUndeleted = op => {
  op = op?.next || null
  while (op != null && $deleteOp.check(op)) {
    op = op.next
  }
  return op
}

/**
 * Total order on marks by their (unique) id, used to sort a mark list deterministically (e.g. in
 * {@link Delta#toJSON}).
 *
 * @param {Mark} a
 * @param {Mark} b
 */
const compareMarksById = (a, b) => a.id < b.id ? -1 : 1

/**
 * Position-step order for a mark's `key`, mirroring the fingerprint key sort (see {@link Delta#fingerprint}):
 * numbers (child indices) first, ascending; then string (attribute) keys, lexicographically.
 *
 * @param {number|string} a
 * @param {number|string} b
 */
const cmpKey = (a, b) =>
  s.$number.check(a) ? (s.$number.check(b) ? a - b : -1) : s.$number.check(b) ? 1 : (a < b ? -1 : a > b ? 1 : 0)

/**
 * Total order over the marks of one node: by position-step {@link cmpKey}, tie-broken by the (unique)
 * id so the order is fully deterministic even when two cursors share a key. Used by {@link sortMarks}.
 *
 * @param {Mark} a
 * @param {Mark} b
 */
// the `: 0` id tie is unreachable: a Marks set is deduplicated by id (`add` replaces), so the sort
// never compares two marks with the same id
/* c8 ignore next */
const cmpMarkKey = (a, b) => cmpKey(a.key, b.key) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)

/**
 * Sort a {@link Marks} set in place into its canonical {@link cmpMarkKey} order and return it. Called
 * lazily whenever the set is iterated, so every reader (e.g.
 * {@link import('./position.js').marksToPositions}) sees one deterministic, position-ordered sequence
 * regardless of the order marks were added in. Order is not part of a delta's identity, so reordering
 * is safe even on a `done` delta.
 *
 * @param {Marks} marks
 */
const sortMarks = marks => { marks._marks.sort(cmpMarkKey); return marks }

/**
 * The set of leaf {@link Mark marks} on a single delta node, deduplicated by id (so adding the same id
 * again replaces it). An internal data-shape class (never a public/`new`-from-outside class): every
 * mutation goes through its methods, so a stored {@link Mark} is treated as immutable and is never
 * manipulated in place — to "move" a mark you replace it with a fresh `Mark`. {@link clone}/{@link
 * slice} copy the set so a `done` delta's marks are never mutated through a shared reference. Marks are
 * local/ephemeral cursor state
 * and not part of a delta's identity, so this set has no fingerprint/equality of its own.
 */
class Marks {
  constructor () {
    /**
     * @type {Array<Mark>}
     */
    this._marks = []
  }

  get size () { return this._marks.length }

  /**
   * Add or replace `mark` (by id). Returns `true` if it was added as a new mark, `false` if it
   * replaced an existing one with the same id.
   *
   * @param {Mark} mark
   * @return {boolean}
   */
  add (mark) {
    const i = this._marks.findIndex(m => m.id === mark.id)
    if (i < 0) {
      this._marks.push(mark)
      return true
    }
    this._marks[i] = mark
    return false
  }

  /**
   * Remove the mark with `id`. Returns `true` if a mark was present and removed, `false` if absent.
   *
   * @param {string} id
   * @return {boolean}
   */
  delete (id) {
    const i = this._marks.findIndex(m => m.id === id)
    if (i < 0) return false
    this._marks.splice(i, 1)
    return true
  }

  [Symbol.iterator] () { sortMarks(this); return this._marks[Symbol.iterator]() }
}

/**
 * Whether any delta element in `insert` maybe carries a mark in its subtree (short-circuits). Used to
 * OR-propagate the conservative {@link DeltaData#maybeHasMarks} flag when content is inserted.
 *
 * @param {Array<any>} insert
 * @return {boolean}
 */
const elemsMaybeHaveMarks = insert => insert.some(el => $deltaAny.check(el) && el.maybeHasMarks)

/**
 * Add (or replace) a {@link Mark} on `node`, flagging {@link DeltaData#maybeHasMarks}. Adding a mark
 * cancels any pending delete of the same id on `node` (so a node never holds both an add and a delete
 * for one id — the add wins), keeping the apply path consistent with rebase's add-vs-delete rule.
 *
 * @param {DeltaAny} node
 * @param {Mark} mark
 */
const addMarkTo = (node, mark) => {
  const marks = node.marks ?? new Marks()
  marks.add(mark)
  node.marks = marks
  node.maybeHasMarks = true
  if (node.deleteMarks !== null) {
    node.deleteMarks.delete(mark.id)
    if (node.deleteMarks.size === 0) node.deleteMarks = null
  }
}

/**
 * Mirror of {@link addMarkTo}: assert mark `id` is *absent* on `node` (last-writer-wins). Strips a
 * present add of `id` (a delete cancels a pending add — newest wins), then, only for a CHANGE delta
 * (`record`, i.e. a non-final apply), records the transmittable tombstone in `deleteMarks`. A
 * final/materializing apply passes `record === false`: the mark is removed in place but no tombstone is
 * collected (a final delta carries none — see {@link DeltaData#deleteMarks}). The `maybeHasMarks` flag is
 * intentionally left as-is (never decremented - {@link import('./position.js').marksToPositions}
 * self-corrects it). Together with `addMarkTo` these are the only two writers of `marks`/`deleteMarks`,
 * so a node can never hold both an add and a delete for one id.
 *
 * @param {DeltaAny} node
 * @param {string} id
 * @param {boolean} record whether to keep a transmittable `deleteMarks` tombstone (`= !final`)
 */
const deleteMarkTo = (node, id, record) => {
  if (node.marks !== null) {
    node.marks.delete(id)
    if (node.marks.size === 0) node.marks = null
  }
  if (record) {
    const dm = node.deleteMarks ?? new Set()
    dm.add(id)
    node.deleteMarks = dm
  }
}

/**
 * Apply mark add/delete ops to `target` (flagging `target.maybeHasMarks` on add). `addMarks` may be a
 * {@link Marks} set or a plain array; `deleteMarks` is a set of ids. The two id-keyed primitives
 * {@link deleteMarkTo}/{@link addMarkTo} are the only writers of `marks`/`deleteMarks`, so a node can
 * never end holding both for one id. Deletes are processed before adds so that a single change carrying
 * both for one id resolves to *add wins* (the add re-runs last and strips the just-recorded tombstone),
 * matching the rebase policy. `final` marks a document-materializing apply (propagated from
 * {@link DeltaBuilder#apply}, defaulting to `this.isFinal`): a settled document removes a present mark in
 * place but records no tombstone, while a non-final change delta records the delete (passed as
 * `record = !final`) so `create().removeMark(pos, id)` stays transmittable.
 *
 * @param {DeltaAny} target
 * @param {Iterable<Mark>?} addMarks
 * @param {Set<string>?} deleteMarks
 * @param {boolean} final whether this is a final (document-materializing) apply
 */
const applyMarkOps = (target, addMarks, deleteMarks, final) => {
  if (deleteMarks !== null) {
    for (const id of deleteMarks) deleteMarkTo(target, id, !final)
  }
  if (addMarks !== null) {
    for (const m of addMarks) addMarkTo(target, m)
  }
}

/**
 * Re-key `node`'s *root* marks in place: each mark's `key` is mapped through `mapKey` (return `null` to
 * drop it, an unchanged key to keep it, or a new key to move it). Used after a full {@link clone} by an
 * attribute-renaming transformer so a mark on a renamed attribute follows the rename (and a mark on a
 * dropped attribute is dropped). `node.maybeHasMarks` is left set (conservative; never decremented).
 *
 * @param {DeltaAny} node
 * @param {(key: number|string) => number|string|null} mapKey
 * @internal transformer plumbing (used by mark-carrying transformers), not consumer API.
 */
export const remapRootMarks = (node, mapKey) => {
  const marks = node.marks
  if (marks === null) return
  for (const m of [...marks]) {
    const k = mapKey(m.key)
    if (k === m.key) continue
    deleteMarkTo(node, m.id, false) // relocate a present mark: strip it, but synthesize no tombstone
    if (k !== null) addMarkTo(node, m.copy(k))
  }
}

/**
 * Add (or replace, by id) a single root {@link Mark} on `node`, flagging `node.maybeHasMarks`. The
 * public wrapper of the internal `addMarkTo`, used by the restructuring
 * {@link import('./transformer/core.js').Transformer transformers} (`inline`, `project`) to place a
 * mark at a position they compute themselves.
 *
 * @param {DeltaAny} node
 * @param {Mark} mark
 * @internal transformer plumbing (used by `inline`/`project`), not consumer API.
 */
export const addRootMark = (node, mark) => addMarkTo(node, mark)

/**
 * Mark id `id` as deleted on `node` (the mirror of {@link addRootMark}): the public wrapper of the
 * internal {@link deleteMarkTo} with `record === true`, used by the restructuring
 * {@link import('./transformer/core.js').Transformer transformers} (`inline`, `project`) to carry a mark
 * *delete* (which is id-keyed, so it needs no position) onto the node they build. Strips a present add of
 * the same id (last-writer-wins) so the node never holds both.
 *
 * @param {DeltaAny} node
 * @param {string} id
 * @internal transformer plumbing (used by `inline`/`project`), not consumer API.
 */
export const deleteRootMark = (node, id) => deleteMarkTo(node, id, true)

/**
 * Merge `source`'s *root* marks onto `target`, mapping each add mark's `key` through `mapKey` (return
 * `null` to drop it, a new key to move it via {@link Mark#copy}). `source`'s `deleteMarks` are merged
 * verbatim through {@link deleteMarkTo} — a delete is keyed only by its (cross-side-stable) id, so it
 * maps in either direction and strips a conflicting add on `target` (last-writer-wins, never both). Safe
 * to call repeatedly to accumulate marks from several sources onto one target (e.g.
 * {@link import('./transformer/inline.js') inline} lifting the marks of every spliced node onto the
 * flattened parent), and — onto a fresh target — to copy a node's marks across a key remapping (e.g.
 * {@link cloneShallow}, and the `attr` transformer following an attribute rename). `target.maybeHasMarks`
 * is flagged for the added marks; marks inside copied delta-valued attributes/children are flagged when
 * the caller appends them via the builder (and {@link cloneShallow} copies the flag wholesale).
 *
 * @param {DeltaAny} target
 * @param {DeltaAny} source
 * @param {(key: number|string) => number|string|null} [mapKey]
 * @internal transformer plumbing (used by `inline`/`attr`/`cloneShallow`), not consumer API.
 */
export const mergeRootMarks = (target, source, mapKey = k => k) => {
  if (source.marks !== null) {
    for (const m of source.marks) {
      const k = mapKey(m.key)
      if (k !== null) addMarkTo(target, k === m.key ? m : m.copy(k))
    }
  }
  if (source.deleteMarks !== null) {
    for (const id of source.deleteMarks) deleteMarkTo(target, id, true)
  }
}

/**
 * Map a number `key` (a content offset) of a leaf mark on a node through the content ops of `change`,
 * returning its new offset. An insert before the key pushes it right (tie-broken at the exact offset by
 * `assoc`). A delete covering the key applies *collapse-to-cut*: the mark slides to the cut point
 * (`newPos`) — i.e. to *before* any replacement text inserted there — instead of being dropped, so a
 * cursor whose content is deleted survives at the deletion site. `oldPos` walks the pre-change doc and
 * `newPos` the post-change doc *independently* — `key` is never mutated, so an insert past the key is
 * always compared against the mark's original offset even after earlier deletes shifted the content.
 *
 * NOTE: collapse-to-cut is intentionally NOT confluent under concurrent {@link DeltaBuilder#rebase} — a
 * deletion maps a whole range to one point, losing where in the range the cursor was, so a concurrent
 * insert there lands on different sides on the two replays. Marks are therefore treated as local /
 * ephemeral cursor state, excluded from a delta's identity ({@link Delta#fingerprint} / equality); only
 * the document *content* is guaranteed to converge.
 *
 * @param {DeltaAny} change
 * @param {number} key
 * @param {1|-1} assoc
 * @return {number}
 */
const shiftMarkKey = (change, key, assoc) => {
  let oldPos = 0
  let newPos = 0
  for (const op of change.children) {
    if ($retainOp.check(op) || $modifyOp.check(op)) {
      const n = op.length
      if (key < oldPos + n) return newPos + (key - oldPos) // mark falls inside this retained run
      oldPos += n
      newPos += n
    } else if ($insertOp.check(op) || $textOp.check(op)) {
      if (oldPos < key || (oldPos === key && assoc === 1)) newPos += op.length
    } else if ($deleteOp.check(op)) {
      const n = op.delete
      // mark lies in [oldPos, oldPos+n) — its content is being deleted ⇒ collapse to the cut point
      if (key < oldPos + n) return newPos
      // right boundary with LEFT gravity anchors to the last deleted char ⇒ also collapses to the cut
      // (n > 0 guards the degenerate delete(0) rebase can emit, which deletes nothing)
      if (n > 0 && key === oldPos + n && assoc === -1) return newPos
      oldPos += n
    }
  }
  return newPos + (key - oldPos)
}

/**
 * Shift `node`'s own number-keyed leaf marks by the content ops of `change`, collapsing any covered by a
 * delete to the cut point (see {@link shiftMarkKey}). Attribute-key (string) marks are untouched.
 *
 * @param {DeltaAny} node
 * @param {DeltaAny} change
 */
const shiftMarksByChange = (node, change) => {
  const marks = node.marks
  if (marks === null) return
  /** @type {Array<Mark>} */
  const shifted = []
  for (const m of marks) {
    if (typeof m.key === 'number') {
      const nk = shiftMarkKey(change, m.key, m.assoc)
      if (nk !== m.key) shifted.push(m.copy(nk)) // a fresh Mark — never mutate in place
    }
  }
  // re-add shifted marks under their (unchanged) ids ⇒ replace in place (the flag stays set)
  for (const m of shifted) marks.add(m)
}

/**
 * Collect the ids of a {@link Marks} set, a plain {@link Mark} array, or an id array (any may be
 * `null`) into a `Set`, used for the by-id conflict checks during {@link DeltaBuilder#rebase}.
 *
 * @param {Iterable<Mark|string>?} marksOrIds
 * @return {Set<string>}
 */
const markIdSet = marksOrIds => {
  /** @type {Set<string>} */
  const ids = new Set()
  if (marksOrIds !== null) {
    for (const x of marksOrIds) ids.add(typeof x === 'string' ? x : x.id)
  }
  return ids
}

/**
 * Core mark reconciliation for one node level during {@link DeltaBuilder#rebase}. `adds` are this
 * side's mark adds (with keys) and `deletes` its delete ids; `otherAddIds`/`otherDelIds` are the
 * concurrent side's ids and `otherContent` the delta whose content ops shift surviving number keys.
 *
 * Rules (each by mark id): add-vs-add → `priority` decides; add-vs-delete → the add wins (a re-placed
 * cursor is not killed by a stale removal); delete-vs-delete → the duplicate is dropped. A surviving
 * number-keyed add is shifted by `otherContent`, collapsing to the cut point if its content was
 * concurrently deleted (see {@link shiftMarkKey}). Cursor positions are best-effort, not guaranteed to
 * converge (marks are excluded from the document's identity); only content convergence is guaranteed.
 *
 * @param {Iterable<Mark>} adds
 * @param {Array<string>} deletes
 * @param {Set<string>} otherAddIds
 * @param {Set<string>} otherDelIds
 * @param {DeltaAny} otherContent
 * @param {boolean} priority
 * @return {{ adds: Array<Mark>, deletes: Array<string> }}
 */
const rebaseMarkOps = (adds, deletes, otherAddIds, otherDelIds, otherContent, priority) => {
  /** @type {Array<Mark>} */
  const keptAdds = []
  /** @type {Array<string>} */
  const delIds = deletes.slice()
  for (const m of adds) {
    if (!priority && otherAddIds.has(m.id)) continue // lost an add-vs-add conflict
    if (typeof m.key === 'number') {
      const nk = shiftMarkKey(otherContent, m.key, m.assoc) // collapses to the cut if its content was deleted
      keptAdds.push(nk === m.key ? m : m.copy(nk))
    } else {
      keptAdds.push(m)
    }
  }
  // a delete survives only where the other side neither re-adds (add wins) nor already deletes (dedup)
  /** @type {Set<string>} */
  const seen = new Set()
  /** @type {Array<string>} */
  const keptDels = []
  for (const id of delIds) {
    if (!seen.has(id) && !otherAddIds.has(id) && !otherDelIds.has(id)) {
      seen.add(id)
      keptDels.push(id)
    }
  }
  return { adds: keptAdds, deletes: keptDels }
}

/**
 * Reconcile a node's own root mark add/delete ops against a concurrent change `other` during
 * {@link DeltaBuilder#rebase}, rebuilding `node.marks`/`node.deleteMarks` from {@link rebaseMarkOps}.
 * `maybeHasMarks` is left set (conservative; marksToPositions self-corrects a now-empty subtree).
 *
 * @param {DeltaAny} node
 * @param {DeltaAny} other
 * @param {boolean} priority
 */
const rebaseRootMarks = (node, other, priority) => {
  const { adds, deletes } = rebaseMarkOps(
    node.marks !== null ? [...node.marks] : [],
    node.deleteMarks !== null ? [...node.deleteMarks] : [],
    markIdSet(other.marks), markIdSet(other.deleteMarks), other, priority
  )
  // rebuild both fields wholesale from rebaseMarkOps's provably-disjoint output (concurrent reconciliation
  // keeps its deliberate add-wins rule, NOT the apply path's last-writer-wins; not a primitive write)
  if (adds.length === 0) {
    node.marks = null
  } else {
    const m = new Marks()
    for (const x of adds) m.add(x)
    node.marks = m
  }
  node.deleteMarks = deletes.length === 0 ? null : new Set(deletes)
}

/**
 * Build a change delta that adds (or, with `isDelete`, removes) the mark for `pos`. The descent is a
 * `retain`/`modify`/`modifyAttr` chain re-derived from `pos.path`; the mark always rides on the **leaf
 * delta's own marks** (root marks). Each interior step just wraps the next level in a `modify`
 * (content index) or `modifyAttr` (attribute key) — apply/rebase carry the wrapped value's root marks
 * onto/through the target for free.
 *
 * @param {import('./position.js').Pos} pos
 * @param {string} id
 * @param {boolean} isDelete
 * @param {boolean} [transient] never to be stored (see {@link Mark#transient})
 * @return {DeltaBuilderAny}
 */
const markChange = (pos, id, isDelete, transient = false) => {
  const path = pos.path
  // a mark anchors at the terminal step of its path (a content offset or attribute key); the root
  // position `[]` has no terminal and cannot carry a mark - reject it instead of recursing forever
  if (path.length === 0) throw error.create('cannot place a mark at the root position (empty path): a mark needs a terminal content-offset or attribute-key step')
  const mark = isDelete ? null : createMark(path[path.length - 1], id, pos.assoc, pos.attrs ?? null, transient)
  /**
   * @param {number} i
   * @return {DeltaBuilderAny}
   */
  const build = i => {
    if (i === path.length - 1) {
      // leaf reached: carry the mark on the delta's own marks
      const d = /** @type {DeltaBuilderAny} */ (create())
      if (mark === null) deleteMarkTo(d, id, true) // a change delta records the transmittable delete
      else addMarkTo(d, mark)
      return d
    }
    const step = path[i]
    return s.$string.check(step)
      ? /** @type {DeltaBuilderAny} */ (create()).modifyAttr(step, build(i + 1))
      : /** @type {DeltaBuilderAny} */ (create()).retain(step).modify(build(i + 1))
  }
  return build(0)
}

/**
 * @template {DeltaConf} Conf
 * @extends {s.Schema<Delta<Conf>>}
 * @internal use the {@link $delta} factory; this class is exported only for the `$$delta` predicate.
 */
export class $Delta extends s.Schema {
  /**
   * @param {s.Schema<any>} $name
   * @param {s.Schema<any>} $attrs
   * @param {s.Schema<any>} $children
   * @param {any} hasText
   * @param {any} recursiveChildren
   * @param {s.Schema<{[K:string]:any}>} $formats
   */
  constructor ($name, $attrs, $children, hasText, recursiveChildren, $formats) {
    super()
    const $attrsPartial = s.$$object.check($attrs) ? $attrs.partial : $attrs
    if (recursiveChildren) {
      // @ts-ignore
      $children = s.$union($children, this)
    }
    /**
     * @type {{
     *   $name: s.Schema<DeltaConfGetName<Conf>>,
     *   $attrs: s.Schema<DeltaConfGetAttrs<Conf>>,
     *   $children: s.Schema<DeltaConfGetChildren<Conf>>,
     *   hasText: DeltaConfGetText<Conf>
     *   recursiveChildren: DeltaConfGetRecursiveChildren<Conf>,
     *   $formats: s.Schema<{[K:string]:any}>
     * }}
     */
    this.shape = { $name, $attrs: $attrsPartial, $children, hasText, $formats, recursiveChildren }
  }

  /**
   * @param {any} o
   * @param {s.ValidationError} [err]
   * @return {o is Delta<Conf>}
   */
  check (o, err = undefined) {
    const { $name, $attrs, $children, hasText, $formats } = this.shape
    if (!$deltaAny.check(o, err)) {
      /* c8 ignore next */
      err?.extend(null, 'Delta', o?.constructor.name, 'Constructor match failed')
    } else if (o.name != null && !$name.check(o.name, err)) {
      /* c8 ignore next */
      err?.extend('Delta.name', $name.toString(), o.name, 'Unexpected node name')
    } else if (list.toArray(o.children).some(c => (!hasText && $textOp.check(c)) || (hasText && $textOp.check(c) && c.format != null && !$formats.check(c.format)) || ($insertOp.check(c) && !c.insert.every(ins => $children.check(ins))))) {
      err?.extend('Delta.children', '', '', 'Children don\'t match the schema')
    } else if (object.some(o.attrs, (op, k) => $setAttrOp.check(op) && !$attrs.check({ [k]: op.value }, err))) {
      err?.extend('Delta.attrs', '', '', 'Attrs don\'t match the schema')
    } else {
      return true
    }
    return false
  }
}

/**
 * @typedef {{
 *   name?: s.Schema<string>|string|Array<string>,
 *   attrs?: s.Schema<{ [key: string|number]: any }>|{ [key:string|number]:any },
 *   children?: any,
 *   text?: boolean,
 *   recursiveChildren?: boolean,
 *   formats?: { [k:string]: any }
 * }} ReadableDeltaConf
 */

/**
 * Transforms a ReadableDeltaConf (the input shape of `$delta(spec)`) to a DeltaConf.
 * @template {ReadableDeltaConf} DConfSpec
 * @typedef {[
 *   DConfSpec extends {name: infer N} ? s.ReadSchemaUnwrapped<N> : any,
 *   DConfSpec extends {attrs: infer A} ? s.ReadSchemaUnwrapped<A> : {},
 *   DConfSpec extends {children: infer C} ? s.ReadSchemaUnwrapped<C> : never
 * ] extends [infer NodeName, infer Attrs, infer Children]
 *   ? AsDeltaConf<PrettifyDeltaConf<(
 *       import('../ts.js').TypeIsAny<NodeName, {}, { name: NodeName }> &
 *       ([keyof Attrs] extends [never] ? {} : { attrs: Attrs }) &
 *       ([Children] extends [never] ? {} : { children: Children }) &
 *       (DConfSpec extends {text: true} ? { text: true } : {}) &
 *       (DConfSpec extends {recursiveChildren: true} ? { recursiveChildren: true } : {})
 *     )>>
 *   : never
 * } ReadDeltaConf
 */

/**
 * Name-first form: a string-literal `name` as the first argument is preserved as a literal type
 * (generic argument inference), so callers don't need `s.$literal('x')` / `@type {const}` to keep the
 * node name literal. The remaining options follow in `opts`.
 *
 * @template {string} Name
 * @template {Omit<ReadableDeltaConf, 'name'>} [Opts={}]
 * @overload
 * @param {Name} name
 * @param {Opts} [opts]
 * @return {s.Schema<Delta<ReadDeltaConf<Opts & { name: Name }>>>}
 */
/**
 * @template {ReadableDeltaConf} [Conf={}]
 * @overload
 * @param {Conf} opts
 * @return {s.Schema<Delta<ReadDeltaConf<Conf>>>}
 */
/**
 * @param {ReadableDeltaConf|string} optsOrName
 * @param {Omit<ReadableDeltaConf, 'name'>} [opts]
 * @return {any}
 */
export const $delta = (optsOrName, opts) => {
  const { name, attrs, children, text, formats, recursiveChildren: recursive } =
    s.$string.check(optsOrName) ? { ...opts, name: optsOrName } : optsOrName
  return /** @type {any} */ (new $Delta(
    /** @type {any} */ (name == null ? s.$any : s.$(name)),
    /** @type {any} */ (attrs == null ? s.$object({}) : s.$(attrs)),
    /** @type {any} */ (children == null ? s.$never : s.$(children)),
    text ?? false,
    recursive ?? false,
    formats == null ? s.$any : s.$(formats)
  ))
}

export const $$delta = /* @__PURE__ */s.$constructedBy($Delta)

export const $deltaAny = /** @type {s.Schema<Delta<any>>} */ (Delta.prototype.$type = s.$type('d:delta', Delta))
export const $deltaBuilderAny = /** @type {s.Schema<DeltaBuilderAny>} */ (/* @__PURE__ */s.$custom(o => $deltaAny.check(o) && !o.isDone))

/**
 * Helper function to merge attribution and attributes. The latter input "wins". The nested `format` key is
 * merged per inner key (one level — see {@link mergeAttr}); every other key is replaced wholesale.
 *
 * @template {{ [key: string]: any }} T
 * @param {T | null} a
 * @param {T | null} b
 */
export const mergeAttributions = (a, b) => a == null
  ? b
  : (b == null ? a : /** @type {T} */ (mergeAttr(a, b, true)))

/**
 * Helper function to merge formats. The latter input "wins".
 *
 * @template {{ [key: string]: any }} T
 * @param {T | null} a
 * @param {T | null} b
 */
export const mergeFormats = (a, b) => object.isEmpty(a)
  ? (object.isEmpty(b) ? null : b)
  : (object.isEmpty(b) ? a : object.assign({}, a, b))

/**
 * @template {DeltaAny|null} D
 * @param {D} a
 * @param {D} b
 * @return {D}
 */
export const mergeDeltas = (a, b) => {
  if (a != null && b != null) {
    const c = clone(a)
    c.apply(b)
    return /** @type {any} */ (c)
  }
  return /** @type {D} */ (a || b || null)
}

/**
 * The node (child delta) at child-position `pos` of `source`, or `null` when `pos` lands on a text
 * run or a non-delta child. Lets {@link random} target an existing node with a `modify` op.
 *
 * @param {DeltaAny} source
 * @param {number} pos
 * @return {DeltaAny?}
 */
const childNodeAt = (source, pos) => {
  let p = 0
  for (const op of source.children) {
    if ($textOp.check(op)) {
      if (pos < p + op.insert.length) return null
      p += op.insert.length
    } else if ($insertOp.check(op)) {
      for (const el of op.insert) {
        if (p === pos) return $deltaAny.check(el) ? el : null
        p++
      }
    }
  }
  /* c8 ignore start */
  // the sole caller passes pos < source.childCnt (sourceLen >= 1), so a node or text run always
  // resolves first; this guards an out-of-range pos defensively.
  return null
  /* c8 ignore stop */
}

/**
 * "Find the child schema for this node": the `$Delta` schemas within `$children` (a single schema or
 * any member of a union) that accept `node`. Only `$Delta` members can drive a recursive `random`
 * modify, so non-delta children (numbers, strings, `$any`) and non-matching names are dropped.
 *
 * @param {s.Schema<any>} $children
 * @param {DeltaAny} node
 * @return {Array<s.Schema<DeltaAny>>}
 */
const matchingNodeSchemas = ($children, node) => (s.$$union.check($children) ? $children.shape : [$children])
  .filter($c => $$delta.check($c) && $c.check(node))

/**
 * Random {@link Attribution} for fuzz tests. There is no schema for attributions; the canonical shape is
 * `(insert|delete + …At)` intersected with an optional format part. The format part exists only on
 * children (content ops), never on node attribute ops. Sometimes returns `undefined` (skip) or `null`
 * (clear). The optional `format` part carries a random non-empty subset of the format names, so two random
 * attributions differ in which inner `format` keys they carry — exercising the per-inner-key set/remove/merge
 * of {@link mergeAttr}/{@link diffDim} on the diff round-trip.
 *
 * @param {prng.PRNG} gen
 * @param {boolean} withFormat include the children-only `format`/`formatAt` part
 * @return {Attribution|null|undefined}
 */
const randomAttribution = (gen, withFormat) => {
  const r = prng.uint32(gen, 0, 5)
  if (r <= 2) return undefined // ~half: skip (no attribution change)
  if (r === 3) return null // clear all attribution
  const user = () => [prng.oneOf(gen, ['alice', 'bob', 'carol'])]
  /** @type {Attribution} */
  const attribution = prng.bool(gen)
    ? { insert: user(), insertAt: prng.uint32(gen, 0, 100) }
    : { delete: user(), deleteAt: prng.uint32(gen, 0, 100) }
  if (withFormat && prng.bool(gen)) {
    // a random non-empty subset of format names (never an empty `format`, per the storage invariant)
    /** @type {Record<string, string[]>} */
    const format = {}
    for (const name of ['bold', 'italic', 'under']) { if (prng.bool(gen)) format[name] = user() }
    if (!object.isEmpty(format)) {
      attribution.format = format
      attribution.formatAt = prng.uint32(gen, 0, 100)
    }
  }
  return attribution
}

/**
 * @template {DeltaConf} Conf
 * @param {prng.PRNG} gen
 * @param {s.Schema<Delta<Conf>>} $d
 * @param {object} conf
 * @param {DeltaAny?} [conf.source]
 * @param {number} [conf.minChildOps]
 * @param {number} [conf.maxChildOps]
 * @param {boolean} [conf.attribution] generate random attributions (off by default; consumes no PRNG
 * draws when off, so existing seeds are unaffected). Rebase does not converge attributions, so leave
 * this off for rebase-convergence fuzzing and enable it for diff fuzzing.
 * @return {DeltaBuilder<Conf>}
 */
export const random = (gen, $d, conf = {}) => {
  const { source = null, minChildOps = 1, maxChildOps = 9, attribution = false } = conf
  let sourceLen = source == null ? 0 : source.childCnt
  const { $name, $attrs, $children, hasText, $formats: $formats_ } = /** @type {$Delta<any>} */ (/** @type {any} */ ($d)).shape
  const d = s.$$any.check($name) ? create($deltaAny) : create(s.random(gen, $name), $deltaAny)
  const $formats = s.$$any.check($formats_) ? s.$null : $formats_
  const genAttribution = /** @param {boolean} withFormat */ withFormat => attribution ? randomAttribution(gen, withFormat) : undefined
  // set random attrs (node attribute ops carry the insert/delete part of an attribution only — no format part)
  prng.bool(gen) && d.setAttrs(s.random(gen, $attrs, random), genAttribution(false))
  // delete a single attr
  if (source && !object.isEmpty(source.attrs) && prng.bool(gen)) {
    d.deleteAttr(prng.oneOf(gen, object.keys(source.attrs)), genAttribution(false))
  }
  for (let i = prng.uint32(gen, minChildOps, maxChildOps); i > 0; i--) {
    /**
     * @type {Array<() => void>}
     */
    const possibleOps = []
    if (hasText) {
      possibleOps.push(() => {
        d.insert(prng.oneOf(gen, ['a', 'b', ' ', '\n', '.']), s.random(gen, $formats), genAttribution(true))
      })
    }
    if (!s.$$never.check($children)) {
      possibleOps.push(() => {
        /**
         * @type {Array<any>}
         */
        const ins = []
        let insN = prng.int32(gen, 0, 5)
        while (insN--) {
          ins.push(s.random(gen, $children, random))
        }
        d.insert(ins, s.random(gen, $formats), genAttribution(true))
      })
    }
    if (sourceLen > 0) {
      possibleOps.push(() => {
        const len = prng.uint32(gen, 1, sourceLen)
        sourceLen -= len
        d.delete(len)
      })
      possibleOps.push(() => {
        const len = prng.uint32(gen, 1, sourceLen)
        sourceLen -= len
        // a retain's format is a skip (`undefined`) or a per-key set object — never a blanket `null` clear
        // ($null formats coerce to skip). Blanket clears are a local-only utility and discouraged on a
        // channel (they "win" a rebase, dropping concurrent edits — see Formats); the diff fuzz
        // already exercises the recommended per-key `{k:null}` removals (which converge without data loss).
        d.retain(len, prng.bool(gen) ? undefined : (s.random(gen, $formats) ?? undefined), genAttribution(true))
      })
      // if the op we currently point at is a node, it's also a choice to modify it: find the child
      // schema that matches the node and recursively generate a change against it.
      const src = /** @type {DeltaAny} */ (source)
      const node = childNodeAt(src, src.childCnt - sourceLen)
      const $nodeMatches = node != null ? matchingNodeSchemas($children, node) : []
      if ($nodeMatches.length > 0) {
        possibleOps.push(() => {
          d.modify(random(gen, prng.oneOf(gen, $nodeMatches), { source: node }), undefined, genAttribution(true))
          sourceLen -= 1
        })
      }
    }
    if (possibleOps.length > 0) {
      prng.oneOf(gen, possibleOps)()
    } else {
      break
    }
  }
  return /** @type {any} */ (d)
}

/**
 * @overload
 * @return {DeltaBuilder<{}>}
 */
/**
 * @template {string|null} NodeName
 * @overload
 * @param {NodeName} nodeName
 * @return {DeltaBuilder<NodeName extends string ? { name: NodeName } : {}>}
 */
/**
 * @template {string} NodeName
 * @template {s.Schema<DeltaAny>} Schema
 * @overload
 * @param {NodeName} nodeName
 * @param {Schema} schema
 * @return {Schema extends s.Schema<Delta<infer Conf>> ? DeltaBuilder<Conf, true> : never}
 */
/**
 * @template {s.Schema<DeltaAny>} Schema
 * @overload
 * @param {Schema} schema
 * @return {Schema extends s.Schema<Delta<infer Conf>> ? DeltaBuilder<Conf, true> : never}
 */
/**
 * The conf produced by the condensed factories ({@link create}'s 3-arg form and {@link from}) -
 * the whole node declared in one call instead of built up with chained `setAttr`/`insert`. Routed
 * through {@link DeltaConfOverwrite} - and hence `PrettifyDeltaConf`/`_SanifyDelta` - so a condensed
 * call yields exactly the conf its `create(name).setAttrs(attrs).insert(children)`
 * equivalent would: delta-valued attrs/children normalize to `Delta<..>` (not `DeltaBuilder<..>`),
 * and an unsupplied slot is omitted rather than declared as `children: never` / `text: false`.
 *
 * The `[X] extends [never]` bracketing is load-bearing: `Extract<never,Array<any>>` is `never`, and
 * a bare `never extends Array<infer Ac>` matches with `Ac = never`.
 *
 * @template {string|null} NodeName
 * @template {{[k:string|number]:any}|null} Attrs
 * @template {Array<any>|string} Children
 * @typedef {DeltaConfOverwrite<
 *   NodeName extends string ? { name: NodeName } : {},
 *   (Attrs extends null ? {} : { attrs: Attrs })
 *   & ([Extract<Children,Array<any>>] extends [never] ? {} : (Extract<Children,Array<any>> extends Array<infer Ac> ? (unknown extends Ac ? {} : { children: Ac }) : {}))
 *   & ([Extract<Children,string>] extends [never] ? {} : { text: true })
 * >} CondensedDeltaConf
 */

/**
 * @template {string|null} NodeName
 * @template {{[k:string|number]:any}|null} Attrs
 * @template {Array<any>|string} [Children=never]
 * @overload
 * @param {NodeName} nodeName
 * @param {Attrs} attrs
 * @param {Children} [children]
 * @return {DeltaBuilder<CondensedDeltaConf<NodeName,Attrs,Children>>}
 */
/**
 * @param {string|s.Schema<DeltaAny>|null} [nodeNameOrSchema]
 * @param {{[K:string|number]:any}|s.Schema<DeltaAny>} [attrsOrSchema]
 * @param {(Array<any>|string)} [children]
 * @return {DeltaBuilder<{}>}
 */
export const create = (nodeNameOrSchema, attrsOrSchema, children) => {
  const nodeName = /** @type {any} */ (s.$string.check(nodeNameOrSchema) ? nodeNameOrSchema : null)
  const schema = /** @type {any} */ (s.$$schema.check(nodeNameOrSchema) ? nodeNameOrSchema : (s.$$schema.check(attrsOrSchema) ? attrsOrSchema : null))
  const d = /** @type {DeltaBuilder<any>} */ (new DeltaBuilder(nodeName, schema))
  if (s.$objectAny.check(attrsOrSchema)) {
    d.setAttrs(attrsOrSchema)
  }
  children && d.insert(children)
  return d
}

/**
 * Returns `any` because the resulting conf is computed by the call signatures of {@link from} at
 * the type level only - as in a ts overload implementation, the body can't be checked against
 * each of them.
 *
 * @param {...any} args
 * @return {any}
 */
const _from = (...args) => {
  const hasName = s.$string.check(args[0])
  let i = hasName ? 1 : 0
  const d = create(hasName ? /** @type {string} */ (args[0]) : null)
  if (s.$objectAny.check(args[i])) {
    d.setAttrs(args[i++])
  }
  for (; i < args.length; i++) {
    d.insert(args[i])
  }
  return d
}

/**
 * Build a delta from positional args: an optional node name, optional attrs, then any number of
 * children (each either a string or an array of embeds) - see {@link create} for the fixed-arity
 * condensed form.
 *
 * Typed as a set of call signatures instead of `@overload` tags, because a rest parameter can't be
 * declared consistently in an `@overload` tag: the checker only turns `@param {...X} children`
 * into `X[]` if the implementation's rest parameter has the same name, otherwise it takes `X` as
 * the type of the whole rest array - while the declaration emit always writes `...children: X[]`.
 * lib0's source check and the consumers of the emitted declarations hence saw different
 * signatures. `Children` is the tuple of all children arguments, so that string and array children
 * can be mixed (`Children[number]` is the union of them).
 *
 * @type {{
 *   <NodeName extends string|null, Children extends Array<Array<any>|string> = []>(nodeName: NodeName, ...children: Children): DeltaBuilder<CondensedDeltaConf<NodeName,null,Children[number]>>,
 *   <Children extends Array<Array<any>|string> = []>(...children: Children): DeltaBuilder<CondensedDeltaConf<null,null,Children[number]>>,
 *   <Attrs extends {[k:string|number]:any}|null, Children extends Array<Array<any>|string> = []>(attrs: Attrs, ...children: Children): DeltaBuilder<CondensedDeltaConf<null,Attrs,Children[number]>>,
 *   <NodeName extends string|null, Attrs extends {[k:string|number]:any}|null, Children extends Array<Array<any>|string> = []>(nodeName: NodeName, attrs: Attrs, ...children: Children): DeltaBuilder<CondensedDeltaConf<NodeName,Attrs,Children[number]>>
 * }}
 */
export const from = _from

/**
 * Shorthand for `delta.create().insert(..)` — see {@link DeltaBuilder#insert}.
 *
 * ## Builder shorthands
 *
 * A changeset is written as a chain (see the readme's "condensed vs. builder chain"), and the
 * `create()` head of that chain carries no information. Every content/attr builder method is
 * therefore also exported standalone — {@link insert}, {@link modify}, {@link retain},
 * {@link delete_} (trailing underscore: `delete` is a reserved word),
 * {@link setAttr}, {@link setAttrs}, {@link deleteAttr}, {@link modifyAttr} — each returning the very
 * same {@link DeltaBuilder}, so the rest of the chain is unchanged:
 *
 *     delta.retain(5).delete(6).insert('!') // ⇔ delta.create().retain(5).delete(6).insert('!')
 *     delta.delete_(6)                      // ⇔ delta.create().delete(6)
 *
 * There is deliberately no `format` shorthand: formatting is the second argument of
 * `insert`/`retain`/`modify` (`delta.retain(5, { bold: true })`). The ambient-context setters
 * (`useFormats` & co), the mark methods, `apply`/`append`/`rebase` and the terminal methods (`done`,
 * `toJSON`, ..) are not mirrored — they either say nothing on an empty delta or don't build one.
 *
 * Each shorthand is typed by indexing the method off {@link DeltaBuilder}, pinned to `create()`'s
 * zero-arg overload — so a shorthand and its `create()`-headed equivalent infer the identical conf,
 * with no re-declared generics to drift. The type arguments must be written out
 * (`DeltaBuilder<{}, false>['insert']`, not `DeltaBuilder['insert']`): a bare reference to a generic
 * type in JSDoc resolves its parameters to `any` rather than to the declared defaults, and
 * `FixedConf = any` satisfies both arms of `insert`'s `FixedConf extends true ? ..` branch, which
 * collapses the accreted conf.
 *
 * @type {DeltaBuilder<{}, false>['insert']}
 */
export const insert = (content, formatting, attribution) => create().insert(content, formatting, attribution)

/**
 * Shorthand for `delta.create().modify(..)` — see {@link DeltaBuilder#modify} and {@link insert}.
 *
 * @type {DeltaBuilder<{}, false>['modify']}
 */
export const modify = (modify, formatting, attribution) => create().modify(modify, formatting, attribution)

/**
 * Shorthand for `delta.create().retain(..)` — see {@link DeltaBuilder#retain} and {@link insert}.
 *
 * @type {DeltaBuilder<{}, false>['retain']}
 */
export const retain = (len, format, attribution) => create().retain(len, format, attribution)

/**
 * Shorthand for `delta.create().delete(..)` — see {@link DeltaBuilder}'s `delete` and {@link insert}.
 *
 * Trailing underscore because `delete` is a reserved word: `export const delete` is a syntax error,
 * and the rename clause that would work at runtime (`export { _delete as delete }`) makes `tsc` 6
 * emit a duplicated specifier into the `.d.ts` (`export { _delete as delete, _delete as delete }`),
 * which then fails `npm run check-dist-types` with "Duplicate identifier". The bug is specific to
 * declaration emit from a `.js` file — the same clause emits correctly from a `.ts` — so it cannot
 * be worked around in source; re-test it when the TypeScript floor moves.
 *
 * @type {DeltaBuilder<{}, false>['delete']}
 */
export const delete_ = len => create().delete(len)

/**
 * Shorthand for `delta.create().setAttr(..)` — see {@link DeltaBuilder#setAttr} and {@link insert}.
 *
 * @type {DeltaBuilder<{}, false>['setAttr']}
 */
export const setAttr = (key, val, attribution) => create().setAttr(key, val, attribution)

/**
 * Shorthand for `delta.create().setAttrs(..)` — see {@link DeltaBuilder#setAttrs} and {@link insert}.
 *
 * @type {DeltaBuilder<{}, false>['setAttrs']}
 */
export const setAttrs = (attrs, attribution) => create().setAttrs(attrs, attribution)

/**
 * Shorthand for `delta.create().deleteAttr(..)` — see {@link DeltaBuilder#deleteAttr} and {@link insert}.
 *
 * @type {DeltaBuilder<{}, false>['deleteAttr']}
 */
export const deleteAttr = (key, attribution) => create().deleteAttr(key, attribution)

/**
 * Shorthand for `delta.create().modifyAttr(..)` — see {@link DeltaBuilder#modifyAttr} and {@link insert}.
 *
 * @type {DeltaBuilder<{}, false>['modifyAttr']}
 */
export const modifyAttr = (key, modify, attribution) => create().modifyAttr(key, modify, attribution)

/**
 * An array item that is a string, wrapped so the content passes treat it as one unit (a raw string token
 * would be split by the separators) and fingerprint it once.
 */
class _DiffStringWrapper {
  /**
   * @param {string} str
   */
  constructor (str) {
    this.str = str
    /**
     * @type {string?}
     */
    this._fingerprint = null
  }

  [fingerprintTrait.FingerprintTraitSymbol] () {
    return this._fingerprint || (this._fingerprint = fingerprintTrait.fingerprint(this.str))
  }
}

/*
 * # Diffing two states — how `diff` chooses among the possible diffs
 *
 * `diff(d1, d2, options)` computes a change delta that turns the *state* `d1` (insert-only) into the
 * state `d2`, with the round-trip guarantee `clone(d1).apply(diff(d1, d2)).equals(d2)`.
 *
 * ## Intentions
 *
 * A diff between two states is rarely unique. Deleting one of the `x`s in `xx` can be expressed as
 * "delete index 0" or "delete index 1" — both are correct, both are minimal. A content-only diff breaks
 * such ties by accident of its implementation (a greedy common-prefix scan that is blind to everything
 * but characters), and that accident is wrong in two ways that matter for collaborative editing:
 *
 * 1. **Provenance and formatting.** `[bold x][x]` → `[x]` would come out as "un-bold the first x, delete
 *    the second" instead of "delete the bold x". With attribution instead of bold this *rewrites who wrote
 *    the surviving character* — it strips alice's provenance from a character she still owns and deletes
 *    one that was never hers. Content-wise both diffs are the same size; only one of them is true.
 * 2. **Cursors.** When a user deletes the *second* `a` of `aaa`, a content-only diff reports "delete
 *    index 2". Remote cursors are transformed against that report, so a caret sitting after the second `a`
 *    does not move although the character before it vanished. The editor that produced `d2` knows where
 *    the edit happened; `options.hint` lets it say so.
 *
 * The goal is *correct always, decent most of the time* — never a wrong diff, usually the natural one,
 * minimality preferred but not guaranteed — at low overhead: no post-processing, nothing proportional to
 * the document beyond the alignment itself.
 *
 * ## How it works
 *
 * The key observation: for a single edit, the *only* decision that places the change is the common
 * prefix/suffix strip. Patience (the alignment used for the interior) never sees a pure insertion or
 * deletion — by the time it runs, one side of the interior is empty and there is nothing left to align.
 * So the tie-breaking lives in the strip; everything after it is a plain content diff.
 *
 * 1. **Fingerprint short-circuit.** Equal fingerprints (content + format + attribution, marks excluded)
 *    ⇒ empty diff.
 * 2. **Styled, hint-capped strip** ({@link commonLen}). Both states are walked in lockstep, unit by unit
 *    (a character or an array item is one unit), from the start for the prefix and from the end for the
 *    suffix:
 *    - two ops with equal fingerprints are skipped whole (the fast path for large documents);
 *    - inside two ops that differ, units keep matching only while the ops carry the **same format and
 *      attribution**. Style is uniform per op, so that is one comparison per op pair, and it is what makes
 *      `[bold x][x]` → `[x]` strip the trailing plain `x` as a suffix and leave the bold one to be deleted;
 *    - a hint caps the prefix at the offset where the caller says the change starts; the greedy suffix
 *      then does the rest, so a single pure hunk lands exactly at the hint;
 *    - a text match that stops in the middle of both ops (not at the cap, not at an op boundary) is
 *      committed only up to the last line break. Without this the strip would swallow the head of a
 *      line that the line-level pass wanted as an anchor and produce a worse alignment (e.g.
 *      `- item 1\n- item 2\n` → `- item 1\n- item 3\n- item 2\n` must insert a whole line, not
 *      `3\n- item ` somewhere in the middle). Op boundaries and the hint are real boundaries, line breaks
 *      are what the first content pass splits on, so those are the only places a partial match commits.
 *      Inside a single line the strip commits nothing on its own and the content passes decide.
 * 3. **Content passes.** The interior (what the strip did not consume) is tokenised — text joined across
 *    ops, array items one token each — and aligned by three patience passes
 *    ({@link diffChangesetWithSeparator}: lines, then words, then characters). When the
 *    hint bound the prefix, the hunk that starts there is diffed with patience's `leftmost` placement, so
 *    its first sub-hunk stays at the hint even when the interior holds several edits (the passes' own
 *    greedy prefix strip would otherwise push it to the end of its run again — `aaa bbb` → `aa bb` with a
 *    hint at 1 must delete index 1, not 2).
 * 4. **Changeset → ops.** Retains, deletes and inserts are emitted; nested deltas that `options.compare`
 *    pairs become `modify(diff(child1, child2))`. The hint descends into the one child it names.
 * 5. **Styles** are reconciled while the changeset is emitted ({@link applyChangesetToDelta}): a retained
 *    range carries the tri-state format/attribution updates ({@link diffDim}) between the `d1` and `d2`
 *    ops it spans, inserted content carries `d2`'s style. Only the interior is walked — the strip
 *    guarantees the stripped prefix and suffix are style-equal.
 * 6. **Attributes** ({@link applyAttrDiff}): delta-valued attributes that `compare` pairs are diffed
 *    recursively, the rest set/deleted wholesale. The hint descends into the attribute it names.
 *
 * ## The hint
 *
 * `options.hint` is a {@link import('./position.js').Pos position} into `d1` whose `path` names where the
 * change *starts* (`assoc` and `attrs` are ignored): numbers are content offsets, a trailing number is the
 * gap the change starts at, string steps descend into attributes (`[5]`, `[1, 5]` = inside the child at
 * slot 1, `['body', 5]`). It is the `from` of the editor transaction that produced `d2`, in pre-edit
 * coordinates: typing at gap `p` → `[p]` (not the caret *after* typing), backspace at caret `p` →
 * `[p - 1]`, delete-forward at `p` → `[p]`, replacing the selection `[s, e)` → `[s]`. The offset is
 * identical in `d1` and `d2` because it lies on the common prefix. It is an upper bound on the prefix,
 * nothing more: a wrong hint moves the interior boundary and the content passes re-align greedily —
 * placement may shift, correctness never does. A hint past the content, an empty path, or a string step
 * at a children level is simply no hint. When the interior holds several edits (a transaction with more
 * than one step), only the first one is placed by the hint; the rest land where the passes put them.
 *
 * ## Optimisations done, and deliberately not done
 *
 * - Text units are compared by native string equality of galloping slices (grow the chunk while equal,
 *   shrink to the mismatch), never by a per-character JS loop — 10-25 % slower than regex tokenisation on
 *   a 200k-char op — and never by `startsWith`/`endsWith`, which V8 lowers to a per-character loop after
 *   warm-up.
 * - Nested deltas are compared by their cached fingerprint; other array items by `equalityDeep`. Plain
 *   objects have no cached fingerprint (`fingerprint(obj)` re-encodes and hashes every time: ~6 µs each),
 *   so 5k embeds compare in 1 ms instead of 65.
 * - `hint` forwarding allocates nothing when there is no hint.
 * - The strip hands its stop cursors ({@link Cursors}) to the tokeniser and the emit, so nothing in front
 *   of the interior is walked twice (that re-walk was ~1.1 of ~2.6 ms at 20k ops).
 * - Adjacent text ops are joined before tokenisation ({@link interiorTokens}): to the passes, an op
 *   boundary is a style boundary, never a content boundary. So a style-only edit — the interior's text is
 *   identical on both sides — always comes out as retains carrying the style updates, never as a delete
 *   plus re-insert that would synthesise provenance (`insert('lorem lorem lorem', {bold})` →
 *   `'lorem lo'(bold) 're' 'm lo'(bold) 'rem'` is four retains; `[bold foo][ bar foo]` →
 *   `[foo bar ][bold foo]` no longer anchors crosswise on `foo`). The price: op boundaries no longer
 *   supply accidental anchors, so text in which no line, word or character is unique between two edits
 *   (a long line of a few repeated words) can align worse than before — patience's own limitation, which
 *   the per-op tokens used to mask.
 * - NOT done: splitting a hunk. `[bold x][x][italic y][y]` → `xy` is one pure hunk `xy`; the ideal
 *   diff deletes the bold `x` and the italic `y` separately, which no strip can express.
 *
 * ## Cost
 *
 * O(ops skipped whole + units matched inside the boundary ops + the content diff of the interior). A
 * single edit in a 200k-char op costs ~0.1 ms, one edit in 20k small formatted ops ~1.6 ms — of which the
 * strip's two lockstep walks are ~1.4 (≈35 ns per op visit), the floor without an index over the ops.
 * All of that assumes cached op fingerprints:
 * a freshly built state pays for them on the first read (the top-level short-circuit forces every op's
 * fingerprint, ~6 µs each — ~135 ms for 20k ops), which predates this algorithm and dwarfs the diff
 * itself. Measured numbers: `testDiffBenchmark` in `delta.test.js`.
 */

/**
 * @typedef {object} DiffOptions
 * @property {(d1: DeltaAny, d2: DeltaAny) => boolean} [compare] Predicate deciding when two nodes
 * are paired into a `modify` (vs. replaced wholesale). Defaults to comparing names
 * (`(d1, d2) => d1.name === d2.name`). Called as `compare(fromNode, toNode)`.
 * @property {boolean} [clone=false] By default `diff` does **not** produce a fully fresh delta: wherever
 * it emits content from `d2` — a wholesale-inserted child node, or a delta-valued attribute — it *shares*
 * that nested delta by reference, so the result aliases `d2`'s subtrees. Set `true` to
 * {@link cloneDeep deep-clone} every such delta (children **and** attributes) as it is included in the
 * output, yielding a result that shares no structure with `d2` (and is safe to hand to something that
 * manipulates deltas in place, e.g. a transformer).
 * @property {import('./position.js').Pos} [hint] Where the change starts, as a position into `d1`: the
 * `path`'s numbers are content offsets (the trailing number is the gap the change starts at, in pre-edit
 * coordinates — the `from` of the editor transaction), string steps descend into attributes; `assoc` and
 * `attrs` are ignored. Places the first edit there when the content allows it (a wrong hint only shifts
 * placement, never correctness) — see "The hint" above.
 */

/**
 * Both ops carry the same format and attribution. Style is uniform per op, so this is decided once per op
 * pair.
 *
 * @param {TextOp|InsertOp<any>} a
 * @param {TextOp|InsertOp<any>} b
 */
const sameStyle = (a, b) => fun.equalityDeep(a.format, b.format) && fun.equalityDeep(a.attribution, b.attribution)

/**
 * Two array-insert items are the same unit. Nested deltas compare by their cached fingerprint; everything
 * else (strings, numbers, embed objects) structurally.
 *
 * @param {any} x
 * @param {any} y
 */
const sameItem = (x, y) => $deltaAny.check(x) ? $deltaAny.check(y) && x.fingerprint === y.fingerprint : fun.equalityDeep(x, y)

/**
 * Count the equal units of `a` and `b` after `ai` / `bi` units were already consumed from the walked end
 * (the start, or the end when `back`), at most `max`. Text runs are compared as native string equality of
 * galloping chunks — grow while equal, shrink to the mismatch — so a long equal run costs a few memcmps.
 *
 * @param {TextOp|InsertOp<any>} a
 * @param {number} ai
 * @param {TextOp|InsertOp<any>} b
 * @param {number} bi
 * @param {number} max
 * @param {boolean} back
 */
const commonUnits = (a, ai, b, bi, max, back) => {
  let k = 0
  if ($textOp.check(a) && $textOp.check(b)) {
    const s1 = a.insert
    const s2 = b.insert
    const aEnd = s1.length - ai
    const bEnd = s2.length - bi
    for (let step = 32; k < max;) {
      const n = math.min(step, max - k)
      if (back ? s1.slice(aEnd - k - n, aEnd - k) === s2.slice(bEnd - k - n, bEnd - k) : s1.slice(ai + k, ai + k + n) === s2.slice(bi + k, bi + k + n)) {
        k += n
        step *= 2
      } else if (n === 1) {
        break
      } else {
        step = n >> 1
      }
    }
  } else {
    const c1 = /** @type {InsertOp<any>} */ (a).insert
    const c2 = /** @type {InsertOp<any>} */ (b).insert
    while (k < max && sameItem(c1[back ? c1.length - 1 - ai - k : ai + k], c2[back ? c2.length - 1 - bi - k : bi + k])) k++
  }
  return k
}

/**
 * Where a lockstep walk of two states stands: the current op of each side and the units of it already
 * consumed from the walked end.
 *
 * @typedef {{ a: ChildrenOpAny?, ai: number, b: ChildrenOpAny?, bi: number }} Cursors
 */

/**
 * Length of the common prefix (`back` = false, walking `.next`) or suffix (`back` = true, walking
 * `.prev`) of two states from the cursors `c`, in content units, at most `max`. `c` is left where the
 * match stopped, so the interior can be walked from there without seeking to it again.
 *
 * Whole ops with equal fingerprints are skipped in one step. Inside two ops that differ, units are matched
 * while both ops carry the same style ({@link sameStyle}). A text match that ends on a content mismatch
 * inside *both* ops is committed only up to its last line break (partial lines are left to the content
 * passes, which split on lines first — see the diffing notes above).
 *
 * @param {Cursors} c
 * @param {number} max
 * @param {boolean} back
 * @return {number}
 */
const commonLen = (c, max, back) => {
  let { a, ai, b, bi } = c
  let len = 0
  while (a !== null && b !== null && len < max) {
    let k = 0
    if (ai === 0 && bi === 0 && len + a.length <= max && a.fingerprint === b.fingerprint) {
      // equal ops (content + style) are skipped whole — unless they aren't content ops at all (two equal
      // deletes): those never match and land in the interior, where they are rejected
      if (!$textOp.check(a) && !$insertOp.check(a)) break
      k = a.length
    } else if (($textOp.check(a) && $textOp.check(b)) || ($insertOp.check(a) && $insertOp.check(b))) {
      // only a text op pairs with a text op and an array insert with an array insert: to the diff, the
      // char `'a'` and the array item `'a'` are different content (`insert('a')` vs `insert(['a'])`)
      k = sameStyle(a, b) ? commonUnits(a, ai, b, bi, math.min(math.min(a.length - ai, b.length - bi), max - len), back) : 0
    }
    if (k === 0) break
    if (len + k < max && ai + k < a.length && bi + k < b.length && $textOp.check(a)) {
      // stopped on a content mismatch inside both ops: commit whole lines only, and stop there
      const s = a.insert
      const start = back ? s.length - ai - k : ai
      const end = start + k
      const nl = back ? s.indexOf('\n', start) : s.lastIndexOf('\n', end - 1)
      k = nl < start || nl >= end ? 0 : back ? end - nl - 1 : nl - start + 1
      max = len + k
    }
    len += k
    ai += k
    bi += k
    if (ai === a.length) {
      a = back ? a.prev : a.next
      ai = 0
    }
    if (bi === b.length) {
      b = back ? b.prev : b.next
      bi = 0
    }
  }
  c.a = a
  c.ai = ai
  c.b = b
  c.bi = bi
  return len
}

/**
 * Push the diff tokens of the `len` content units starting at unit `off` of `op` onto `tokens`: adjacent
 * text ops as one raw string (the content passes split it; a style boundary must not act as a content
 * boundary), array items one token each (strings wrapped so they stay one unit).
 *
 * @param {ChildrenOpAny?} op
 * @param {number} off
 * @param {number} len
 * @param {Array<any>} tokens
 */
const interiorTokens = (op, off, len, tokens) => {
  let text = '' // adjacent text ops are joined: an op boundary is a style boundary, not a content boundary
  for (; op !== null && len > 0; op = op.next, off = 0) {
    const stop = math.min(off + len, op.length)
    if ($textOp.check(op)) {
      text += op.insert.slice(off, stop)
    } else if ($insertOp.check(op)) {
      text.length > 0 && tokens.push(text)
      text = ''
      for (let i = off; i < stop; i++) {
        const item = op.insert[i]
        tokens.push(typeof item === 'string' ? new _DiffStringWrapper(item) : item)
      }
    } else {
      throw error.create('[lib0/delta] diffing deletes unsupported')
    }
    len -= stop - off
  }
  text.length > 0 && tokens.push(text)
}

/**
 * Options for the diff of a nested delta at `step` (a child's content index, or an attribute key): the
 * hint descends into the one child/attribute it names and is dropped for all others.
 *
 * @param {DiffOptions} options
 * @param {number|string} step
 * @return {DiffOptions}
 */
const childOptions = (options, step) => {
  const hint = options.hint
  return hint == null
    ? options
    : { ...options, hint: hint.path[0] === step ? { path: hint.path.slice(1), assoc: hint.assoc } : undefined }
}

/**
 * Diff the node attributes of `d1` and `d2` onto `d`: a delta-valued attribute whose nodes `compare`
 * pairs is diffed recursively (`modifyAttr`, the hint descending when it names the key), anything else
 * is set or deleted wholesale.
 *
 * @param {DeltaBuilderAny} d
 * @param {DeltaAny} d1
 * @param {DeltaAny} d2
 * @param {DiffOptions} options
 */
const applyAttrDiff = (d, d1, d2, options) => {
  const compare = options.compare ?? defaultCompare
  for (const attr2 of d2.attrs) {
    const key = attr2.key
    // @ts-ignore
    const attr1 = d1.attrs[key]
    if (attr1 == null || attr1.fingerprint !== attr2.fingerprint) {
      if ($setAttrOp.check(attr2)) {
        const prevVal = attr1?.value
        const nextVal = attr2.value
        if ($deltaAny.check(prevVal) && $deltaAny.check(nextVal) && compare(prevVal, nextVal)) {
          // modifyAttr carries the *incremental* attribution update (apply merges it onto the target attr
          // op's attribution); the inner diff updates its value.
          // reason: diffDim returns an opaque update (a `{k:null}` removal isn't a canonical Attribution),
          // while modifyAttr's param is typed Attribution for API ergonomics
          d.modifyAttr(key, diff(prevVal, nextVal, childOptions(options, key)), /** @type {Attribution|null|undefined} */ (diffDim(attr1?.attribution, attr2.attribution, true)))
        } else {
          // setAttr replaces the whole attr, so it carries the new attribution as data (object-or-none).
          // `nextVal` is shared from `d2`; deep-clone it when the caller wants an independent result.
          d.setAttr(key, options.clone ? _cloneMaybeDeltaDeep(nextVal) : nextVal, attr2.attribution)
        }
      /* c8 ignore start */
      } else {
        // unreachable: a state (insert-only) delta holds only setAttr ops
        error.unexpectedCase()
      }
      /* c8 ignore stop */
    }
  }
  for (const { key } of d1.attrs) {
    // @ts-ignore
    if (d2.attrs[key] == null) {
      d.deleteAttr(key)
    }
  }
}

/**
 * Compute a delta that, when applied to the state `d1`, produces the state `d2`. Only the children and
 * attributes of `d1` and `d2` are compared; the top-level node names of `d1` and `d2` are *not*. Diffing
 * `<div>a</div>` against `<span>a</span>` is valid and yields an empty diff — they have the same
 * children and attributes, so as far as `diff` is concerned they are equal at the level it cares
 * about. The top-level name is treated as a document-type marker, not as diffable content.
 *
 * Names *are* compared on children: a child node whose name changes between `d1` and `d2` is
 * replaced wholesale (delete + insert), not converted into a `modify` op. Same-name child nodes
 * at aligned positions are paired and recursed into via `modify`.
 *
 * Pairing is decided by `options.compare`, which defaults to comparing names
 * (`(d1, d2) => d1.name === d2.name`). Supply a stricter predicate to tighten the granularity —
 * e.g. to also require their first child to match — in which case nodes that don't satisfy it are
 * replaced wholesale instead. `options` is forwarded to every child diff, so the chosen granularity
 * applies consistently all the way down the tree. `compare` is always called as
 * `compare(fromNode, toNode)` (the node from `d1` first).
 *
 * Where several diffs are equally valid (repeated characters), `diff` prefers the one that leaves the
 * format/attribution of retained content untouched, and `options.hint` — the position where the change
 * starts — places the first edit there (a wrong hint only shifts placement, never correctness). See the
 * diffing notes above for how and why.
 *
 * NOTE: `diff` compares **content only** — it short-circuits on the {@link Delta#fingerprint}, which
 * excludes marks. Two states that differ *only* in their marks diff to an empty change, so cursor marks
 * do NOT cross a `diff` (e.g. a `Binding`'s initial-state sync). Marks survive on the live
 * `apply`/`rebase`/transform path; carrying them across a diff-based boundary needs a separate channel.
 *
 * @template {DeltaConf} Conf
 * @param {Delta<Conf>} d1
 * @param {NoInfer<Delta<Conf>>} d2
 * @param {DiffOptions} [options]
 * @return {Delta<Conf>}
 */
export const diff = (d1, d2, options = {}) => {
  const d = create(d1.name === d2.name ? d1.name : null, $deltaAny)
  if (d1.fingerprint !== d2.fingerprint) {
    const n = d1.childCnt
    const m = d2.childCnt
    const hint = options.hint?.path[0]
    const c = { a: d1.children.start, ai: 0, b: d2.children.start, bi: 0 }
    const prefixLen = commonLen(c, math.min(math.min(n, m), typeof hint === 'number' ? hint : n), false)
    const suffixLen = commonLen({ a: d1.children.end, ai: 0, b: d2.children.end, bi: 0 }, math.min(n, m) - prefixLen, true)
    /**
     * @type {Array<any>}
     */
    const cs1 = []
    /**
     * @type {Array<any>}
     */
    const cs2 = []
    interiorTokens(c.a, c.ai, n - suffixLen - prefixLen, cs1)
    interiorTokens(c.b, c.bi, m - suffixLen - prefixLen, cs2)
    // when the hint bound the prefix, the hunk starting there keeps its leftmost placement through the
    // passes (otherwise their own greedy prefix strip would move it to the end of its run again)
    const leftmostIndex = prefixLen === hint ? prefixLen : -1
    // align the interior: by lines, then by words, then by characters
    let changeset = [{ index: prefixLen, insert: cs2, remove: cs1 }]
    changeset = diffChangesetWithSeparator(changeset, /[\n]+/g, leftmostIndex)
    changeset = diffChangesetWithSeparator(changeset, patience.smartSplitRegex, leftmostIndex)
    changeset = diffChangesetWithSeparator(changeset, /./g, leftmostIndex)
    applyChangesetToDelta(d, changeset, c, prefixLen, n - suffixLen, options)
    applyAttrDiff(d, d1, d2, options)
  }
  return d.done(false)
}

/**
 * The tri-state update that undoes a `format`/`attribution` instruction `update` against the stored
 * value `stored` (object or `null`/none) it was applied to: applying the result after `update` restores
 * `stored`. Merge the instruction onto `stored` the way an apply resolves it ({@link applyDim}), then
 * diff the merged result back to `stored` ({@link diffDim}). `undefined` (skip) inverts to `undefined`.
 *
 * @param {{[k:string]:any}|null|undefined} stored
 * @param {{[k:string]:any}|null|undefined} update
 * @param {boolean} deep `true` for the `attribution` dimension (its `format` key merges per inner key —
 *   see {@link mergeAttr}); `false` for `format` (shallow)
 * @return {{[k:string]:any}|null|undefined}
 */
const inverseDim = (stored, update, deep) => {
  if (update === undefined) return undefined
  const merged = update === null ? null : (deep ? mergeAttr(stored, update, true) : mergeShallow(stored, update, true))
  return diffDim(object.isEmpty(merged) ? null : merged, stored ?? null, deep)
}

/**
 * Compute a delta that undoes `d` — a change that was applied to the settled document state `base`
 * (inserts and set attributes only, e.g. an RDT's final state): `base.apply(d).apply(inverse(d, base))`
 * equals `base`. Inspired by Quill's `Delta#invert`, `base` supplies what the change alone doesn't
 * carry: deleted content is re-inserted from `base` (with its stored format/attribution), an
 * overwritten or deleted attribute is restored to its `base` value, and an inverted `retain`/`modify`
 * instruction is diffed against the `base` op's stored format/attribution.
 *
 * Content only: marks are local/ephemeral cursor state and are not inverted (mirroring {@link diff}).
 * Like {@link diff}, the result *shares* re-inserted children and restored attribute values with `base`.
 *
 * The round-trip holds for changes whose content-consuming ops (`retain`/`delete`/`modify`) stay within
 * `base`'s bounds. Beyond-base ops degrade gracefully: following ops stay positioned, but there is no
 * stored content to restore — and what a final apply *materializes* beyond content (e.g. a format set
 * kept as a trailing formatted retain, or an appended modify) cannot be removed by any change, so such
 * a change is not fully undone.
 *
 * @template {DeltaConf} Conf
 * @param {DeltaAny} d
 * @param {Delta<Conf>} base
 * @return {Delta<Conf>}
 */
export const inverse = (d, base) => {
  const inv = create(d.name === base.name ? d.name : null, $deltaAny)
  for (const op of d.attrs) {
    const baseOp = /** @type {AttrOpAny|undefined} */ ((/** @type {any} */ (base.attrs))[op.key])
    if ($modifyAttrOp.check(op) && $setAttrOp.check(baseOp) && $deltaAny.check(baseOp.value)) {
      // the modify edited base's nested delta in place: undo it there, and undo the attribution
      // instruction it merged onto the attr op
      inv.modifyAttr(op.key, inverse(op.value, baseOp.value), /** @type {Attribution?} */ (inverseDim(baseOp.attribution, op.attribution, true)))
    } else if ($setAttrOp.check(baseOp)) {
      // a set/delete (or a modify that replaced a non-delta value) overwrote a settled attr:
      // restore value + attribution from base
      inv.setAttr(op.key, baseOp.value, baseOp.attribution)
    } else if (!$deleteAttrOp.check(op)) {
      // a set/modify introduced an attr base didn't have: undo by deleting it
      inv.deleteAttr(op.key)
    } // else: deleted an attr base didn't have — nothing to restore
  }
  // walk base's children alongside d's ops: an insert consumes no base positions; retain/delete/modify
  // consume their length in base coordinates
  let baseOp = /** @type {ChildrenOpAny?} */ (base.children.start)
  let baseOffset = 0
  /**
   * Advance the base cursor by `len`, reporting each traversed chunk `(op, start, end)` (stored
   * format/attribution are uniform per op, so per-op chunks are exact); returns the length that
   * extends beyond base's content.
   *
   * @param {number} len
   * @param {(op: TextOp|InsertOp<any>, start: number, end: number) => void} f
   */
  const forBase = (len, f) => {
    while (len > 0 && baseOp != null) {
      const chunk = math.min(len, baseOp.length - baseOffset)
      f(/** @type {TextOp|InsertOp<any>} */ (baseOp), baseOffset, baseOffset + chunk)
      len -= chunk
      baseOffset += chunk
      if (baseOffset === baseOp.length) {
        baseOp = baseOp.next
        baseOffset = 0
      }
    }
    return len
  }
  for (const op of d.children) {
    if ($textOp.check(op) || $insertOp.check(op)) {
      inv.delete(op.length)
    } else if ($retainOp.check(op)) {
      const rest = forBase(op.retain, (bop, start, end) => {
        inv.retain(end - start, inverseDim(bop.format, op.format, false), /** @type {Attribution?} */ (inverseDim(bop.attribution, op.attribution, true)))
      })
      // beyond base there is no stored content to restore — keep following ops positioned (see the
      // beyond-base note in the fn docs)
      rest > 0 && inv.retain(rest)
    } else if ($modifyOp.check(op)) {
      if (baseOp == null) {
        // a modify beyond base edited no stored content — keep the position (mirrors retain-beyond)
        inv.retain(1)
      } else {
        const bop = /** @type {InsertOp<any>} */ (baseOp)
        const node = /** @type {DeltaAny} */ (bop.insert[baseOffset])
        inv.modify(inverse(/** @type {DeltaAny} */ (op.value), node), inverseDim(bop.format, op.format, false), /** @type {Attribution?} */ (inverseDim(bop.attribution, op.attribution, true)))
        forBase(1, () => {})
      }
    } else if ($deleteOp.check(op)) {
      // re-insert the deleted base content with its stored format/attribution
      forBase(op.delete, (bop, start, end) => {
        inv.insert(bop.insert.slice(start, end), bop.format, bop.attribution)
      })
    }
  }
  return inv.done(false)
}

/**
 * Default pairing predicate for {@link diff}: two delta nodes are paired (and recursed into via
 * `modify`) when their names match.
 *
 * @param {DeltaAny} d1
 * @param {DeltaAny} d2
 */
const defaultCompare = (d1, d2) => d1.name === d2.name

/**
 * Content units of a diff token: a text run counts its chars, anything else (an array item) is one unit.
 *
 * @param {string|any} c
 */
const contentLen = c => typeof c === 'string' ? c.length : 1

/**
 * Emit the aligned changeset onto `d` while reconciling styles. `[from, to)` is the interior in `d1`
 * units — the stripped prefix and suffix are style-equal by construction ({@link commonLen}), so only the
 * interior is walked, from the cursors the strip left: a retained range carries the tri-state
 * format/attribution updates ({@link diffDim}) between the `d1` and `d2` ops it spans, inserted content
 * carries `d2`'s style. Nested deltas of a hunk are paired via `options.compare` into `modify` ops (a
 * removed delta pairs with the first inserted delta it compares equal to); the hint descends into the
 * child at the content index it names.
 *
 * @param {DeltaBuilderAny} d
 * @param {Array<{ index: number, remove: Array<any>, insert: Array<any> }>} changeset
 * @param {Cursors} c where the interior starts in `d1` / `d2` (the prefix strip's stop)
 * @param {number} from
 * @param {number} to
 * @param {DiffOptions} options
 */
const applyChangesetToDelta = (d, changeset, c, from, to, options) => {
  const compare = options.compare ?? defaultCompare
  // reason: d1/d2 are insert-only (interiorTokens rejects anything else)
  let a = /** @type {TextOp|InsertOp<any>|null} */ (c.a)
  let b = /** @type {TextOp|InsertOp<any>|null} */ (c.b)
  let aOff = c.ai
  let bOff = c.bi
  // advance a cursor by `len` units (a joined text token may span several ops)
  /** @param {number} len */
  const advanceA = len => {
    while (len > 0) {
      const op = /** @type {TextOp|InsertOp<any>} */ (a)
      const step = math.min(len, op.length - aOff)
      if ((aOff += step) === op.length) {
        a = op.next
        aOff = 0
      }
      len -= step
    }
  }
  /** @param {number} len */
  const advanceB = len => {
    while (len > 0) {
      const op = /** @type {TextOp|InsertOp<any>} */ (b)
      const step = math.min(len, op.length - bOff)
      if ((bOff += step) === op.length) {
        b = op.next
        bOff = 0
      }
      len -= step
    }
  }
  /** @param {number} len */
  const retain = len => {
    while (len > 0 && a !== null && b !== null) {
      const step = math.min(len, math.min(a.length - aOff, b.length - bOff))
      d.retain(step, diffDim(a.format, b.format, false), diffDim(a.attribution, b.attribution, true))
      if ((aOff += step) === a.length) {
        a = a.next
        aOff = 0
      }
      if ((bOff += step) === b.length) {
        b = b.next
        bOff = 0
      }
      len -= step
    }
  }
  /**
   * Emit a delete for the first `len` tokens of `tokens` (removing them). Returns the units removed.
   *
   * @param {Array<any>} tokens
   * @param {number} len
   */
  const remove = (tokens, len) => {
    let units = 0
    for (const tok of tokens.splice(0, len)) {
      const n = contentLen(tok)
      advanceA(n)
      units += n
    }
    d.delete(units)
    return units
  }
  /**
   * Emit inserts for the first `len` tokens of `tokens` (removing them), styled like `d2`. A
   * wholesale-inserted child is a nested delta shared from `d2`; deep-clone it when `options.clone` so the
   * result aliases nothing in `d2` (see {@link DiffOptions}).
   *
   * @param {Array<any>} tokens
   * @param {number} len
   */
  const insert = (tokens, len) => {
    for (const tok of tokens.splice(0, len)) {
      if (typeof tok === 'string') {
        // one insert per `d2` op the text spans, so each carries that op's style
        for (let i = 0; i < tok.length;) {
          const op = /** @type {TextOp} */ (b)
          const step = math.min(tok.length - i, op.length - bOff)
          d.insert(tok.slice(i, i + step), op.format, op.attribution)
          advanceB(step)
          i += step
        }
      } else {
        const op = /** @type {InsertOp<any>} */ (b)
        d.insert([tok instanceof _DiffStringWrapper ? tok.str : (options.clone ? _cloneMaybeDeltaDeep(tok) : tok)], op.format, op.attribution)
        advanceB(1)
      }
    }
  }
  d.retain(from)
  let pos = from // content index in d1 of the next unit to be retained or removed
  for (const c of changeset) {
    retain(c.index - pos)
    pos = c.index
    while (true) {
      const ri = c.remove.findIndex(tok => $deltaAny.check(tok))
      if (ri < 0) break
      const ii = c.insert.findIndex(tok => $deltaAny.check(tok) && compare(c.remove[ri], tok))
      if (ii < 0) {
        pos += remove(c.remove, ri + 1)
        continue
      }
      pos += remove(c.remove, ri)
      insert(c.insert, ii)
      const opA = /** @type {TextOp|InsertOp<any>} */ (a)
      const opB = /** @type {TextOp|InsertOp<any>} */ (b)
      d.modify(diff(c.remove[0], c.insert[0], childOptions(options, pos++)), diffDim(opA.format, opB.format, false), diffDim(opA.attribution, opB.attribution, true))
      advanceA(1)
      advanceB(1)
      c.remove.splice(0, 1)
      c.insert.splice(0, 1)
    }
    pos += remove(c.remove, c.remove.length)
    insert(c.insert, c.insert.length)
  }
  retain(to - pos)
}

/**
 * Refine every hunk of `changeset` by splitting its content at `separator` and diffing the pieces
 * (see {@link patience.diff}). The hunk that starts at `leftmostIndex` is diffed with patience's
 * `leftmost` placement, so its first sub-hunk stays at that index (used by {@link diff} to honour a hint
 * inside a multi-hunk interior); `-1` (the default) leaves every hunk at patience's default placement.
 *
 * @param {Array<{ index: number, remove: Array<any>, insert: Array<any> }>} changeset
 * @param {RegExp} separator
 * @param {number} [leftmostIndex]
 */
export const diffChangesetWithSeparator = (changeset, separator, leftmostIndex = -1) => {
  /**
   * @type {Array<any>}
   */
  const next = []
  changeset.forEach(change => {
    // @todo actually split here
    const cs1 = splitContentArrayByRegexp(change.remove, separator)
    const cs2 = splitContentArrayByRegexp(change.insert, separator)
    const fp1 = cs1.map(c => typeof c === 'string' ? c : fingerprintTrait.fingerprint(c))
    const fp2 = cs2.map(c => typeof c === 'string' ? c : fingerprintTrait.fingerprint(c))
    const changesetF = patience.diff(fp1, fp2, change.index === leftmostIndex)
    const nextChangeset = fillFingerprintDiffFromContent(changesetF, cs1, cs2)
    let prevDiffIndex = 0
    let nextChangeIndex = change.index
    // adjust indexes for actual content length and add results to `next`
    nextChangeset.forEach(c => {
      // adjust equal content
      for (; prevDiffIndex < c.index; prevDiffIndex += 1) {
        nextChangeIndex += contentLen(cs1[prevDiffIndex])
      }
      // need to adjust index with the actual length of the previous items
      c.index = nextChangeIndex
      next.push(c)
    })
  })
  return next
}

/**
 * @param {Array<any>} cs
 * @param {RegExp} regexp
 */
const splitContentArrayByRegexp = (cs, regexp) => {
  /**
   * @type {Array<any>}
   */
  const res = []
  cs.forEach(c => {
    if (typeof c === 'string') {
      // no spread: a separator-free run of ≥ ~125k chars yields as many tokens and overflows the call stack
      arr.appendTo(res, patience.splitByRegexp(c, regexp, true))
    } else {
      res.push(c)
    }
  })
  return res
}

/**
 * @template C
 * @param {Array<{ index: number, remove: Array<string>, insert: Array<string> }>} changeset
 * @param {Array<C>} is
 * @param {Array<C>} should
 * @return {Array<{ index: number, remove: Array<C>, insert: Array<C> }>}
 */
const fillFingerprintDiffFromContent = (changeset, is, should) => {
  // overwrite fingerprinted content with actual content
  for (let i = 0, adj = 0; i < changeset.length; i++) {
    const cd = changeset[i]
    // @ts-ignore
    cd.remove = is.slice(cd.index, cd.index + cd.remove.length)
    // @ts-ignore
    cd.insert = should.slice(cd.index + adj, cd.index + adj + cd.insert.length)
    adj += cd.insert.length - cd.remove.length
  }
  return /** @type {any} */ (changeset)
}