"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Peritext = void 0;
const printTree_1 = require("tree-dump/lib/printTree");
const constants_1 = require("./rga/constants");
const Point_1 = require("./rga/Point");
const Range_1 = require("./rga/Range");
const Editor_1 = require("./editor/Editor");
const nodes_1 = require("../../json-crdt/nodes");
const Slices_1 = require("./slice/Slices");
const LocalSlices_1 = require("./slice/LocalSlices");
const Overlay_1 = require("./overlay/Overlay");
const constants_2 = require("./constants");
const clock_1 = require("../../json-crdt-patch/clock");
const model_1 = require("../../json-crdt/model");
const hash_1 = require("../../json-hash/hash");
const json_crdt_patch_1 = require("../../json-crdt-patch");
const ExtraSlices_1 = require("./slice/ExtraSlices");
const Fragment_1 = require("./block/Fragment");
const hash_2 = require("../../json-crdt/hash");
const EXTRA_SLICES_SCHEMA = json_crdt_patch_1.s.vec(json_crdt_patch_1.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.
 */
class Peritext {
    /**
     * 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_1.Model.create(EXTRA_SLICES_SCHEMA, model.clock.sid - 1), localSlicesModel = model_1.Model.create(LOCAL_DATA_SCHEMA, 3 /* SESSION.LOCAL */)) {
        this.model = model;
        this.str = str;
        this.overlay = new Overlay_1.Overlay(this);
        /** ------------------------------------------------------ {@link Stateful} */
        this.hash = 0;
        this.savedSlices = new Slices_1.Slices(this, slices);
        this.extraSlices = new ExtraSlices_1.ExtraSlices(this, extraSlicesModel.root.node().get(0));
        const localApi = localSlicesModel.api;
        localApi.onLocalChange.listen(() => {
            localApi.flush();
        });
        this.localSlices = new LocalSlices_1.LocalSlices(this, localSlicesModel.root.node().get(0));
        this.editor = new Editor_1.Editor(this);
        this.blocks = new Fragment_1.Fragment(this, this.pointAbsStart(), this.pointAbsEnd());
    }
    strApi() {
        if (this.str instanceof nodes_1.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 = constants_1.Anchor.After) {
        return new Point_1.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 = constants_1.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 ? constants_1.Anchor.Before : constants_1.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, constants_1.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, constants_1.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_1.Range.from(this.str, p1, p2);
    }
    rangeFromChunkSlice(slice) {
        const startId = slice.off === 0 ? slice.chunk.id : (0, clock_1.tick)(slice.chunk.id, slice.off);
        const endId = (0, clock_1.tick)(slice.chunk.id, slice.off + slice.len - 1);
        const start = this.point(startId, constants_1.Anchor.Before);
        const end = this.point(endId, constants_1.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_1.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_1.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_1.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 === constants_1.Anchor.Before
                ? start.id
                : start.nextId();
        const delEndId = end.isAbsEnd() ? this.point().refEnd().id : end.anchor === constants_1.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;
    }
    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 = constants_2.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, [(0, clock_1.interval)(strChunk.id, 0, 1)]);
        builder.del(this.savedSlices.set.id, [(0, clock_1.interval)(split.id, 0, 1)]);
        api.apply();
    }
    /** ----------------------------------------------------- {@link Printable} */
    toString(tab = '') {
        const nl = () => '';
        const { savedSlices, extraSlices, localSlices } = this;
        return ('Peritext' +
            (0, printTree_1.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),
            ]));
    }
    refresh() {
        let state = hash_1.CONST.START_STATE;
        state = (0, hash_2.updateRga)(state, this.str);
        state = (0, hash_1.updateNum)(state, this.overlay.refresh());
        state = (0, hash_1.updateNum)(state, this.blocks.refresh());
        return (this.hash = state);
    }
}
exports.Peritext = Peritext;
//# sourceMappingURL=Peritext.js.map