import { compare, tick, tss, printTs, containsId, Timestamp, } from '../../../json-crdt-patch/clock';
import { isUint8Array } from '@jsonjoy.com/buffers/lib/isUint8Array';
import { printOctets } from '@jsonjoy.com/buffers/lib/printOctets';
import { rSplay, lSplay, llSplay, rrSplay, lrSplay, rlSplay } from 'sonic-forest/lib/splay/util';
import { splay2 } from 'sonic-forest/lib/splay/util2';
import { insert2, remove2 } from 'sonic-forest/lib/util2';
import { ORIGIN } from '../../../json-crdt-patch/constants';
import { printTree } from 'tree-dump/lib/printTree';
import { printBinary } from 'tree-dump/lib/printBinary';
import { UndEndIterator } from '../../../util/iterator';
const compareById = (c1, c2) => {
    const ts1 = c1.id;
    const ts2 = c2.id;
    return ts1.sid - ts2.sid || ts1.time - ts2.time;
};
const updateLenOne = (chunk) => {
    const l = chunk.l;
    const r = chunk.r;
    chunk.len = (chunk.del ? 0 : chunk.span) + (l ? l.len : 0) + (r ? r.len : 0);
};
const updateLenOneLive = (chunk) => {
    const l = chunk.l;
    const r = chunk.r;
    chunk.len = chunk.span + (l ? l.len : 0) + (r ? r.len : 0);
};
const dLen = (chunk, delta) => {
    while (chunk) {
        chunk.len += delta;
        chunk = chunk.p;
    }
};
const next = (curr) => {
    const r = curr.r;
    if (r) {
        curr = r;
        let tmp;
        while ((tmp = curr.l))
            curr = tmp;
        return curr;
    }
    let p = curr.p;
    while (p && p.r === curr) {
        curr = p;
        p = p.p;
    }
    return p;
};
const prev = (curr) => {
    const l = curr.l;
    if (l) {
        curr = l;
        let tmp;
        while ((tmp = curr.r))
            curr = tmp;
        return curr;
    }
    let p = curr.p;
    while (p && p.l === curr) {
        curr = p;
        p = p.p;
    }
    return p;
};
/**
 * @category CRDT Node
 */
export class AbstractRga {
    id;
    root = undefined;
    ids = undefined;
    count = 0;
    constructor(id) {
        this.id = id;
    }
    // --------------------------------------------------------------- Public API
    ins(after, id, content) {
        const rootId = this.id;
        const afterTime = after.time;
        const afterSid = after.sid;
        // TODO: perf: sid equality check is redundant? As it is implied by time equality.
        const isRootInsert = rootId.time === afterTime && rootId.sid === afterSid;
        if (isRootInsert) {
            this.insAfterRoot(after, id, content);
            return;
        }
        let curr = this.ids;
        let chunk = curr;
        while (curr) {
            const currId = curr.id;
            const currIdSid = currId.sid;
            if (currIdSid > afterSid) {
                curr = curr.l2;
            }
            else if (currIdSid < afterSid) {
                chunk = curr;
                curr = curr.r2;
            }
            else {
                const currIdTime = currId.time;
                if (currIdTime > afterTime) {
                    curr = curr.l2;
                }
                else if (currIdTime < afterTime) {
                    chunk = curr;
                    curr = curr.r2;
                }
                else {
                    chunk = curr;
                    break;
                }
            }
        }
        if (!chunk)
            return;
        const atId = chunk.id;
        const atIdTime = atId.time;
        const atIdSid = atId.sid;
        const atSpan = chunk.span;
        if (atIdSid !== afterSid)
            return;
        const offset = afterTime - atIdTime;
        if (offset >= atSpan)
            return;
        const offsetInInsertAtChunk = afterTime - atIdTime;
        this.insAfterChunk(after, chunk, offsetInInsertAtChunk, id, content);
    }
    insAt(position, id, content) {
        if (!position) {
            const rootId = this.id;
            this.insAfterRoot(rootId, id, content);
            return rootId;
        }
        const found = this.findChunk(position - 1);
        if (!found)
            return undefined;
        const [at, offset] = found;
        const atId = at.id;
        const after = offset === 0 ? atId : new Timestamp(atId.sid, atId.time + offset);
        this.insAfterChunk(after, at, offset, id, content);
        return after;
    }
    insAfterRoot(after, id, content) {
        const chunk = this.createChunk(id, content);
        const first = this.first();
        if (!first)
            this.setRoot(chunk);
        else if (compare(first.id, id) < 0)
            this.insertBefore(chunk, first);
        else {
            if (containsId(first.id, first.span, id))
                return;
            this.insertAfterRef(chunk, after, first);
        }
    }
    insAfterChunk(after, chunk, chunkOffset, id, content) {
        const atId = chunk.id;
        const atIdTime = atId.time;
        const atIdSid = atId.sid;
        const atSpan = chunk.span;
        const newChunk = this.createChunk(id, content);
        const needsSplit = chunkOffset + 1 < atSpan;
        if (needsSplit) {
            const idSid = id.sid;
            const idTime = id.time;
            if (atIdSid === idSid && atIdTime <= idTime && atIdTime + atSpan - 1 >= idTime)
                return;
            if (idTime > after.time + 1 || idSid > after.sid) {
                this.insertInside(newChunk, chunk, chunkOffset + 1);
                this.splay(newChunk);
                return;
            }
        }
        this.insertAfterRef(newChunk, after, chunk);
        this.splay(newChunk);
    }
    delete(spans) {
        const length = spans.length;
        for (let i = 0; i < length; i++)
            this.deleteSpan(spans[i]);
        this.onChange();
    }
    deleteSpan(span) {
        const len = span.span;
        const t1 = span.time;
        const t2 = t1 + len - 1;
        const start = this.findById(span);
        if (!start)
            return;
        let chunk = start;
        let last = chunk;
        while (chunk) {
            last = chunk;
            const id = chunk.id;
            const chunkSpan = chunk.span;
            const c1 = id.time;
            const c2 = c1 + chunkSpan - 1;
            if (chunk.del) {
                if (c2 >= t2)
                    break;
                chunk = chunk.s;
                continue;
            }
            const deleteStartsFromLeft = t1 <= c1;
            const deleteStartsInTheMiddle = t1 <= c2;
            if (deleteStartsFromLeft) {
                const deleteFullyContainsChunk = t2 >= c2;
                if (deleteFullyContainsChunk) {
                    chunk.delete();
                    dLen(chunk, -chunk.span);
                    if (t2 <= c2)
                        break;
                }
                else {
                    const range = t2 - c1 + 1;
                    const newChunk = this.split(chunk, range);
                    chunk.delete();
                    updateLenOne(newChunk);
                    dLen(chunk, -chunk.span);
                    break;
                }
            }
            else if (deleteStartsInTheMiddle) {
                const deleteContainsRightSide = t2 >= c2;
                if (deleteContainsRightSide) {
                    const offset = t1 - c1;
                    const newChunk = this.split(chunk, offset);
                    newChunk.delete();
                    newChunk.len = newChunk.r ? newChunk.r.len : 0;
                    dLen(chunk, -newChunk.span);
                    if (t2 <= c2)
                        break;
                }
                else {
                    const right = this.split(chunk, t2 - c1 + 1);
                    const mid = this.split(chunk, t1 - c1);
                    mid.delete();
                    updateLenOne(right);
                    updateLenOne(mid);
                    dLen(chunk, -mid.span);
                    break;
                }
            }
            chunk = chunk.s;
        }
        if (last)
            this.mergeTombstones2(start, last);
    }
    find(position) {
        let curr = this.root;
        while (curr) {
            const l = curr.l;
            const leftLength = l ? l.len : 0;
            let span;
            if (position < leftLength)
                curr = l;
            else if (curr.del) {
                position -= leftLength;
                curr = curr.r;
            }
            else if (position < leftLength + (span = curr.span)) {
                const ticks = position - leftLength;
                const id = curr.id;
                return !ticks ? id : new Timestamp(id.sid, id.time + ticks);
            }
            else {
                position -= leftLength + span;
                curr = curr.r;
            }
        }
        return;
    }
    findChunk(position) {
        let curr = this.root;
        while (curr) {
            const l = curr.l;
            const leftLength = l ? l.len : 0;
            let span;
            if (position < leftLength)
                curr = l;
            else if (curr.del) {
                position -= leftLength;
                curr = curr.r;
            }
            else if (position < leftLength + (span = curr.span)) {
                return [curr, position - leftLength];
            }
            else {
                position -= leftLength + span;
                curr = curr.r;
            }
        }
        return;
    }
    findInterval(position, length) {
        const ranges = [];
        if (!length)
            return ranges;
        let curr = this.root;
        let offset = 0;
        while (curr) {
            const leftLength = curr.l ? curr.l.len : 0;
            if (position < leftLength)
                curr = curr.l;
            else if (curr.del) {
                position -= leftLength;
                curr = curr.r;
            }
            else if (position < leftLength + curr.span) {
                offset = position - leftLength;
                break;
            }
            else {
                position -= leftLength + curr.span;
                curr = curr.r;
            }
        }
        if (!curr)
            return ranges;
        if (curr.span >= length + offset) {
            const id = curr.id;
            ranges.push(tss(id.sid, id.time + offset, length));
            return ranges;
        }
        const len = curr.span - offset;
        const id = curr.id;
        ranges.push(tss(id.sid, id.time + offset, len));
        length -= len;
        curr = next(curr);
        if (!curr)
            return ranges;
        do {
            if (curr.del)
                continue;
            const id = curr.id;
            const span = curr.span;
            if (length <= span) {
                ranges.push(tss(id.sid, id.time, length));
                return ranges;
            }
            ranges.push(tss(id.sid, id.time, span));
            length -= span;
        } while ((curr = next(curr)) && length > 0);
        return ranges;
    }
    /** Rename to .rangeX() method? */
    findInterval2(from, to) {
        const ranges = [];
        this.range0(undefined, from, to, (chunk, off, len) => {
            const id = chunk.id;
            ranges.push(tss(id.sid, id.time + off, len));
        });
        return ranges;
    }
    /**
     * @note All ".rangeX()" method are not performance optimized. For hot paths
     * it is better to hand craft the loop.
     *
     * @param startChunk Chunk from which to start the range. If undefined, the
     *                   chunk containing `from` will be used. This is an optimization
     *                   to avoid a lookup.
     * @param from ID of the first element in the range.
     * @param to ID of the last element in the range.
     * @param callback Function to call for each chunk slice in the range. If it
     *     returns truthy value, the iteration will stop.
     * @returns Reference to the last chunk in the range.
     */
    range0(startChunk, from, to, callback) {
        let chunk = startChunk ? startChunk : this.findById(from);
        if (startChunk)
            while (chunk && !containsId(chunk.id, chunk.span, from))
                chunk = next(chunk);
        if (!chunk)
            return;
        if (!chunk.del) {
            const off = from.time - chunk.id.time;
            const toContainedInChunk = containsId(chunk.id, chunk.span, to);
            if (toContainedInChunk) {
                const len = to.time - from.time + 1;
                callback(chunk, off, len);
                return chunk;
            }
            const len = chunk.span - off;
            if (callback(chunk, off, len))
                return chunk;
        }
        else {
            if (containsId(chunk.id, chunk.span, to))
                return;
        }
        chunk = next(chunk);
        while (chunk) {
            const toContainedInChunk = containsId(chunk.id, chunk.span, to);
            // TODO: fast path for chunk.del
            if (toContainedInChunk) {
                if (!chunk.del)
                    if (callback(chunk, 0, to.time - chunk.id.time + 1))
                        return chunk;
                return chunk;
            }
            if (!chunk.del)
                if (callback(chunk, 0, chunk.span))
                    return chunk;
            chunk = next(chunk);
        }
        return chunk;
    }
    // ---------------------------------------------------------------- Retrieval
    first() {
        let curr = this.root;
        while (curr) {
            const l = curr.l;
            if (l)
                curr = l;
            else
                return curr;
        }
        return curr;
    }
    last() {
        let curr = this.root;
        while (curr) {
            const r = curr.r;
            if (r)
                curr = r;
            else
                return curr;
        }
        return curr;
    }
    lastId() {
        const chunk = this.last();
        if (!chunk)
            return undefined;
        const id = chunk.id;
        const span = chunk.span;
        return span === 1 ? id : new Timestamp(id.sid, id.time + span - 1);
    }
    /** @todo Maybe use implementation from tree utils, if does not impact performance. */
    /** @todo Or better remove this method completely, as it does not require "this". */
    next(curr) {
        return next(curr);
    }
    /** @todo Maybe use implementation from tree utils, if does not impact performance. */
    /** @todo Or better remove this method completely, as it does not require "this". */
    prev(curr) {
        return prev(curr);
    }
    /** Content length. */
    length() {
        const root = this.root;
        return root ? root.len : 0;
    }
    /** Number of chunks. */
    size() {
        return this.count;
    }
    /** Returns the position of the first element in the chunk. */
    pos(chunk) {
        const p = chunk.p;
        const l = chunk.l;
        if (!p)
            return l ? l.len : 0;
        const parentPos = this.pos(p);
        const isRightChild = p.r === chunk;
        if (isRightChild)
            return parentPos + (p.del ? 0 : p.span) + (l ? l.len : 0);
        const r = chunk.r;
        return parentPos - (chunk.del ? 0 : chunk.span) - (r ? r.len : 0);
    }
    chunks0() {
        let chunk = this.first();
        return () => {
            const result = chunk;
            if (chunk)
                chunk = next(chunk);
            return result;
        };
    }
    chunks() {
        return new UndEndIterator(this.chunks0());
    }
    // --------------------------------------------------------------- Insertions
    setRoot(chunk) {
        this.root = chunk;
        this.insertId(chunk);
        this.onChange();
    }
    insertBefore(chunk, before) {
        const l = before.l;
        before.l = chunk;
        chunk.l = l;
        chunk.p = before;
        let lLen = 0;
        if (l) {
            l.p = chunk;
            lLen = l.len;
        }
        chunk.len = chunk.span + lLen;
        dLen(before, chunk.span);
        this.insertId(chunk);
        this.onChange();
    }
    insertAfter(chunk, after) {
        const r = after.r;
        after.r = chunk;
        chunk.r = r;
        chunk.p = after;
        let rLen = 0;
        if (r) {
            r.p = chunk;
            rLen = r.len;
        }
        chunk.len = chunk.span + rLen;
        dLen(after, chunk.span);
        this.insertId(chunk);
        this.onChange();
    }
    insertAfterRef(chunk, ref, left) {
        const id = chunk.id;
        const sid = id.sid;
        const time = id.time;
        let isSplit = false;
        for (;;) {
            const leftId = left.id;
            const leftNextTick = leftId.time + left.span;
            if (!left.s) {
                isSplit = leftId.sid === sid && leftNextTick === time && leftNextTick - 1 === ref.time;
                if (isSplit)
                    left.s = chunk;
            }
            const right = next(left);
            if (!right)
                break;
            const rightId = right.id;
            const rightIdTime = rightId.time;
            const rightIdSid = rightId.sid;
            if (rightIdTime < time)
                break;
            if (rightIdTime === time) {
                if (rightIdSid === sid)
                    return;
                if (rightIdSid < sid)
                    break;
            }
            left = right;
        }
        if (isSplit && !left.del) {
            this.mergeContent(left, chunk.data);
            left.s = undefined;
        }
        else
            this.insertAfter(chunk, left);
    }
    mergeContent(chunk, content) {
        const span1 = chunk.span;
        chunk.merge(content);
        dLen(chunk, chunk.span - span1);
        this.onChange();
        return;
    }
    insertInside(chunk, at, offset) {
        const p = at.p;
        const l = at.l;
        const r = at.r;
        const s = at.s;
        const len = at.len;
        const at2 = at.split(offset);
        at.s = at2;
        at2.s = s;
        at.l = at.r = at2.l = at2.r = undefined;
        at2.l = undefined;
        chunk.p = p;
        if (!l) {
            chunk.l = at;
            at.p = chunk;
        }
        else {
            chunk.l = l;
            l.p = chunk;
            const a = l.r;
            l.r = at;
            at.p = l;
            at.l = a;
            if (a)
                a.p = at;
        }
        if (!r) {
            chunk.r = at2;
            at2.p = chunk;
        }
        else {
            chunk.r = r;
            r.p = chunk;
            const b = r.l;
            r.l = at2;
            at2.p = r;
            at2.r = b;
            if (b)
                b.p = at2;
        }
        if (!p)
            this.root = chunk;
        else if (p.l === at)
            p.l = chunk;
        else
            p.r = chunk;
        updateLenOne(at);
        updateLenOne(at2);
        if (l)
            l.len = (l.l ? l.l.len : 0) + at.len + (l.del ? 0 : l.span);
        if (r)
            r.len = (r.r ? r.r.len : 0) + at2.len + (r.del ? 0 : r.span);
        chunk.len = len + chunk.span;
        const span = chunk.span;
        let curr = chunk.p;
        while (curr) {
            curr.len += span;
            curr = curr.p;
        }
        // TODO: perf: could insert these two ids in one go
        this.insertId(at2);
        this.insertIdFast(chunk);
        this.onChange();
    }
    split(chunk, ticks) {
        const s = chunk.s;
        const newChunk = chunk.split(ticks);
        const r = chunk.r;
        chunk.s = newChunk;
        newChunk.r = r;
        newChunk.s = s;
        chunk.r = newChunk;
        newChunk.p = chunk;
        this.insertId(newChunk);
        if (r)
            r.p = newChunk;
        return newChunk;
    }
    mergeTombstones(ch1, ch2) {
        if (!ch1.del || !ch2.del)
            return false;
        const id1 = ch1.id;
        const id2 = ch2.id;
        if (id1.sid !== id2.sid)
            return false;
        if (id1.time + ch1.span !== id2.time)
            return false;
        ch1.s = ch2.s;
        ch1.span += ch2.span;
        this.deleteChunk(ch2);
        return true;
    }
    mergeTombstones2(start, end) {
        let curr = start;
        while (curr) {
            const nextCurr = next(curr);
            if (!nextCurr)
                break;
            const merged = this.mergeTombstones(curr, nextCurr);
            if (!merged) {
                if (nextCurr === end) {
                    if (nextCurr) {
                        const n = next(nextCurr);
                        if (n)
                            this.mergeTombstones(nextCurr, n);
                    }
                    break;
                }
                curr = curr.s;
            }
        }
        const left = prev(start);
        if (left)
            this.mergeTombstones(left, start);
    }
    rmTombstones() {
        let curr = this.first();
        const list = [];
        while (curr) {
            if (curr.del)
                list.push(curr);
            curr = next(curr);
        }
        for (let i = 0; i < list.length; i++)
            this.deleteChunk(list[i]);
    }
    deleteChunk(chunk) {
        this.deleteId(chunk);
        const p = chunk.p;
        const l = chunk.l;
        const r = chunk.r;
        chunk.id = ORIGIN; // mark chunk as disposed
        // TODO: perf: maybe set .p, .l, .r to undefined to help GC?
        if (!l && !r) {
            if (!p)
                this.root = undefined;
            else {
                if (p.l === chunk)
                    p.l = undefined;
                else
                    p.r = undefined;
            }
        }
        else if (l && r) {
            let mostRightChildFromLeft = l;
            while (mostRightChildFromLeft.r)
                mostRightChildFromLeft = mostRightChildFromLeft.r;
            mostRightChildFromLeft.r = r;
            r.p = mostRightChildFromLeft;
            const rLen = r.len;
            let curr;
            curr = mostRightChildFromLeft;
            if (!p) {
                this.root = l;
                l.p = undefined;
            }
            else {
                if (p.l === chunk)
                    p.l = l;
                else
                    p.r = l;
                l.p = p;
            }
            while (curr && curr !== p) {
                curr.len += rLen;
                curr = curr.p;
            }
        }
        else {
            const child = (l || r);
            child.p = p;
            if (!p)
                this.root = child;
            else if (p.l === chunk)
                p.l = child;
            else
                p.r = child;
        }
    }
    insertId(chunk) {
        this.ids = insert2(this.ids, chunk, compareById);
        this.count++;
        this.ids = splay2(this.ids, chunk);
    }
    insertIdFast(chunk) {
        this.ids = insert2(this.ids, chunk, compareById);
        this.count++;
    }
    deleteId(chunk) {
        this.ids = remove2(this.ids, chunk);
        this.count--;
    }
    findById(after) {
        const afterSid = after.sid;
        const afterTime = after.time;
        let curr = this.ids;
        let chunk = curr;
        while (curr) {
            const currId = curr.id;
            const currIdSid = currId.sid;
            if (currIdSid > afterSid) {
                curr = curr.l2;
            }
            else if (currIdSid < afterSid) {
                chunk = curr;
                curr = curr.r2;
            }
            else {
                const currIdTime = currId.time;
                if (currIdTime > afterTime) {
                    curr = curr.l2;
                }
                else if (currIdTime < afterTime) {
                    chunk = curr;
                    curr = curr.r2;
                }
                else {
                    chunk = curr;
                    break;
                }
            }
        }
        if (!chunk)
            return;
        const atId = chunk.id;
        const atIdTime = atId.time;
        const atIdSid = atId.sid;
        const atSpan = chunk.span;
        if (atIdSid !== afterSid)
            return;
        if (afterTime < atIdTime)
            return;
        const offset = afterTime - atIdTime;
        if (offset >= atSpan)
            return;
        return chunk;
    }
    posById(id) {
        const chunk = this.findById(id);
        if (!chunk)
            return;
        const pos = this.pos(chunk);
        return chunk.del ? pos : pos + (id.time - chunk.id.time);
    }
    /**
     * @param id ID of character to start the search from.
     * @returns Previous ID in the RGA sequence.
     */
    prevId(id) {
        let chunk = this.findById(id);
        if (!chunk)
            return;
        const time = id.time;
        if (chunk.id.time < time)
            return new Timestamp(id.sid, time - 1);
        chunk = prev(chunk);
        if (!chunk)
            return;
        const prevId = chunk.id;
        const span = chunk.span;
        return span > 1 ? new Timestamp(prevId.sid, prevId.time + chunk.span - 1) : prevId;
    }
    spanView(span) {
        const view = [];
        let remaining = span.span;
        const time = span.time;
        let chunk = this.findById(span);
        if (!chunk)
            return view;
        if (!chunk.del) {
            if (chunk.span >= remaining + time - chunk.id.time) {
                const offset = time - chunk.id.time;
                const end = offset + remaining;
                const viewChunk = chunk.view().slice(offset, end);
                view.push(viewChunk);
                return view;
            }
            else {
                const offset = time - chunk.id.time;
                const viewChunk = chunk.view().slice(offset, span.span);
                remaining -= chunk.span - offset;
                view.push(viewChunk);
            }
        }
        while ((chunk = chunk.s)) {
            const chunkSpan = chunk.span;
            if (!chunk.del) {
                if (chunkSpan > remaining) {
                    const viewChunk = chunk.view().slice(0, remaining);
                    view.push(viewChunk);
                    break;
                }
                view.push(chunk.data);
            }
            remaining -= chunkSpan;
            if (remaining <= 0)
                break;
        }
        return view;
    }
    // ---------------------------------------------------------- Splay balancing
    splay(chunk) {
        const p = chunk.p;
        if (!p)
            return;
        const pp = p.p;
        const l2 = p.l === chunk;
        if (!pp) {
            if (l2)
                rSplay(chunk, p);
            else
                lSplay(chunk, p);
            this.root = chunk;
            updateLenOne(p);
            updateLenOneLive(chunk);
            return;
        }
        const l1 = pp.l === p;
        if (l1) {
            if (l2) {
                this.root = llSplay(this.root, chunk, p, pp);
            }
            else {
                this.root = lrSplay(this.root, chunk, p, pp);
            }
        }
        else {
            if (l2) {
                this.root = rlSplay(this.root, chunk, p, pp);
            }
            else {
                this.root = rrSplay(this.root, chunk, p, pp);
            }
        }
        updateLenOne(pp);
        updateLenOne(p);
        updateLenOneLive(chunk);
        this.splay(chunk);
    }
    // ---------------------------------------------------------- Export / Import
    iterator() {
        let curr = this.first();
        return () => {
            const res = curr;
            if (curr)
                curr = next(curr);
            return res;
        };
    }
    ingest(size, next) {
        if (size < 1)
            return;
        const splitLeftChunks = new Map();
        this.root = this._ingest(size, () => {
            const chunk = next();
            const id = chunk.id;
            const key = id.sid + '.' + id.time;
            const split = splitLeftChunks.get(key);
            if (split) {
                split.s = chunk;
                splitLeftChunks.delete(key);
            }
            const nextStampAfterSpan = tick(id, chunk.span);
            splitLeftChunks.set(nextStampAfterSpan.sid + '.' + nextStampAfterSpan.time, chunk);
            return chunk;
        });
    }
    _ingest(size, next) {
        const leftSize = size >> 1;
        const rightSize = size - leftSize - 1;
        const c1 = leftSize > 0 ? this._ingest(leftSize, next) : undefined;
        const c2 = next();
        if (c1) {
            c2.l = c1;
            c1.p = c2;
        }
        const c3 = rightSize > 0 ? this._ingest(rightSize, next) : undefined;
        if (c3) {
            c2.r = c3;
            c3.p = c2;
        }
        updateLenOne(c2);
        // TODO: perf: splay only nodes with hight clock values and which are not tombstones?
        this.insertId(c2);
        return c2;
    }
    // ---------------------------------------------------------------- Printable
    toStringName() {
        return 'AbstractRga';
    }
    toString(tab = '') {
        const view = this.view();
        let value = '';
        if (isUint8Array(view))
            value += ` { ${printOctets(view) || '∅'} }`;
        else if (typeof view === 'string')
            value += `{ ${view.length > 32 ? JSON.stringify(view.substring(0, 32)) + ' …' : JSON.stringify(view)} }`;
        const header = `${this.toStringName()} ${printTs(this.id)} ${value}`;
        return header + printTree(tab, [(tab) => (this.root ? this.printChunk(tab, this.root) : '∅')]);
    }
    printChunk(tab, chunk) {
        return (this.formatChunk(chunk) +
            printBinary(tab, [
                chunk.l ? (tab) => this.printChunk(tab, chunk.l) : null,
                chunk.r ? (tab) => this.printChunk(tab, chunk.r) : null,
            ]));
    }
    formatChunk(chunk) {
        const id = printTs(chunk.id);
        let str = `chunk ${id}:${chunk.span} .${chunk.len}.`;
        if (chunk.del)
            str += ` [${chunk.span}]`;
        else {
            if (isUint8Array(chunk.data))
                str += ` { ${printOctets(chunk.data) || '∅'} }`;
            else if (typeof chunk.data === 'string') {
                const data = chunk.data.length > 32 ? JSON.stringify(chunk.data.substring(0, 32)) + ' …' : JSON.stringify(chunk.data);
                str += ` { ${data} }`;
            }
        }
        return str;
    }
}
//# sourceMappingURL=AbstractRga.js.map