import { printTree } from 'tree-dump/lib/printTree';
import { Anchor } from './rga/constants';
import { Point } from './rga/Point';
import { Range } from './rga/Range';
import { Editor } from './editor/Editor';
import { StrNode } from '../../json-crdt/nodes';
import { Slices } from './slice/Slices';
import { LocalSlices } from './slice/LocalSlices';
import { Overlay } from './overlay/Overlay';
import { Chars } from './constants';
import { interval, tick } from '../../json-crdt-patch/clock';
import { Model } from '../../json-crdt/model';
import { CONST, updateNum } from '../../json-hash/hash';
import { s } from '../../json-crdt-patch';
import { ExtraSlices } from './slice/ExtraSlices';
import { Fragment } from './block/Fragment';
import { updateRga } from '../../json-crdt/hash';
const EXTRA_SLICES_SCHEMA = s.vec(s.arr([]));
const LOCAL_DATA_SCHEMA = EXTRA_SLICES_SCHEMA;
/**
 * Context for a Peritext instance. Contains all the data and methods needed to
 * interact with the text.
 */
export class Peritext {
    model;
    str;
    editor;
    overlay = new Overlay(this);
    blocks;
    /**
     * Creates a new Peritext context.
     *
     * @param model JSON CRDT model of the document where the text is stored.
     * @param str The {@link StrNode} where the text is stored.
     * @param slices The {@link ArrNode} where the slices are stored.
     * @param extraSlicesModel The JSON CRDT model for the extra slices, which are
     *        not persisted in the main document, but are shared with other users.
     * @param localSlicesModel The JSON CRDT model for the local slices, which are
     *        not persisted in the main document and are not shared with other
     *        users. The local slices capture current-user-only annotations, such
     *        as the current user's selection.
     */
    constructor(model, 
    // TODO: Rename `str` to `rga`.
    str, slices, 
    // TODO: Add test that verifies that SIDs are different across all three models.
    extraSlicesModel = Model.create(EXTRA_SLICES_SCHEMA, model.clock.sid - 1), localSlicesModel = Model.create(LOCAL_DATA_SCHEMA, 3 /* SESSION.LOCAL */)) {
        this.model = model;
        this.str = str;
        this.savedSlices = new Slices(this, slices);
        this.extraSlices = new ExtraSlices(this, extraSlicesModel.root.node().get(0));
        const localApi = localSlicesModel.api;
        localApi.onLocalChange.listen(() => {
            localApi.flush();
        });
        this.localSlices = new LocalSlices(this, localSlicesModel.root.node().get(0));
        this.editor = new Editor(this);
        this.blocks = new Fragment(this, this.pointAbsStart(), this.pointAbsEnd());
    }
    strApi() {
        if (this.str instanceof StrNode)
            return this.model.api.wrap(this.str);
        throw new Error('INVALID_STR');
    }
    // ------------------------------------------------------------------- points
    /**
     * Creates a point at a character ID.
     *
     * @param id Character ID to which the point should be attached.
     * @param anchor Whether the point should be before or after the character.
     * @returns The point.
     */
    point(id = this.str.id, anchor = Anchor.After) {
        return new Point(this.str, id, anchor);
    }
    /**
     * Creates a point at a view position in the text. The `pos` argument
     * specifies the position of the character, not the gap between characters.
     *
     * @param pos Position of the character in the text.
     * @param anchor Whether the point should attach before or after a character.
     *               Defaults to "before".
     * @returns The point.
     */
    pointAt(pos, anchor = Anchor.Before) {
        // TODO: Provide ability to attach to the beginning of the text?
        // TODO: Provide ability to attach to the end of the text?
        const str = this.str;
        const id = str.find(pos);
        if (!id)
            return this.point(str.id, pos ? Anchor.Before : Anchor.After);
        return this.point(id, anchor);
    }
    /**
     * Creates a point which is attached to the start of the text, before the
     * first character.
     *
     * @returns A point at the start of the text.
     */
    pointAbsStart() {
        return this.point(this.str.id, Anchor.After);
    }
    /**
     * Creates a point which is attached to the end of the text, after the last
     * character.
     *
     * @returns A point at the end of the text.
     */
    pointAbsEnd() {
        return this.point(this.str.id, Anchor.Before);
    }
    pointStart() {
        if (!this.str.length())
            return;
        const point = this.pointAbsStart();
        point.refBefore();
        return point;
    }
    pointEnd() {
        if (!this.str.length())
            return;
        const point = this.pointAbsEnd();
        point.refAfter();
        return point;
    }
    // ------------------------------------------------------------------- ranges
    /**
     * Creates a range from two points. The points can be in any order.
     *
     * @param p1 Point
     * @param p2 Point
     * @returns A range with points in correct order.
     */
    rangeFromPoints(p1, p2) {
        return Range.from(this.str, p1, p2);
    }
    rangeFromChunkSlice(slice) {
        const startId = slice.off === 0 ? slice.chunk.id : tick(slice.chunk.id, slice.off);
        const endId = tick(slice.chunk.id, slice.off + slice.len - 1);
        const start = this.point(startId, Anchor.Before);
        const end = this.point(endId, Anchor.After);
        return this.range(start, end);
    }
    /**
     * Creates a range from two points, the points have to be in the correct
     * order.
     *
     * @param start Start point of the range, must be before or equal to end.
     * @param end End point of the range, must be after or equal to start.
     * @returns A range with the given start and end points.
     */
    range(start, end = start) {
        return new Range(this.str, start, start === end ? end.clone() : end);
    }
    /**
     * A convenience method for creating a range from a view position and a
     * length. See {@link Range.at} for more information.
     *
     * @param start Position in the text.
     * @param length Length of the range.
     * @returns A range from the given position with the given length.
     */
    rangeAt(start, length = 0) {
        return Range.at(this.str, start, length);
    }
    /**
     * Creates selection of relative start and end of the whole document.
     *
     * @returns Range, which selects the whole document, if any.
     */
    rangeAll() {
        const start = this.pointStart();
        const end = this.pointEnd();
        if (!start || !end)
            return;
        return this.range(start, end);
    }
    fragment(range) {
        return new Fragment(this, range.start, range.end);
    }
    // ---------------------------------------------------------- text (& slices)
    /**
     * Insert plain text at a view position in the text.
     *
     * @param pos View position in the text.
     * @param text Text to insert.
     */
    insAt(pos, text) {
        const str = this.strApi();
        str.ins(pos, text);
    }
    /**
     * Insert plain text after a character referenced by its ID and return the
     * ID of the insertion operation.
     *
     * @param after Character ID after which the text should be inserted.
     * @param text Text to insert.
     * @returns ID of the insertion operation.
     */
    ins(after, text) {
        if (!text)
            throw new Error('NO_TEXT');
        const api = this.model.api;
        const textId = api.builder.insStr(this.str.id, after, text);
        api.apply();
        return textId;
    }
    delAt(pos, len) {
        const range = this.rangeAt(pos, len);
        this.del(range);
    }
    del(range) {
        this.delSlices(range);
        this.delStr(range);
    }
    delStr(range) {
        const isCaret = range.isCollapsed();
        if (isCaret)
            return false;
        const { start, end } = range;
        const delStartId = start.isAbsStart()
            ? this.point().refStart().id
            : start.anchor === Anchor.Before
                ? start.id
                : start.nextId();
        const delEndId = end.isAbsEnd() ? this.point().refEnd().id : end.anchor === Anchor.After ? end.id : end.prevId();
        if (!delStartId || !delEndId)
            throw new Error('INVALID_RANGE');
        const rga = this.str;
        const spans = rga.findInterval2(delStartId, delEndId);
        const api = this.model.api;
        api.builder.del(rga.id, spans);
        api.apply();
        return true;
    }
    delSlices(range) {
        // TODO: PERF: do we need this refresh?
        this.overlay.refresh();
        range = range.range();
        range.expand();
        const slices = this.overlay.findContained(range);
        let deleted = false;
        if (!slices.size)
            return deleted;
        if (this.savedSlices.delSlices(slices))
            deleted = true;
        if (this.extraSlices.delSlices(slices))
            deleted = true;
        if (this.localSlices.delSlices(slices))
            deleted = true;
        return deleted;
    }
    // ------------------------------------------------------------------- slices
    /**
     * *Slices* are rich-text annotations that appear in the text. The "saved"
     * slices are the ones that are persisted in the document.
     */
    savedSlices;
    /**
     * *Extra slices* are slices that are not persisted in the document. However,
     * they are still shared across users, i.e. they are ephemerally persisted
     * during the editing session.
     */
    extraSlices;
    /**
     * *Local slices* are slices that are not persisted in the document and are
     * not shared with other users. They are used only for local annotations for
     * the current user.
     */
    localSlices;
    getSlice(id) {
        return this.savedSlices.get(id) || this.localSlices.get(id) || this.extraSlices.get(id);
    }
    // ------------------------------------------------------------------ markers
    /** @deprecated Use the method in `Editor` and `Cursor` instead. */
    insMarker(after, type, data, char = Chars.BlockSplitSentinel) {
        return this.savedSlices.insMarkerAfter(after, type, data, char);
    }
    /** @todo This can probably use .del() */
    delMarker(split) {
        const str = this.str;
        const api = this.model.api;
        const builder = api.builder;
        const strChunk = split.start.chunk();
        if (strChunk)
            builder.del(str.id, [interval(strChunk.id, 0, 1)]);
        builder.del(this.savedSlices.set.id, [interval(split.id, 0, 1)]);
        api.apply();
    }
    /** ----------------------------------------------------- {@link Printable} */
    toString(tab = '') {
        const nl = () => '';
        const { savedSlices, extraSlices, localSlices } = this;
        return ('Peritext' +
            printTree(tab, [
                (tab) => this.str.toString(tab),
                nl,
                savedSlices.size() ? (tab) => savedSlices.toString(tab) : null,
                extraSlices.size() ? (tab) => extraSlices.toString(tab) : null,
                localSlices.size() ? (tab) => localSlices.toString(tab) : null,
                nl,
                (tab) => this.overlay.toString(tab),
                nl,
                (tab) => this.blocks.toString(tab),
            ]));
    }
    /** ------------------------------------------------------ {@link Stateful} */
    hash = 0;
    refresh() {
        let state = CONST.START_STATE;
        state = updateRga(state, this.str);
        state = updateNum(state, this.overlay.refresh());
        state = updateNum(state, this.blocks.refresh());
        return (this.hash = state);
    }
}
//# sourceMappingURL=Peritext.js.map