import { AvlMap } from 'sonic-forest/lib/avl/AvlMap';
import { printTree } from 'tree-dump/lib/printTree';
import { PersistedSlice } from './PersistedSlice';
import { Timespan, compare, tss } from '../../../json-crdt-patch/clock';
import { updateRga } from '../../../json-crdt/hash';
import { CONST, updateNum } from '../../../json-hash/hash';
import { SliceHeaderShift, SliceStacking, SliceTupleIndex } from './constants';
import { MarkerSlice } from './MarkerSlice';
import { VecNode } from '../../../json-crdt/nodes';
import { Chars } from '../constants';
import { Anchor } from '../rga/constants';
import { UndEndIterator } from '../../../util/iterator';
import * as schema from './schema';
export class Slices {
    txt;
    set;
    list = new AvlMap(compare);
    rga;
    constructor(
    /** The text RGA. */
    txt, 
    /** The `arr` node, used as a set, where slices are stored. */
    set) {
        this.txt = txt;
        this.set = set;
        this.rga = txt.str;
    }
    ins(range, stacking, type, data, Klass = stacking === SliceStacking.Marker ? MarkerSlice : PersistedSlice) {
        const slicesModel = this.set.doc;
        const set = this.set;
        const api = slicesModel.api;
        const builder = api.builder;
        const stepId = builder.vec();
        const start = range.start.clone();
        const end = range.end.clone();
        stacking = stacking & 0b111;
        const header = (stacking << SliceHeaderShift.Stacking) +
            ((start.anchor & 0b1) << SliceHeaderShift.X1Anchor) +
            ((end.anchor & 0b1) << SliceHeaderShift.X2Anchor);
        const headerId = builder.con(header);
        const x1Id = builder.con(start.id);
        const x2Id = builder.con(compare(start.id, end.id) === 0 ? 0 : end.id);
        const typeId = schema.type(type).build(builder);
        const tupleKeysUpdate = [
            [SliceTupleIndex.Header, headerId],
            [SliceTupleIndex.X1, x1Id],
            [SliceTupleIndex.X2, x2Id],
            [SliceTupleIndex.Type, typeId],
        ];
        if (data !== undefined)
            tupleKeysUpdate.push([SliceTupleIndex.Data, builder.json(data)]);
        builder.insVec(stepId, tupleKeysUpdate);
        const chunkId = builder.insArr(set.id, set.id, [stepId]);
        // TODO: Consider using `s` schema here.
        api.apply();
        const tuple = slicesModel.index.get(stepId);
        const chunk = set.findById(chunkId);
        // TODO: Need to check if split slice text was deleted
        const slice = new Klass(slicesModel, this.txt, chunk, tuple, stacking, start, end);
        this.list.set(chunk.id, slice);
        return slice;
    }
    insMarker(range, type, data) {
        return this.ins(range, SliceStacking.Marker, type, data);
    }
    insMarkerAfter(after, type, data, separator = Chars.BlockSplitSentinel) {
        // TODO: test condition when cursors is at absolute or relative starts
        const txt = this.txt;
        const api = txt.model.api;
        const builder = api.builder;
        /**
         * We skip one clock cycle to prevent Block-wise RGA from merging adjacent
         * characters. We want the marker chunk to always be its own distinct chunk.
         */
        builder.nop(1);
        // TODO: Handle case when marker is inserted at the abs start, prevent abs start/end inserts.
        const textId = builder.insStr(txt.str.id, after, separator);
        api.apply();
        const point = txt.point(textId, Anchor.Before);
        const range = txt.range(point, point.clone());
        return this.insMarker(range, type, data);
    }
    insStack(range, type, data) {
        return this.ins(range, SliceStacking.Many, type, data);
    }
    insOne(range, type, data) {
        return this.ins(range, SliceStacking.One, type, data);
    }
    insErase(range, type, data) {
        return this.ins(range, SliceStacking.Erase, type, data);
    }
    unpack(chunk) {
        const txt = this.txt;
        const model = this.set.doc;
        const tupleId = chunk.data ? chunk.data[0] : undefined;
        if (!tupleId)
            throw new Error('SLICE_NOT_FOUND');
        const tuple = model.index.get(tupleId);
        if (!(tuple instanceof VecNode))
            throw new Error('NOT_TUPLE');
        let slice = PersistedSlice.deserialize(model, txt, chunk, tuple);
        if (slice.isSplit())
            slice = new MarkerSlice(model, txt, chunk, tuple, slice.stacking, slice.start, slice.end);
        return slice;
    }
    get(id) {
        return this.list.get(id);
    }
    del(id) {
        this.list.del(id);
        const set = this.set;
        const api = set.doc.api;
        if (!set.findById(id))
            return;
        // TODO: Is it worth checking if the slice is already deleted?
        api.builder.del(set.id, [tss(id.sid, id.time, 1)]);
        api.apply();
    }
    delSlices(slices) {
        const set = this.set;
        const doc = set.doc;
        const api = doc.api;
        const spans = [];
        for (const slice of slices) {
            if (slice instanceof PersistedSlice) {
                const id = slice.id;
                if (!set.findById(id))
                    continue;
                spans.push(new Timespan(id.sid, id.time, 1));
            }
        }
        if (!spans.length)
            return false;
        api.builder.del(this.set.id, spans);
        api.apply();
        return true;
    }
    size() {
        return this.list._size;
    }
    /**
     * @todo Rename to `each0`.
     */
    iterator0() {
        const iterator = this.list.iterator0();
        return () => iterator()?.v;
    }
    each() {
        return new UndEndIterator(this.iterator0());
    }
    forEach(callback) {
        this.list.forEach((node) => callback(node.v));
    }
    // ----------------------------------------------------------------- Stateful
    _topologyHash = 0;
    hash = 0;
    refresh() {
        const topologyHash = updateRga(CONST.START_STATE, this.set);
        if (topologyHash !== this._topologyHash) {
            this._topologyHash = topologyHash;
            let chunk;
            for (const iterator = this.set.iterator(); (chunk = iterator());) {
                const item = this.list.get(chunk.id);
                if (chunk.del) {
                    if (item)
                        this.list.del(chunk.id);
                }
                else {
                    if (!item)
                        this.list.set(chunk.id, this.unpack(chunk));
                }
            }
        }
        let hash = topologyHash;
        this.list.forEach(({ v: item }) => {
            item.refresh();
            hash = updateNum(hash, item.hash);
        });
        return (this.hash = hash);
    }
    // ---------------------------------------------------------------- Printable
    toStringName() {
        return 'Slices';
    }
    toString(tab = '') {
        return (this.toStringName() +
            printTree(tab, [...this.list.entries()].map(({ v }) => (tab) => v.toString(tab))));
    }
}
//# sourceMappingURL=Slices.js.map