import { printTree } from 'tree-dump/lib/printTree';
import { printBinary } from 'tree-dump/lib/printBinary';
import { first, insertLeft, insertRight, last, next, prev, remove } from 'sonic-forest/lib/util';
import { first2, insert2, last2, next2, prev2, remove2 } from 'sonic-forest/lib/util2';
import { splay } from 'sonic-forest/lib/splay/util';
import { Anchor } from '../rga/constants';
import { OverlayPoint } from './OverlayPoint';
import { MarkerOverlayPoint } from './MarkerOverlayPoint';
import { OverlayRefSliceEnd, OverlayRefSliceStart } from './refs';
import { compare } from '../../../json-crdt-patch/clock';
import { CONST, updateNum } from '../../../json-hash/hash';
import { MarkerSlice } from '../slice/MarkerSlice';
import { UndEndIterator } from '../../../util/iterator';
import { SliceStacking } from '../slice/constants';
const spatialComparator = (a, b) => a.cmpSpatial(b);
/**
 * Overlay is a tree structure that represents all the intersections of slices
 * in the text. It is used to quickly find all the slices that overlap a
 * given point in the text. The overlay is a read-only structure, its state
 * is changed only by calling the `refresh` method, which updates the overlay
 * based on the current state of the text and slices.
 */
export class Overlay {
    txt;
    root = undefined;
    root2 = undefined;
    /** A virtual absolute start point, used when the absolute start is missing. */
    START;
    /** A virtual absolute end point, used when the absolute end is missing. */
    END;
    constructor(txt) {
        this.txt = txt;
        const id = txt.str.id;
        this.START = this.point(id, Anchor.After);
        this.END = this.point(id, Anchor.Before);
    }
    point(id, anchor) {
        return new OverlayPoint(this.txt.str, id, anchor);
    }
    mPoint(marker, anchor) {
        return new MarkerOverlayPoint(this.txt.str, marker.start.id, anchor, marker);
    }
    first() {
        return this.root ? first(this.root) : undefined;
    }
    last() {
        return this.root ? last(this.root) : undefined;
    }
    firstMarker() {
        return this.root2 ? first2(this.root2) : undefined;
    }
    lastMarker() {
        return this.root2 ? last2(this.root2) : undefined;
    }
    /**
     * Retrieve overlay point or the previous one, measured in spacial dimension.
     */
    getOrNextLower(point) {
        if (point.isAbsStart()) {
            const first = this.first();
            if (!first)
                return;
            return first.isAbsStart() ? first : void 0;
        }
        else if (point.isAbsEnd())
            return this.last();
        let curr = this.root;
        let result;
        while (curr) {
            const cmp = curr.cmpSpatial(point);
            if (cmp === 0)
                return curr;
            if (cmp > 0)
                curr = curr.l;
            else {
                const next = curr.r;
                result = curr;
                if (!next)
                    return result;
                curr = next;
            }
        }
        return result;
    }
    /**
     * Retrieve overlay point or the next one, measured in spacial dimension.
     */
    getOrNextHigher(point) {
        if (point.isAbsEnd()) {
            const last = this.last();
            if (!last)
                return;
            return last.isAbsEnd() ? last : void 0;
        }
        else if (point.isAbsStart())
            return this.first();
        let curr = this.root;
        let result;
        while (curr) {
            const cmp = curr.cmpSpatial(point);
            if (cmp === 0)
                return curr;
            if (cmp < 0)
                curr = curr.r;
            else {
                const next = curr.l;
                result = curr;
                if (!next)
                    return result;
                curr = next;
            }
        }
        return result;
    }
    /**
     * Retrieve a {@link MarkerOverlayPoint} at the specified point or the
     * previous one, measured in spacial dimension.
     */
    getOrNextLowerMarker(point) {
        if (point.isAbsStart()) {
            const first = this.firstMarker();
            if (!first)
                return;
            return first.isAbsStart() ? first : void 0;
        }
        else if (point.isAbsEnd())
            return this.lastMarker();
        let curr = this.root2;
        let result;
        while (curr) {
            const cmp = curr.cmpSpatial(point);
            if (cmp === 0)
                return curr;
            if (cmp > 0)
                curr = curr.l2;
            else {
                const next = curr.r2;
                result = curr;
                if (!next)
                    return result;
                curr = next;
            }
        }
        return result;
    }
    /** @todo Rename to `chunks()`. */
    chunkSlices0(chunk, p1, p2, callback) {
        const rga = this.txt.str;
        const strId = rga.id;
        let checkFirstAnchor = p1.anchor === Anchor.After;
        const adjustForLastAnchor = p2.anchor === Anchor.Before;
        let id1 = p1.id;
        const id1IsStr = !compare(id1, strId);
        if (id1IsStr) {
            const first = rga.first();
            if (!first)
                return;
            id1 = first.id;
            checkFirstAnchor = false;
        }
        const id2 = p2.id;
        if (!checkFirstAnchor && !adjustForLastAnchor) {
            return rga.range0(chunk, id1, id2, callback);
        }
        const sid1 = id1.sid;
        const time1 = id1.time;
        const sid2 = id2.sid;
        const time2 = id2.time;
        return rga.range0(undefined, id1, id2, (chunk, off, len) => {
            if (checkFirstAnchor) {
                checkFirstAnchor = false;
                const chunkId = chunk.id;
                if (chunkId.sid === sid1 && chunkId.time + off === time1) {
                    if (len <= 1)
                        return;
                    off += 1;
                    len -= 1;
                }
            }
            if (adjustForLastAnchor) {
                const chunkId = chunk.id;
                if (chunkId.sid === sid2 && chunkId.time + off + len - 1 === time2) {
                    if (len <= 1)
                        return;
                    len -= 1;
                }
            }
            if (callback(chunk, off, len))
                return true;
        });
    }
    points0(after, inclusive) {
        let curr = after ? (inclusive ? after : next(after)) : this.first();
        return () => {
            const ret = curr;
            if (curr)
                curr = next(curr);
            return ret;
        };
    }
    points(after, inclusive) {
        return new UndEndIterator(this.points0(after, inclusive));
    }
    /**
     * Returns all {@link MarkerOverlayPoint} instances in the overlay, starting
     * from the given marker point, not including the marker point itself.
     *
     * If the `after` parameter is not provided, the iteration starts from the
     * first marker point in the overlay.
     *
     * @param after The marker point after which to start the iteration.
     * @returns All marker points in the overlay, starting from the given marker
     *     point.
     */
    markers0(after) {
        let curr = after ? next2(after) : first2(this.root2);
        return () => {
            const ret = curr;
            if (curr)
                curr = next2(curr);
            return ret;
        };
    }
    markers(after) {
        return new UndEndIterator(this.markers0(after));
    }
    /**
     * Returns all {@link MarkerOverlayPoint} instances in the overlay, starting
     * from a give {@link Point}, including any marker overlay points that are
     * at the same position as the given point.
     *
     * @param point Point (inclusive) from which to return all markers.
     * @returns All marker points in the overlay, starting from the given marker
     *     point.
     */
    markersFrom0(point) {
        if (point.isAbsStart())
            return this.markers0(undefined);
        let after = this.getOrNextLowerMarker(point);
        if (after && after.cmp(point) === 0)
            after = prev2(after);
        return this.markers0(after);
    }
    /**
     * Returns a pair of overlay marker points for each pair of adjacent marker
     * points in the overlay, starting from a given point (which may not be a
     * marker). The very first point in the first pair might be `undefined`, if
     * the given point is not a marker. Similarly, the very last point in the last
     * pair might be `undefined`, if the iteration end point is not a marker.
     *
     * @param start Start point of the iteration, inclusive.
     * @param end End point of the iteration. If not provided, the iteration
     *     continues until the end of the overlay.
     * @returns Iterator that returns pairs of overlay points.
     */
    markerPairs0(start, end) {
        const i = this.markersFrom0(start);
        let closed = false;
        let p1;
        let p2 = i();
        if (p2) {
            if (p2.isAbsStart() || !p2.cmp(start)) {
                p1 = p2;
                p2 = i();
            }
            if (end && p2) {
                const cmp = end.cmpSpatial(p2);
                if (cmp <= 0)
                    return () => (closed ? void 0 : ((closed = true), [p1, cmp ? void 0 : p2]));
            }
        }
        return () => {
            if (closed)
                return;
            if (!p2 || p2.isAbsEnd())
                return (closed = true), [p1, p2];
            else if (p2 && end) {
                const cmp = end.cmpSpatial(p2);
                if (cmp <= 0) {
                    closed = true;
                    return [p1, cmp ? void 0 : p2];
                }
            }
            const result = [p1, p2];
            p1 = p2;
            p2 = i();
            return result;
        };
    }
    pairs0(after) {
        const isEmpty = !this.root;
        if (isEmpty) {
            const u = undefined;
            let closed = false;
            return () => (closed ? u : ((closed = true), [u, u]));
        }
        let p1;
        let p2 = after;
        const iterator = this.points0(after);
        return () => {
            const next = iterator();
            const isEnd = !next;
            if (isEnd) {
                if (!p2 || p2.isAbsEnd())
                    return;
                p1 = p2;
                p2 = undefined;
                return [p1, p2];
            }
            p1 = p2;
            p2 = next;
            if (!p1) {
                if (p2 && p2.isAbsStart()) {
                    p1 = p2;
                    p2 = iterator();
                }
            }
            return p1 || p2 ? [p1, p2] : undefined;
        };
    }
    pairs(after) {
        return new UndEndIterator(this.pairs0(after));
    }
    tuples0(after) {
        const iterator = this.pairs0(after);
        return () => {
            const pair = iterator();
            if (!pair)
                return;
            pair[0] ??= this.START;
            pair[1] ??= this.END;
            return pair;
        };
    }
    tuples(after) {
        return new UndEndIterator(this.tuples0(after));
    }
    /**
     * Finds the first point that satisfies the given predicate function.
     *
     * @param predicate Predicate function to find the point, returns true if the
     *     point is found.
     * @returns The first point that satisfies the predicate, or undefined if no
     *     point is found.
     */
    find(predicate) {
        let point = this.first();
        while (point) {
            if (predicate(point))
                return point;
            point = next(point);
        }
        return;
    }
    /**
     * Finds all slices that are contained within the given range. A slice is
     * considered contained if its start and end points are within the range,
     * inclusive (uses {@link Range#contains} method to check containment).
     *
     * @param range The range to search for contained slices.
     * @returns A set of slices that are contained within the given range.
     */
    findContained(range) {
        const result = new Set();
        let point = this.getOrNextLower(range.start) ?? this.first();
        if (!point)
            return result;
        do {
            if (!range.containsPoint(point))
                continue;
            const slices = point.layers;
            const length = slices.length;
            for (let i = 0; i < length; i++) {
                const slice = slices[i];
                if (!result.has(slice) && range.contains(slice))
                    result.add(slice);
            }
            if (point instanceof MarkerOverlayPoint) {
                const marker = point.marker;
                if (marker && !result.has(marker) && range.contains(marker))
                    result.add(marker);
            }
        } while (point && (point = next(point)) && range.containsPoint(point));
        return result;
    }
    /**
     * Finds all slices that overlap with the given range. A slice is considered
     * overlapping if its start or end point is within the range, inclusive
     * (uses {@link Range#containsPoint} method to check overlap).
     *
     * @param range The range to search for overlapping slices.
     * @returns A set of slices that overlap with the given range.
     */
    findOverlapping(range) {
        const result = new Set();
        let point = this.getOrNextLower(range.start) ?? this.first();
        if (!point)
            return result;
        do {
            const slices = point.layers;
            const length = slices.length;
            for (let i = 0; i < length; i++)
                result.add(slices[i]);
            if (point instanceof MarkerOverlayPoint) {
                const marker = point.marker;
                if (marker)
                    result.add(marker);
            }
        } while (point && (point = next(point)) && range.containsPoint(point));
        return result;
    }
    /**
     * Returns a summary of how different slice types overlap with the given range.
     *
     * @param range Range over which to search for slices.
     * @param endOnMarker If set to a positive number, the search will stop after
     *     the given number of marker points have been observed.
     * @returns Summary of the slices in this range. `complete` contains all
     *     "Overwrite" slice types, which overlay the full range, which have not
     *     been removed by "Erase" slice type. `partial` contains all "Overwrite"
     *     slice types, which mark a part of the range, and have not been removed
     *     by "Erase" slice type.
     */
    stat(range, endOnMarker = 10) {
        const { start, end: end_ } = range;
        let end = end_;
        const isSamePoint = start.cmp(end_) === 0;
        if (isSamePoint) {
            end = end.clone();
            end.halfstep(1);
        }
        const after = this.getOrNextLower(start);
        const hasLeadingPoint = !!after;
        const iterator = this.points0(after, true);
        let complete = new Set();
        let partial = new Set();
        let isFirst = true;
        let markerCount = 0;
        for (let point = iterator(); point && point.cmpSpatial(end) < 0; point = iterator()) {
            if (point instanceof MarkerOverlayPoint) {
                markerCount++;
                if (markerCount >= endOnMarker)
                    break;
                continue;
            }
            const current = new Set();
            const layers = point.layers;
            const length = layers.length;
            for (let i = 0; i < length; i++) {
                const slice = layers[i];
                const type = slice.type();
                if (typeof type === 'object')
                    continue;
                const stacking = slice.stacking;
                switch (stacking) {
                    case SliceStacking.One:
                        current.add(type);
                        break;
                    case SliceStacking.Erase:
                        current.delete(type);
                        break;
                }
            }
            if (isFirst) {
                isFirst = false;
                if (hasLeadingPoint)
                    complete = current;
                else
                    partial = current;
                continue;
            }
            for (const type of complete)
                if (!current.has(type)) {
                    complete.delete(type);
                    partial.add(type);
                }
            for (const type of current)
                if (!complete.has(type))
                    partial.add(type);
        }
        return [complete, partial, markerCount];
    }
    /**
     * Returns `true` if the current character is a marker sentinel.
     *
     * @param id ID of the point to check.
     * @returns Whether the point is a marker point.
     */
    isMarker(id) {
        const p = this.txt.point(id, Anchor.Before);
        const op = this.getOrNextLower(p);
        return op instanceof MarkerOverlayPoint && op.id.time === id.time && op.id.sid === id.sid;
    }
    skipMarkers(point, direction) {
        while (true) {
            const isMarker = this.isMarker(point.id);
            if (!isMarker)
                return true;
            const end = point.step(direction);
            if (end)
                break;
        }
        return false;
    }
    // ----------------------------------------------------------------- Stateful
    hash = 0;
    refresh(slicesOnly = false) {
        const txt = this.txt;
        let hash = CONST.START_STATE;
        hash = this.refreshSlices(hash, txt.savedSlices);
        hash = this.refreshSlices(hash, txt.extraSlices);
        hash = this.refreshSlices(hash, txt.localSlices);
        // TODO: Move test hash calculation out of the overlay.
        if (!slicesOnly) {
            // hash = updateRga(hash, txt.str);
            hash = this.refreshTextSlices(hash);
        }
        return (this.hash = hash);
    }
    slices = new Map();
    refreshSlices(state, slices) {
        const oldSlicesHash = slices.hash;
        const changed = oldSlicesHash !== slices.refresh();
        const sliceSet = this.slices;
        state = updateNum(state, slices.hash);
        if (changed) {
            slices.forEach((slice) => {
                let tuple = sliceSet.get(slice);
                if (tuple) {
                    if (slice.isDel && slice.isDel()) {
                        this.delSlice(slice, tuple);
                        return;
                    }
                    const positionMoved = tuple[0].cmp(slice.start) !== 0 || tuple[1].cmp(slice.end) !== 0;
                    if (positionMoved)
                        this.delSlice(slice, tuple);
                    else
                        return;
                }
                tuple = slice instanceof MarkerSlice ? this.insMarker(slice) : this.insSlice(slice);
                this.slices.set(slice, tuple);
            });
            if (slices.size() < sliceSet.size) {
                sliceSet.forEach((tuple, slice) => {
                    const mutSlice = slice;
                    if (mutSlice.isDel) {
                        if (!mutSlice.isDel())
                            return;
                        this.delSlice(slice, tuple);
                    }
                });
            }
        }
        return state;
    }
    insSlice(slice) {
        const x0 = slice.start;
        const x1 = slice.end;
        const [start, isStartNew] = this.upsertPoint(x0);
        const [end, isEndNew] = this.upsertPoint(x1);
        const isCollapsed = x0.cmp(x1) === 0;
        start.refs.push(new OverlayRefSliceStart(slice));
        end.refs.push(new OverlayRefSliceEnd(slice));
        if (isStartNew) {
            const beforeStartPoint = prev(start);
            if (beforeStartPoint)
                start.layers.push(...beforeStartPoint.layers);
        }
        if (!isCollapsed) {
            if (isEndNew) {
                const beforeEndPoint = prev(end);
                if (beforeEndPoint)
                    end.layers.push(...beforeEndPoint.layers);
            }
            let curr = start;
            do
                curr.addLayer(slice);
            while ((curr = next(curr)) && curr !== end);
        }
        else
            start.addMarker(slice);
        return [start, end];
    }
    insMarker(slice) {
        const point = this.mPoint(slice, Anchor.Before);
        const pivot = this.insPoint(point);
        if (!pivot) {
            point.refs.push(slice);
            const prevPoint = prev(point);
            if (prevPoint)
                point.layers.push(...prevPoint.layers);
        }
        return [point, point];
    }
    delSlice(slice, [start, end]) {
        this.slices.delete(slice);
        let curr = start;
        do {
            curr.removeLayer(slice);
            curr.removeMarker(slice);
            curr = next(curr);
        } while (curr && curr !== end);
        start.removeRef(slice);
        end.removeRef(slice);
        if (!start.refs.length)
            this.delPoint(start);
        if (!end.refs.length && start !== end)
            this.delPoint(end);
    }
    /**
     * Retrieve an existing {@link OverlayPoint} or create a new one, inserted
     * in the tree, sorted by spatial dimension.
     */
    upsertPoint(point) {
        const newPoint = this.point(point.id, point.anchor);
        const pivot = this.insPoint(newPoint);
        if (pivot)
            return [pivot, false];
        return [newPoint, true];
    }
    /**
     * Inserts a point into the tree, sorted by spatial dimension.
     * @param point Point to insert.
     * @returns Returns the existing point if it was already in the tree.
     */
    insPoint(point) {
        if (point instanceof MarkerOverlayPoint) {
            this.root2 = insert2(this.root2, point, spatialComparator);
            // if (this.root2 !== point) this.root2 = splay2(this.root2!, point, 10);
        }
        let pivot = this.getOrNextLower(point);
        if (!pivot)
            pivot = first(this.root);
        if (!pivot) {
            this.root = point;
            return;
        }
        else {
            if (pivot.cmp(point) === 0)
                return pivot;
            const cmp = pivot.cmpSpatial(point);
            if (cmp < 0)
                insertRight(point, pivot);
            else
                insertLeft(point, pivot);
        }
        if (this.root !== point)
            this.root = splay(this.root, point, 10);
        return;
    }
    delPoint(point) {
        if (point instanceof MarkerOverlayPoint)
            this.root2 = remove2(this.root2, point);
        this.root = remove(this.root, point);
    }
    leadingTextHash = 0;
    refreshTextSlices(stateTotal) {
        const txt = this.txt;
        const str = txt.str;
        const firstChunk = str.first();
        if (!firstChunk)
            return stateTotal;
        let chunk = firstChunk;
        let marker;
        const i = this.tuples0(undefined);
        let state = CONST.START_STATE;
        for (let pair = i(); pair; pair = i()) {
            const [p1, p2] = pair;
            const id1 = p1.id;
            state = (state << 5) + state + (id1.sid >>> 0) + id1.time;
            let overlayPointHash = CONST.START_STATE;
            chunk = this.chunkSlices0(chunk || firstChunk, p1, p2, (chunk, off, len) => {
                const id = chunk.id;
                overlayPointHash =
                    (overlayPointHash << 5) + overlayPointHash + ((((id.sid >>> 0) + id.time) << 8) + (off << 4) + len);
            });
            state = updateNum(state, overlayPointHash);
            for (const slice of p1.layers)
                state = updateNum(state, slice.hash);
            for (const slice of p1.markers)
                state = updateNum(state, slice.hash);
            p1.hash = overlayPointHash;
            stateTotal = updateNum(stateTotal, overlayPointHash);
            if (p2 instanceof MarkerOverlayPoint) {
                if (marker) {
                    marker.textHash = state;
                }
                else {
                    this.leadingTextHash = state;
                }
                stateTotal = updateNum(stateTotal, state);
                state = CONST.START_STATE;
                marker = p2;
            }
        }
        if (marker instanceof MarkerOverlayPoint) {
            marker.textHash = state;
        }
        else {
            this.leadingTextHash = state;
        }
        return stateTotal;
    }
    // ---------------------------------------------------------------- Printable
    toString(tab = '') {
        const printPoint = (tab, point) => {
            return (point.toString(tab) +
                printBinary(tab, [
                    !point.l ? null : (tab) => printPoint(tab, point.l),
                    !point.r ? null : (tab) => printPoint(tab, point.r),
                ]));
        };
        const printMarkerPoint = (tab, point) => {
            return (point.toString(tab) +
                printBinary(tab, [
                    !point.l2 ? null : (tab) => printMarkerPoint(tab, point.l2),
                    !point.r2 ? null : (tab) => printMarkerPoint(tab, point.r2),
                ]));
        };
        return (`Overlay #${this.hash.toString(36)}` +
            printTree(tab, [
                !this.root ? null : (tab) => printPoint(tab, this.root),
                !this.root2 ? null : (tab) => printMarkerPoint(tab, this.root2),
            ]));
    }
}
//# sourceMappingURL=Overlay.js.map