"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Editor = void 0;
const Cursor_1 = require("./Cursor");
const constants_1 = require("../rga/constants");
const util_1 = require("../slice/util");
const EditorSlices_1 = require("./EditorSlices");
const util_2 = require("sonic-forest/lib/util");
const printTree_1 = require("tree-dump/lib/printTree");
const SliceRegistry_1 = require("../registry/SliceRegistry");
const PersistedSlice_1 = require("../slice/PersistedSlice");
const toLine_1 = require("pojo-dump/lib/toLine");
const slice_1 = require("../slice");
const util_3 = require("./util");
const sync_store_1 = require("../../../util/events/sync-store");
const MarkerOverlayPoint_1 = require("../overlay/MarkerOverlayPoint");
const iterator_1 = require("../../../util/iterator");
const json_crdt_patch_1 = require("../../../json-crdt-patch");
const constants_2 = require("../slice/constants");
const model_1 = require("../../../json-crdt/model");
/**
 * For inline boolean ("Overwrite") slices, both range endpoints should be
 * attached to {@link Anchor.Before} as per the Peritext paper. This way, say
 * bold text, automatically extends to include the next character typed as
 * user types.
 *
 * @param range The range to be adjusted.
 */
const makeRangeExtendable = (range) => {
    if (range.end.anchor !== constants_1.Anchor.Before || range.start.anchor !== constants_1.Anchor.Before) {
        const start = range.start.clone();
        const end = range.end.clone();
        start.refBefore();
        end.refBefore();
        range.set(start, end);
    }
};
class Editor {
    constructor(txt) {
        this.txt = txt;
        /**
         * Formatting basic inline formatting which will be applied to the next
         * inserted text. This is a temporary store for formatting which is not yet
         * applied to the text, but will be if the cursor is not moved. This is used
         * for {@link SliceStacking.One} formatting which is set as "pending" when
         * user toggles it while cursor is caret.
         */
        this.pending = new sync_store_1.ValueSyncStore(void 0);
        this.saved = new EditorSlices_1.EditorSlices(txt, txt.savedSlices);
        this.extra = new EditorSlices_1.EditorSlices(txt, txt.extraSlices);
        this.local = new EditorSlices_1.EditorSlices(txt, txt.localSlices);
    }
    getRegistry() {
        let registry = this.registry;
        if (!registry)
            this.registry = registry = SliceRegistry_1.SliceRegistry.withCommon();
        return registry;
    }
    text() {
        return this.txt.strApi().view();
    }
    // ------------------------------------------------------------------ cursors
    addCursor(range = this.txt.rangeAt(0), anchor = constants_2.CursorAnchor.Start) {
        const cursor = this.txt.localSlices.ins(range, constants_2.SliceStacking.Cursor, anchor, undefined, Cursor_1.Cursor);
        return cursor;
    }
    /**
     * Cursor is the the current user selection. It can be a caret or a range. If
     * range is collapsed to a single point, it is a *caret*.
     *
     * Returns the first cursor in the text and removes all other cursors. If
     * there is no cursor, creates one and inserts it at the start of the text.
     * To work with multiple cursors, use `.cursors()` method.
     */
    get cursor() {
        let cursor;
        for (let i, iterator = this.cursors0(); (i = iterator());)
            if (!cursor)
                cursor = i;
            else
                this.local.del(i);
        return cursor ?? this.addCursor();
    }
    cursors0() {
        const iterator = this.txt.localSlices.iterator0();
        return () => {
            while (true) {
                const slice = iterator();
                if (slice instanceof Cursor_1.Cursor)
                    return slice;
                if (!slice)
                    return;
            }
        };
    }
    mainCursor() {
        return this.cursors0()();
    }
    cursors() {
        return new iterator_1.UndEndIterator(this.cursors0());
    }
    forCursor(callback) {
        for (let cursor, i = this.cursors0(); (cursor = i());)
            callback(cursor);
    }
    /**
     * @returns Returns `true` if there is at least one cursor in the document.
     */
    hasCursor() {
        return !!this.cursors0()();
    }
    /**
     * Returns the first cursor in the document, if any.
     *
     * @returns Returns the first cursor in the document, or `undefined` if there
     *     are no cursors.
     */
    getCursor() {
        return this.hasCursor() ? this.cursor : void 0;
    }
    /**
     * @returns Returns the exact number of cursors in the document.
     */
    cursorCount() {
        let cnt = 0;
        for (const i = this.cursors0(); i();)
            cnt++;
        return cnt;
    }
    /**
     * Returns relative count of cursors (cardinality).
     *
     * @returns 0 if there are no cursors, 1 if there is exactly one cursor, 2 if
     *     there are more than one cursor.
     */
    cursorCard() {
        const i = this.cursors0();
        return !i() ? 0 : !i() ? 1 : 2;
    }
    delCursor(cursor) {
        this.local.del(cursor);
    }
    delCursors() {
        for (let cursor, i = this.cursors0(); (cursor = i());)
            this.delCursor(cursor);
    }
    /**
     * Ensures there is no range selection. If user has selected a range,
     * the contents is removed and the cursor is set at the start of the range
     * as caret.
     */
    collapseCursor(cursor) {
        this.delRange(cursor);
        cursor.collapseToStart();
    }
    collapseCursors() {
        for (let i = this.cursors0(), cursor = i(); cursor; cursor = i())
            this.collapseCursor(cursor);
    }
    // ------------------------------------------------------------- text editing
    /**
     * Ensures there is exactly one cursor. If the cursor is a range, contents
     * inside the range is deleted and cursor is collapsed to a single point.
     *
     * @returns A single cursor collapsed to a single point.
     */
    caret() {
        const cursor = this.cursor;
        if (!cursor.isCollapsed())
            this.delRange(cursor);
        return cursor;
    }
    /**
     * Insert inline text at current cursor position. If cursor selects a range,
     * the range is removed and the text is inserted at the start of the range.
     */
    insert0(range, text) {
        if (!text)
            return;
        if (!range.isCollapsed())
            this.delRange(range);
        const after = range.start.clone();
        after.refAfter();
        const txt = this.txt;
        const textId = txt.ins(after.id, text);
        const span = new json_crdt_patch_1.Timespan(textId.sid, textId.time, text.length);
        const shift = text.length - 1;
        const point = txt.point(shift ? (0, json_crdt_patch_1.tick)(textId, shift) : textId, constants_1.Anchor.After);
        if (range instanceof Cursor_1.Cursor)
            range.set(point, point, constants_2.CursorAnchor.Start);
        else
            range.set(point);
        return span;
    }
    /**
     * Inserts text at the cursor positions and collapses cursors, if necessary.
     * Then applies any pending inline formatting to the inserted text.
     */
    insert(text, ranges) {
        const spans = [];
        if (!ranges) {
            if (!this.hasCursor())
                this.addCursor();
            ranges = this.cursors();
        }
        if (!ranges)
            return [];
        for (const range of ranges) {
            const span = this.insert0(range, text);
            if (span)
                spans.push(span);
            const pending = this.pending.value;
            if (pending?.size) {
                this.pending.next(void 0);
                const start = range.start.clone();
                start.step(-text.length);
                const toggleRange = this.txt.range(start, range.end.clone());
                for (const [type, data] of pending)
                    this.toggleRangeExclFmt(toggleRange, type, data);
            }
        }
        return spans;
    }
    /**
     * Deletes the previous character at current cursor positions. If cursors
     * select a range, deletes the whole range.
     */
    del(step = -1) {
        this.delete(step, 'char');
    }
    delRange(range) {
        const txt = this.txt;
        const overlay = txt.overlay;
        const contained = overlay.findContained(range);
        for (const slice of contained)
            if (slice instanceof PersistedSlice_1.PersistedSlice && slice.stacking !== constants_2.SliceStacking.Cursor)
                slice.del();
        txt.delStr(range);
    }
    /**
     * Deletes one or more units of text in all cursors. If cursor is a range,
     * deletes the whole range.
     *
     * @param step Number of units to delete.
     * @param unit A unit of deletion: "char", "word", "line".
     */
    delete(step, unit) {
        const txt = this.txt;
        for (let i = this.cursors0(), cursor = i(); cursor; cursor = i()) {
            if (!cursor.isCollapsed()) {
                this.collapseCursor(cursor);
                return;
            }
            let point1 = cursor.start.clone();
            let point2 = point1.clone();
            if (step > 0)
                point2 = this.skip(point2, step, unit);
            else if (step < 0)
                point1 = this.skip(point1, step, unit);
            else if (step === 0) {
                point1 = this.skip(point1, -1, unit);
                point2 = this.skip(point2, 1, unit);
            }
            const range = txt.range(point1, point2);
            this.delRange(range);
            point1.refAfter();
            cursor.set(point1);
        }
    }
    // ----------------------------------------------------------------- movement
    /**
     * Returns an iterator through visible text, one `step`, one character at a
     * time, starting from a given {@link Point}.
     *
     * @param start The starting point.
     * @param step Number of visible characters to skip.
     * @returns The next visible character iterator.
     */
    walk(start, step = 1) {
        let point = start.clone();
        return () => {
            if (!point)
                return;
            const char = step > 0 ? point.rightChar() : point.leftChar();
            if (!char)
                return (point = undefined);
            const end = point.step(step);
            if (end)
                point = undefined;
            return char;
        };
    }
    /**
     * Returns a forward iterator through visible text, one character at a time,
     * starting from a given {@link Point}.
     *
     * @param start The starting point.
     * @param chunk Chunk to start from.
     * @returns The next visible character iterator.
     */
    fwd(start) {
        return this.walk(start, 1);
    }
    /**
     * Returns a backward iterator through visible text, one character at a time,
     * starting from a given {@link Point}.
     *
     * @param start The starting point.
     * @param chunk Chunk to start from.
     * @returns The previous visible character iterator.
     */
    bwd(start) {
        return this.walk(start, -1);
    }
    /**
     * Skips a word in an arbitrary direction. A word is defined by the `predicate`
     * function, which returns `true` if the character is part of the word.
     *
     * @param iterator Character iterator.
     * @param predicate Predicate function to match characters, returns `true` if
     *     the character is part of the word.
     * @param firstLetterFound Whether the first letter has already been found. If
     *     not, will skip any characters until the first letter, which is matched
     *     by the `predicate` is found.
     * @returns Point after the last skipped character.
     */
    skipWord(iterator, predicate, firstLetterFound) {
        let next;
        let prev;
        while ((next = iterator())) {
            const char = next.view()[0];
            if (firstLetterFound) {
                if (!predicate(char))
                    break;
            }
            else if (predicate(char))
                firstLetterFound = true;
            prev = next;
        }
        if (!prev)
            return;
        return this.txt.point(prev.id(), constants_1.Anchor.After);
    }
    /**
     * Hard skips line, skips to the next "\n" newline character.
     *
     * @param iterator Character iterator.
     * @returns Point after the last skipped character.
     */
    skipLine(iterator) {
        let next;
        let prev;
        while ((next = iterator())) {
            const char = next.view()[0];
            if (char === '\n')
                break;
            prev = next;
        }
        if (!prev)
            return;
        return this.txt.point(prev.id(), constants_1.Anchor.After);
    }
    /**
     * End of word iterator (eow). Skips a word forward. A word is defined by the
     * `predicate` function, which returns `true` if the character is part of the
     * word.
     *
     * @param point Point from which to start skipping.
     * @param predicate Character class to skip.
     * @param firstLetterFound Whether the first letter has already been found. If
     *        not, will skip any characters until the first letter, which is
     *        matched by the `predicate` is found.
     * @returns Point after the last character skipped.
     */
    eow(point, predicate = util_3.isLetter, firstLetterFound = false) {
        return this.skipWord(this.fwd(point), predicate, firstLetterFound) || point;
    }
    /**
     * Beginning of word iterator (bow). Skips a word backward. A word is defined
     * by the `predicate` function, which returns `true` if the character is part
     * of the word.
     *
     * @param point Point from which to start skipping.
     * @param predicate Character class to skip.
     * @param firstLetterFound Whether the first letter has already been found. If
     *        not, will skip any characters until the first letter, which is
     *        matched by the `predicate` is found.
     * @returns Point after the last character skipped.
     */
    bow(point, predicate = util_3.isLetter, firstLetterFound = false) {
        const bwd = this.bwd(point);
        const endPoint = this.skipWord(bwd, predicate, firstLetterFound);
        if (endPoint)
            endPoint.anchor = constants_1.Anchor.Before;
        return endPoint || point;
    }
    /** Find end of line, starting from given point. */
    eol(point) {
        return this.skipLine(this.fwd(point)) || this.end();
    }
    /** Find beginning of line, starting from given point. */
    bol(point) {
        const bwd = this.bwd(point);
        const endPoint = this.skipLine(bwd);
        if (endPoint)
            endPoint.anchor = constants_1.Anchor.Before;
        return endPoint || this.start();
    }
    /**
     * Find end of block, starting from given point. Overlay should be refreshed
     * before calling this method.
     */
    eob(point) {
        const txt = this.txt;
        const overlay = txt.overlay;
        point = point.clone();
        point.halfstep(1);
        if (point.isAbsEnd())
            return point;
        let overlayPoint = overlay.getOrNextHigher(point);
        if (!overlayPoint)
            return this.end();
        if (point.cmp(overlayPoint) === 0)
            overlayPoint = (0, util_2.next)(overlayPoint);
        while (!(overlayPoint instanceof MarkerOverlayPoint_1.MarkerOverlayPoint) && overlayPoint)
            overlayPoint = (0, util_2.next)(overlayPoint);
        if (overlayPoint instanceof MarkerOverlayPoint_1.MarkerOverlayPoint) {
            const point = overlayPoint.clone();
            point.refAfter();
            return point;
        }
        else
            return this.end();
    }
    /**
     * Find beginning of block, starting from given point. Overlay should be
     * refreshed before calling this method.
     */
    bob(point) {
        const overlay = this.txt.overlay;
        point = point.clone();
        point.halfstep(-1);
        if (point.isAbsStart())
            return point;
        let overlayPoint = overlay.getOrNextLower(point);
        if (!overlayPoint)
            return this.start();
        while (!(overlayPoint instanceof MarkerOverlayPoint_1.MarkerOverlayPoint) && overlayPoint)
            overlayPoint = (0, util_2.prev)(overlayPoint);
        if (overlayPoint instanceof MarkerOverlayPoint_1.MarkerOverlayPoint) {
            const point = overlayPoint.clone();
            point.refBefore();
            return point;
        }
        else
            return this.start();
    }
    /**
     * Move a point given number of steps in a specified direction. The unit of
     * one move step is defined by the `unit` parameter.
     *
     * @param point The point to start from.
     * @param steps Number of steps to move. Negative number moves backward.
     * @param unit The unit of move per step: "char", "word", "line", etc.
     * @returns The destination point after the move.
     */
    skip(point, steps, unit, ui) {
        if (!steps)
            return point;
        switch (unit) {
            case 'point': {
                const p = point.clone();
                return p.halfstep(steps), p;
            }
            case 'char': {
                const p = point.clone();
                return p.step(steps), p;
            }
            case 'word': {
                if (steps > 0)
                    for (let i = 0; i < steps; i++)
                        point = this.eow(point);
                else
                    for (let i = 0; i < -steps; i++)
                        point = this.bow(point);
                return point;
            }
            case 'line': {
                if (steps > 0)
                    for (let i = 0; i < steps; i++)
                        point = this.eol(point);
                else
                    for (let i = 0; i < -steps; i++)
                        point = this.bol(point);
                return point;
            }
            case 'vline': {
                if (steps > 0)
                    for (let i = 0; i < steps; i++)
                        point = ui?.eol?.(point, 1) ?? this.eol(point);
                else
                    for (let i = 0; i < -steps; i++)
                        point = ui?.eol?.(point, -1) ?? this.bol(point);
                return point;
            }
            case 'vert': {
                return ui?.vert?.(point, steps) || point;
            }
            case 'block': {
                if (steps > 0)
                    for (let i = 0; i < steps; i++)
                        point = this.eob(point);
                else
                    for (let i = 0; i < -steps; i++)
                        point = this.bob(point);
                return point;
            }
            case 'all':
                return steps > 0 ? this.end() : this.start();
        }
    }
    /**
     * Move all cursors given number of units.
     *
     * @param steps Number of steps to move.
     * @param unit The unit of move per step: "char", "word", "line".
     * @param endpoint 0 for "focus", 1 for "anchor", 2 for both.
     * @param collapse Whether to collapse the range to a single point.
     */
    move(steps, unit, endpoint = 0, collapse = true, ui) {
        this.forCursor((cursor) => {
            switch (endpoint) {
                case 0: {
                    let point = cursor.focus();
                    point = this.skip(point, steps, unit, ui);
                    if (collapse)
                        cursor.set(point);
                    else
                        cursor.setEndpoint(point, 0);
                    break;
                }
                case 1: {
                    let point = cursor.anchor();
                    point = this.skip(point, steps, unit, ui);
                    if (collapse)
                        cursor.set(point);
                    else
                        cursor.setEndpoint(point, 1);
                    break;
                }
                case 2: {
                    const start = this.skip(cursor.start, steps, unit, ui);
                    const end = collapse ? start.clone() : this.skip(cursor.end, steps, unit, ui);
                    cursor.set(start, end);
                    break;
                }
            }
        });
    }
    // ---------------------------------------------------------------- selection
    /**
     * Leaves only the first cursor, and sets it selection to the whole text.
     *
     * @returns Returns `true` if the selection was successful.
     */
    selectAll() {
        const range = this.txt.rangeAll();
        if (!range)
            return false;
        this.cursor.setRange(range);
        return true;
    }
    /**
     * Selects a word by extending the selection to the left and right of the point.
     *
     * @param point Point to the right of which is the starting character of the word.
     * @returns Range which contains the word.
     */
    rangeWord(point) {
        const char = point.rightChar() || point.leftChar();
        if (!char)
            return;
        const c = String(char.view())[0];
        const predicate = (0, util_3.isLetter)(c) ? util_3.isLetter : (0, util_3.isWhitespace)(c) ? util_3.isWhitespace : util_3.isPunctuation;
        const start = this.bow(point, predicate, true);
        const end = this.eow(point, predicate, true);
        return this.txt.range(start, end);
    }
    /**
     * Returns a range by expanding the selection to the left and right of the
     * given point.
     *
     * @param point Point from which to start range expansion.
     * @param unit Unit of the range expansion.
     * @returns Range which contains the specified unit.
     */
    range(point, unit, ui) {
        if (unit === 'word')
            return this.rangeWord(point);
        const point1 = this.skip(point, -1, unit, ui);
        const point2 = this.skip(point, 1, unit, ui);
        return this.txt.range(point1, point2);
    }
    select(unit, ui) {
        this.forCursor((cursor) => {
            const range = this.range(cursor.start, unit, ui);
            if (range)
                cursor.set(range.start, range.end, constants_2.CursorAnchor.Start);
            else
                this.delCursors;
        });
    }
    selectAt(at, unit, ui) {
        this.cursor.set(this.pos2point(at));
        if (unit)
            this.select(unit, ui);
    }
    // --------------------------------------------------------------- formatting
    eraseFormatting(store = this.saved, selection = this.cursors()) {
        const overlay = this.txt.overlay;
        for (const range of selection) {
            overlay.refresh();
            const contained = overlay.findContained(range);
            for (const slice of contained) {
                if (slice instanceof PersistedSlice_1.PersistedSlice) {
                    switch (slice.stacking) {
                        case constants_2.SliceStacking.One:
                        case constants_2.SliceStacking.Many:
                        case constants_2.SliceStacking.Erase:
                            slice.del();
                    }
                }
            }
            overlay.refresh();
            const overlapping = overlay.findOverlapping(range);
            for (const slice of overlapping) {
                switch (slice.stacking) {
                    case constants_2.SliceStacking.One:
                    case constants_2.SliceStacking.Many: {
                        store.insErase(slice.type());
                    }
                }
            }
        }
    }
    clearFormatting(store = this.saved, selection = this.cursors()) {
        const overlay = this.txt.overlay;
        overlay.refresh();
        for (const range of selection) {
            const overlapping = overlay.findOverlapping(range);
            for (const slice of overlapping)
                store.del(slice.id);
        }
    }
    // -------------------------------------------------------- inline formatting
    toggleRangeExclFmt(range, type, data, store = this.saved) {
        if (range.isCollapsed())
            throw new Error('Range is collapsed');
        const txt = this.txt;
        const overlay = txt.overlay;
        const [complete] = overlay.stat(range, 1e6);
        const needToRemoveFormatting = complete.has(type);
        makeRangeExtendable(range);
        const contained = overlay.findContained(range);
        for (const slice of contained)
            if (slice instanceof PersistedSlice_1.PersistedSlice && slice.type() === type)
                slice.del();
        if (needToRemoveFormatting) {
            overlay.refresh();
            const [complete2, partial2] = overlay.stat(range, 1e6);
            const needsErase = complete2.has(type) || partial2.has(type);
            if (needsErase)
                store.slices.insErase(range, type);
        }
        else {
            if (range.start.isAbs()) {
                const start = txt.pointStart();
                if (!start)
                    return;
                if (start.cmpSpatial(range.end) >= 0)
                    return;
                range.start = start;
            }
            if (range.end.isAbs()) {
                const end = txt.pointEnd();
                if (!end)
                    return;
                if (end.cmpSpatial(range.start) <= 0)
                    return;
                range.end = end;
            }
            store.slices.insOne(range, type, data);
        }
    }
    toggleExclFmt(type, data, store = this.saved, selection = this.cursors()) {
        // TODO: handle mutually exclusive slices (<sub>, <sub>)
        this.txt.overlay.refresh();
        for (const range of selection) {
            if (range.isCollapsed()) {
                const pending = this.pending.value ?? new Map();
                if (pending.has(type))
                    pending.delete(type);
                else
                    pending.set(type, data);
                this.pending.next(pending);
                continue;
            }
            this.toggleRangeExclFmt(range, type, data, store);
        }
    }
    // --------------------------------------------------------- block formatting
    /**
     * Returns block split marker of the block inside which the point is located.
     *
     * @param point The point to get the marker at.
     * @returns The split marker at the point, if any.
     */
    getMarker(point) {
        return this.txt.overlay.getOrNextLowerMarker(point)?.marker;
    }
    /**
     * Returns the block type at the given point. Block type is a nested array of
     * tags, e.g. `['p']`, `['blockquote', 'p']`, `['ul', 'li', 'p']`, etc.
     *
     * @param point The point to get the block type at.
     * @returns Current block type at the point.
     */
    getBlockType(point) {
        const marker = this.getMarker(point);
        if (!marker)
            return [[0 /* SliceTypeCon.p */]];
        let steps = marker?.type() ?? [0 /* SliceTypeCon.p */];
        if (!Array.isArray(steps))
            steps = [steps];
        return [steps, marker];
    }
    /**
     * Insert a block split at the start of the document. The start of the
     * document is defined as immediately after all deleted characters starting
     * from the beginning of the document, or as the ABS start of the document if
     * there are no deleted characters.
     *
     * @param type The type of the marker.
     * @returns The inserted marker slice.
     */
    insStartMarker(type) {
        const txt = this.txt;
        const start = txt.pointStart() ?? txt.pointAbsStart();
        start.refAfter();
        return this.txt.savedSlices.insMarkerAfter(start.id, type);
    }
    /**
     * Find the block split marker which contains the point and sets the block
     * type of the marker. If there is no block split marker at the point, a new
     * block split marker is inserted at the beginning of the document with the
     * specified block type.
     *
     * @param point The point at which to set the block type.
     * @param type The new block type.
     * @returns The marker slice at the point, or a new marker slice if there is none.
     */
    setBlockType(point, type) {
        const marker = this.getMarker(point);
        if (marker) {
            marker.update({ type });
            return marker;
        }
        return this.insStartMarker(type);
    }
    getContainerPath(steps) {
        const registry = this.getRegistry();
        const length = steps.length;
        for (let i = length - 1; i >= 0; i--) {
            const step = steps[i];
            const tag = Array.isArray(step) ? step[0] : step;
            const isContainer = registry.isContainer(tag);
            if (isContainer)
                return steps.slice(0, i + 1);
        }
        return [];
    }
    getDeepestCommonContainer(steps1, steps2) {
        const length1 = steps1.length;
        const length2 = steps2.length;
        const min = Math.min(length1, length2);
        for (let i = 0; i < min; i++) {
            const step1 = steps1[i];
            const step2 = steps2[i];
            const tag1 = Array.isArray(step1) ? step1[0] : step1;
            const tag2 = Array.isArray(step2) ? step2[0] : step2;
            const disc1 = Array.isArray(step1) ? step1[1] : 0;
            const disc2 = Array.isArray(step2) ? step2[1] : 0;
            if (tag1 !== tag2 || disc1 !== disc2)
                return i - 1;
            if (!this.getRegistry().isContainer(tag1))
                return i - 1;
        }
        return min;
    }
    /**
     * @param at Point at which split block split happens.
     * @param slices The slices set to use.
     * @returns True if a marker was inserted, false if it was updated.
     */
    splitAt(at, slices = this.saved) {
        const [type, marker] = this.getBlockType(at);
        const prevMarker = marker ? this.getMarker(marker.start.copy((p) => p.halfstep(-1))) : void 0;
        if (marker && prevMarker) {
            const rangeFromMarker = this.txt.range(marker.start, at);
            const noLeadingText = rangeFromMarker.length() <= 1;
            if (noLeadingText) {
                const markerSteps = marker.typeSteps();
                const prevMarkerSteps = prevMarker.typeSteps();
                if (markerSteps.length > 1) {
                    const areMarkerTypesEqual = (0, util_3.stepsEqual)(markerSteps, prevMarkerSteps);
                    if (areMarkerTypesEqual) {
                        const i = this.getDeepestCommonContainer(markerSteps, prevMarkerSteps);
                        if (i >= 0) {
                            const newType = [...markerSteps];
                            const step = newType[i];
                            const tag = Array.isArray(step) ? step[0] : step;
                            const disc = Array.isArray(step) ? step[1] : 0;
                            newType[i] = [tag, (disc + 1) % 8];
                            marker.update({ type: newType });
                            return false;
                        }
                    }
                }
            }
        }
        const containerPath = this.getContainerPath(type);
        const newType = containerPath.concat([slice_1.CommonSliceType.p]);
        slices.insMarker(newType);
        return true;
    }
    split(type, data, selection = this.cursors(), slices = this.saved) {
        if (type === void 0) {
            for (const range of selection) {
                this.collapseCursor(range);
                const didInsertMarker = this.splitAt(range.start, slices);
                if (didInsertMarker && range instanceof Cursor_1.Cursor)
                    range.move(1);
            }
        }
        else {
            for (const range of selection) {
                this.collapseCursor(range);
                if (type === void 0) {
                    // TODO: detect current block type
                    type = slice_1.CommonSliceType.p;
                }
                slices.insMarker(type, data);
                if (range instanceof Cursor_1.Cursor)
                    range.move(1);
            }
        }
    }
    setStartMarker(type, data, slices = this.saved) {
        const after = this.txt.pointStart() ?? this.txt.pointAbsStart();
        after.refAfter();
        return slices.slices.insMarkerAfter(after.id, type, data);
    }
    tglMarkerAt(point, type, data, slices = this.saved, def = 0 /* SliceTypeCon.p */) {
        const overlay = this.txt.overlay;
        const markerPoint = overlay.getOrNextLowerMarker(point);
        if (markerPoint) {
            const marker = markerPoint.marker;
            const markerTag = marker.nestedType().tag().name();
            const tagStep = type[type.length - 1];
            const tag = Array.isArray(tagStep) ? tagStep[0] : tagStep;
            if (markerTag === tag)
                type = [...type.slice(0, -1), def];
            marker.update({ type });
        }
        else
            this.setStartMarker(type, data, slices);
    }
    /**
     * Toggle the type of a block split between the slice type and the default
     * (paragraph) block type.
     *
     * @param type Slice type to toggle.
     * @param data Custom data of the slice.
     */
    tglMarker(type, data, selection = this.cursors(), slices = this.saved, def = 0 /* SliceTypeCon.p */) {
        for (const range of selection)
            this.tglMarkerAt(range.start, type, data, slices, def);
    }
    delMarker(selection = this.cursors()) {
        const markerPoints = new Set();
        for (const range of selection) {
            const markerPoint = this.txt.overlay.getOrNextLowerMarker(range.start);
            if (markerPoint)
                markerPoints.add(markerPoint);
        }
        for (const markerPoint of markerPoints) {
            const boundary = markerPoint.marker.boundary();
            this.delRange(boundary);
        }
    }
    updMarkerSlice(marker, target, ops) {
        const node = target === 'type'
            ? marker.nestedType().asArr()
            : target[0] === 'tag'
                ? marker.nestedType().tag(target[1]).asVec()
                : target[0] === 'data'
                    ? marker.nestedType().tag(target[1])?.data()
                    : void 0;
        if (!node)
            return;
        for (const op of ops)
            node.op(op);
        if (target === 'type' && node instanceof model_1.ArrApi && node.length() === 0)
            marker.del();
    }
    updMarkerAt(point, target, ops, slices = this.saved) {
        const overlay = this.txt.overlay;
        const markerPoint = overlay.getOrNextLowerMarker(point);
        const marker = markerPoint?.marker ?? this.setStartMarker([0 /* SliceTypeCon.p */], void 0, slices);
        this.updMarkerSlice(marker, target, ops);
    }
    updMarker(target, ops, selection = this.cursors(), slices = this.saved) {
        for (const range of selection)
            this.updMarkerAt(range.start, target, ops, slices);
    }
    // ---------------------------------------------------------- export / import
    export(range) {
        const r = range.range();
        r.start.refBefore();
        r.end.refAfter();
        const text = r.text();
        const offset = r.start.viewPos();
        const viewSlices = [];
        const view = [text, offset, viewSlices];
        const txt = this.txt;
        const overlay = txt.overlay;
        const slices = overlay.findOverlapping(r);
        for (const slice of slices) {
            const isSavedSlice = slice.id.sid === txt.model.clock.sid;
            if (!isSavedSlice)
                continue;
            const stacking = slice.stacking;
            switch (stacking) {
                case constants_2.SliceStacking.One:
                case constants_2.SliceStacking.Many:
                case constants_2.SliceStacking.Erase:
                case constants_2.SliceStacking.Marker: {
                    const { stacking, start, end } = slice;
                    const header = (stacking << constants_2.SliceHeaderShift.Stacking) +
                        (start.anchor << constants_2.SliceHeaderShift.X1Anchor) +
                        (end.anchor << constants_2.SliceHeaderShift.X2Anchor);
                    const viewSlice = [header, start.viewPos(), end.viewPos(), slice.type()];
                    const data = slice.data();
                    if (data !== void 0)
                        viewSlice.push(data);
                    viewSlices.push(viewSlice);
                }
            }
        }
        return view;
    }
    /**
     * "Copy formatting-only", copies inline formatting applied to the selected
     * range.
     *
     * @param range Range copy formatting from, normally a single visible character.
     * @returns A list of serializable inline formatting applied to the selected range.
     */
    exportStyle(range) {
        const formatting = [];
        const txt = this.txt;
        const overlay = txt.overlay;
        const slices = overlay.findOverlapping(range);
        for (const slice of slices) {
            const isSavedSlice = slice.id.sid === txt.model.clock.sid;
            if (!isSavedSlice)
                continue;
            if (!slice.contains(range))
                continue;
            const stacking = slice.stacking;
            switch (stacking) {
                case constants_2.SliceStacking.One:
                case constants_2.SliceStacking.Many:
                case constants_2.SliceStacking.Erase: {
                    const sliceFormatting = [stacking, slice.type()];
                    const data = slice.data();
                    if (data !== void 0)
                        sliceFormatting.push(data);
                    formatting.push(sliceFormatting);
                }
            }
        }
        return formatting;
    }
    import(pos, view) {
        let [text, offset, slices] = view;
        const txt = this.txt;
        let removeFirstMarker = false;
        const firstSlice = slices[0];
        if (firstSlice) {
            const [header, x1, , type] = firstSlice;
            const stacking = (header & constants_2.SliceHeaderMask.Stacking) >>> constants_2.SliceHeaderShift.Stacking;
            const isBlockSplitMarker = stacking === constants_2.SliceStacking.Marker;
            if (isBlockSplitMarker) {
                const markerStartsAtZero = x1 - offset === 0;
                if (markerStartsAtZero) {
                    const point = txt.pointAt(pos);
                    const markerBefore = txt.overlay.getOrNextLowerMarker(point);
                    if (markerBefore) {
                        if (markerBefore.type() === type)
                            removeFirstMarker = true;
                    }
                    else {
                        if (type === slice_1.CommonSliceType.p)
                            removeFirstMarker = true;
                    }
                }
            }
        }
        if (removeFirstMarker) {
            text = text.slice(1);
            offset += 1;
            slices = slices.slice(1);
        }
        const length = slices.length;
        const splits = [];
        const annotations = [];
        const texts = [];
        let curr = 0;
        for (let i = 0; i < length; i++) {
            const slice = slices[i];
            const [header, x1] = slice;
            const stacking = (header & constants_2.SliceHeaderMask.Stacking) >>> constants_2.SliceHeaderShift.Stacking;
            const isBlockSplitMarker = stacking === constants_2.SliceStacking.Marker;
            if (isBlockSplitMarker) {
                const end = x1 - offset;
                texts.push(text.slice(curr, end));
                curr = end + 1;
                splits.push(slice);
            }
            else
                annotations.push(slice);
        }
        const lastText = text.slice(curr);
        const splitLength = splits.length;
        curr = pos;
        for (let i = 0; i < splitLength; i++) {
            const str = texts[i];
            const split = splits[i];
            if (str) {
                txt.insAt(curr, str);
                curr += str.length;
            }
            if (split) {
                const [, , , type, data] = split;
                const after = txt.pointAt(curr);
                after.refAfter();
                txt.savedSlices.insMarkerAfter(after.id, type, data);
                curr += 1;
            }
        }
        if (lastText) {
            txt.insAt(curr, lastText);
            curr += lastText.length;
        }
        const annotationsLength = annotations.length;
        for (let i = 0; i < annotationsLength; i++) {
            const slice = annotations[i];
            const [header, x1, x2, type, data] = slice;
            const anchor1 = (header & constants_2.SliceHeaderMask.X1Anchor) >>> constants_2.SliceHeaderShift.X1Anchor;
            const anchor2 = (header & constants_2.SliceHeaderMask.X2Anchor) >>> constants_2.SliceHeaderShift.X2Anchor;
            const stacking = (header & constants_2.SliceHeaderMask.Stacking) >>> constants_2.SliceHeaderShift.Stacking;
            const x1Src = x1 - offset;
            const x2Src = x2 - offset;
            const x1Capped = Math.max(0, x1Src);
            const x2Capped = Math.min(text.length, x2Src);
            const x1Dest = x1Capped + pos;
            const annotationLength = x2Capped - x1Capped;
            const range = txt.rangeAt(x1Dest, annotationLength);
            if (!!x1Dest && anchor1 === constants_1.Anchor.After)
                range.start.refAfter();
            // else range.start.refBefore();
            if (anchor2 === constants_1.Anchor.Before)
                range.end.refBefore();
            // else range.end.refAfter();
            if (range.end.isAbs())
                range.end.refAfter();
            txt.savedSlices.ins(range, stacking, type, data);
        }
        return curr - pos;
    }
    importStyle(range, formatting) {
        const txt = this.txt;
        const length = formatting.length;
        for (let i = 0; i < length; i++) {
            const [stacking, type, data] = formatting[i];
            txt.savedSlices.ins(range, stacking, type, data);
        }
    }
    // ------------------------------------------------------------------ various
    pos2point(at) {
        const txt = this.txt;
        return typeof at === 'number' ? txt.pointAt(at) : Array.isArray(at) ? txt.pointAt(at[0], at[1]) : at;
    }
    sel2range(at) {
        if (!Array.isArray(at))
            return [at, constants_2.CursorAnchor.End];
        const [pos1, pos2] = at;
        const p1 = this.pos2point(pos1);
        const txt = this.txt;
        if (pos2 === undefined) {
            p1.refAfter();
            return [txt.range(p1), constants_2.CursorAnchor.End];
        }
        const p2 = this.pos2point(pos2);
        const range = txt.rangeFromPoints(p1, p2);
        const anchor = range.start === p1 ? constants_2.CursorAnchor.Start : constants_2.CursorAnchor.End;
        return [range, anchor];
    }
    end() {
        const txt = this.txt;
        return txt.pointEnd() ?? txt.pointAbsEnd();
    }
    start() {
        const txt = this.txt;
        return txt.pointStart() ?? txt.pointAbsStart();
    }
    /** ----------------------------------------------------- {@link Printable} */
    toString(tab = '') {
        const pending = this.pending.value;
        const pendingFormatted = {};
        if (pending)
            for (const [type, data] of pending)
                pendingFormatted[(0, util_1.formatType)(type)] = data;
        return ('Editor' +
            (0, printTree_1.printTree)(tab, [
                (tab) => 'cursors' +
                    (0, printTree_1.printTree)(tab, [...this.cursors()].map((cursor) => (tab) => cursor.toString(tab))),
                (tab) => this.getRegistry().toString(tab),
                pending ? () => `pending ${(0, toLine_1.toLine)(pendingFormatted)}` : null,
            ]));
    }
}
exports.Editor = Editor;
//# sourceMappingURL=Editor.js.map