"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Point = void 0;
const clock_1 = require("../../../json-crdt-patch/clock");
const constants_1 = require("./constants");
const ChunkSlice_1 = require("../util/ChunkSlice");
const hash_1 = require("../../../json-crdt/hash");
const constants_2 = require("../constants");
/**
 * A "point" in a rich-text Peritext document. It is a combination of a
 * character ID and an anchor. Anchor specifies the side of the character to
 * which the point is attached. For example, a point with an anchor "before" .▢
 * points just before the character, while a point with an anchor "after" ▢.
 * points just after the character. Points attached to string characters are
 * referred to as *relative* points, while points attached to the beginning or
 * end of the string are referred to as *absolute* points.
 *
 * The *absolute* points are reference the string itself, by using the string's
 * ID as the character ID. The *absolute (abs) start* references the very start
 * of the string, before the first character, and even before any deleted
 * characters. The *absolute (abs) end* references the very end of the string,
 * after the last character, and even after any deleted characters at the end
 * of the string.
 */
class Point {
    constructor(rga, id, anchor) {
        this.rga = rga;
        this.id = id;
        this.anchor = anchor;
    }
    /**
     * Overwrites the internal state of this point with the state of the given
     * point.
     *
     * @param point Point to copy.
     */
    set(point) {
        this.id = point.id;
        this.anchor = point.anchor;
    }
    /**
     * Creates a copy of this point.
     *
     * @returns Returns a new point with the same ID and anchor as this point.
     */
    clone() {
        return new Point(this.rga, this.id, this.anchor);
    }
    copy(mutate) {
        const copy = this.clone();
        mutate(copy);
        return copy;
    }
    /**
     * Compares two points by their character IDs and anchors. First, the character
     * IDs are compared. If they are equal, the anchors are compared. The anchor
     * "before" is considered less than the anchor "after".
     *
     * @param other The other point to compare to.
     * @returns Returns 0 if the two points are equal, -1 if this point is less
     *          than the other point, and 1 if this point is greater than the other
     *          point.
     */
    cmp(other) {
        const cmp = (0, clock_1.compare)(this.id, other.id);
        if (cmp !== 0)
            return cmp;
        return (this.anchor - other.anchor);
    }
    /**
     * Compares two points by their spatial (view) location in the string. Takes
     * into account not only the character position in the view, but also handles
     * deleted characters, attachment anchors, and absolute points.
     *
     * @param other The other point to compare to.
     * @returns Returns 0 if the two points are equal, negative if this point is
     *          less than the other point, and positive if this point is greater
     *          than the other point.
     */
    cmpSpatial(other) {
        const thisId = this.id;
        const otherId = other.id;
        if (this.isAbs()) {
            const isStart = this.anchor === constants_1.Anchor.After;
            return isStart ? (other.isAbsStart() ? 0 : -1) : other.isAbsEnd() ? 0 : 1;
        }
        else if (other.isAbs()) {
            const isStart = other.anchor === constants_1.Anchor.After;
            return isStart ? (this.isAbsStart() ? 0 : 1) : this.isAbsEnd() ? 0 : -1;
        }
        const cmp0 = (0, clock_1.compare)(thisId, otherId);
        if (!cmp0)
            return this.anchor - other.anchor;
        const cmp1 = this.pos() - other.pos();
        if (cmp1)
            return cmp1;
        let chunk = this.chunk();
        if (!chunk)
            return cmp0;
        if ((0, clock_1.containsId)(chunk.id, chunk.span, otherId))
            return thisId.time - otherId.time;
        const rga = this.rga;
        chunk = rga.next(chunk);
        while (chunk) {
            if ((0, clock_1.containsId)(chunk.id, chunk.span, otherId))
                return -1;
            chunk = rga.next(chunk);
        }
        return 1;
    }
    /**
     * @returns Returns the chunk that contains the character referenced by the
     *          point, or `undefined` if the chunk is not found.
     */
    chunk() {
        let chunk = this._chunk;
        const id = this.id;
        if (chunk) {
            const chunkId = chunk.id;
            const chunkIdTime = chunkId.time;
            const idTime = id.time;
            if (id.sid === chunkId.sid && idTime >= chunkIdTime && idTime < chunkIdTime + chunk.span)
                return chunk;
        }
        this._chunk = chunk = this.rga.findById(this.id);
        return chunk;
    }
    /**
     * @returns Returns position of the character referenced by the point.
     */
    pos() {
        const chunk = this.chunk();
        if (!chunk)
            return this.isAbsEnd() ? constants_2.Position.AbsEnd : constants_2.Position.AbsStart;
        const pos = this.rga.pos(chunk);
        if (chunk.del)
            return pos;
        return pos + this.id.time - chunk.id.time;
    }
    /**
     * @returns Returns the view position of the point, as if it is a caret in
     *          the text pointing between characters (0 is before the first
     *          character, 1 is after the first character, etc.).
     */
    viewPos() {
        const pos = this.pos();
        const isAbs = (0, clock_1.equal)(this.rga.id, this.id);
        if (isAbs)
            return this.anchor === constants_1.Anchor.After ? 0 : this.rga.length();
        return this.anchor === constants_1.Anchor.Before ? pos : pos + 1;
    }
    /**
     * @returns Returns `true` if the point is at the very start of the string, i.e.
     *     there are no visible characters before it.
     */
    isStart() {
        const chunk = this.chunk();
        if (!chunk)
            return true;
        if (!chunk.del && chunk.id.time < this.id.time)
            return false;
        const l = chunk.l;
        return l ? !l.len : true;
    }
    /**
     * Goes to the next visible character in the string. The `move` parameter
     * specifies how many characters to move the cursor by. If the cursor reaches
     * the end of the string, it will return `undefined`.
     *
     * @param skip How many characters to move by.
     * @returns Next visible ID in string.
     */
    nextId(skip = 1) {
        if (this.isAbsEnd())
            return;
        let remaining = skip;
        const { id, rga } = this;
        let chunk;
        if (this.isAbsStart()) {
            chunk = rga.first();
            while (chunk && chunk.del)
                chunk = rga.next(chunk);
            if (!chunk)
                return;
            const span = chunk.span;
            if (remaining <= span)
                return (0, clock_1.tick)(chunk.id, remaining - 1);
            remaining -= span;
            chunk = rga.next(chunk);
        }
        else {
            chunk = this.chunk();
            if (!chunk)
                return undefined;
            if (!chunk.del) {
                const offset = id.time - chunk.id.time;
                const span = chunk.span;
                if (offset + remaining < span)
                    return (0, clock_1.tick)(id, remaining);
                else
                    remaining -= span - offset - 1;
            }
            chunk = rga.next(chunk);
        }
        let lastVisibleChunk;
        while (chunk && remaining >= 0) {
            if (chunk.del) {
                chunk = rga.next(chunk);
                continue;
            }
            lastVisibleChunk = chunk;
            const span = chunk.span;
            if (remaining <= span)
                return remaining > 1 ? (0, clock_1.tick)(chunk.id, remaining - 1) : chunk.id;
            remaining -= span;
            chunk = rga.next(chunk);
        }
        if (remaining > 0)
            return;
        return lastVisibleChunk ? (0, clock_1.tick)(lastVisibleChunk.id, lastVisibleChunk.span - 1) : undefined;
    }
    /**
     * @returns ID of the character that is `move` characters before the
     *          character referenced by the point, or `undefined` if there is no
     *          such character.
     */
    prevId(skip = 1) {
        if (this.isAbsStart())
            return;
        let remaining = skip;
        const { id, rga } = this;
        let chunk = this.chunk();
        if (!chunk)
            return rga.id;
        if (!chunk.del) {
            const offset = id.time - chunk.id.time;
            if (offset >= remaining)
                return (0, clock_1.tick)(id, -remaining);
            remaining -= offset;
        }
        chunk = rga.prev(chunk);
        while (chunk) {
            if (chunk.del) {
                chunk = rga.prev(chunk);
                continue;
            }
            const span = chunk.span;
            if (remaining <= span) {
                return (0, clock_1.tick)(chunk.id, span - remaining);
            }
            remaining -= span;
            chunk = rga.prev(chunk);
        }
        return;
    }
    /**
     * Returns one character to the left of the point, or `undefined` if there
     * is no such character. Skips any deleted characters. Handles absolute points.
     *
     * @returns A character slice to the left of the point.
     */
    leftChar() {
        const rga = this.rga;
        if (this.isAbsEnd()) {
            const res = rga.findChunk(rga.length() - 1);
            if (!res)
                return;
            return new ChunkSlice_1.ChunkSlice(res[0], res[1], 1);
        }
        const tmp = this.clone();
        tmp.refAfter();
        if (tmp.isAbsStart())
            return;
        const chunk = tmp.chunk();
        if (!chunk || chunk.del)
            return;
        const off = tmp.id.time - chunk.id.time;
        return new ChunkSlice_1.ChunkSlice(chunk, off, 1);
    }
    /**
     * Returns one character to the right of the point, or `undefined` if there
     * is no such character. Skips any deleted characters. Handles absolute points.
     *
     * @returns A character slice to the right of the point.
     */
    rightChar() {
        const rga = this.rga;
        if (this.isAbsStart()) {
            const res = rga.findChunk(0);
            if (!res)
                return;
            return new ChunkSlice_1.ChunkSlice(res[0], res[1], 1);
        }
        const tmp = this.clone();
        tmp.refBefore();
        if (tmp.isAbsEnd())
            return;
        const chunk = tmp.chunk();
        if (!chunk || chunk.del)
            return;
        const off = tmp.id.time - chunk.id.time;
        return new ChunkSlice_1.ChunkSlice(chunk, off, 1);
    }
    char() {
        return this.anchor === constants_1.Anchor.Before ? this.rightChar() : this.leftChar();
    }
    /**
     * Checks if the point is an absolute point. An absolute point is a point that
     * references the string itself, rather than a character in the string. It can
     * be either the very start or the very end of the string.
     *
     * @returns Returns `true` if the point is an absolute point.
     */
    isAbs() {
        return (0, clock_1.equal)(this.id, this.rga.id);
    }
    /**
     * @returns Returns `true` if the point is an absolute point and is anchored
     *          before the first character in the string.
     */
    isAbsStart() {
        return this.isAbs() && this.anchor === constants_1.Anchor.After;
    }
    /**
     * @returns Returns `true` if the point is an absolute point and is anchored
     *          after the last character in the string.
     */
    isAbsEnd() {
        return this.isAbs() && this.anchor === constants_1.Anchor.Before;
    }
    /**
     * @returns Returns `true` if the point is exactly the relative start, i.e.
     *          it is attached to the first visible character in the string and
     *          anchored "before".
     */
    isRelStart() {
        if (this.anchor !== constants_1.Anchor.Before)
            return false;
        const id = this.rga.find(0);
        return !!id && (0, clock_1.equal)(this.id, id);
    }
    /**
     * @returns Returns `true` if the point is exactly the relative end, i.e. it
     *          is attached to the last visible character in the string and
     *          anchored "after".
     */
    isRelEnd() {
        if (this.anchor !== constants_1.Anchor.After)
            return false;
        const rga = this.rga;
        const length = rga.length();
        if (length === 0)
            return false;
        const id = rga.find(length - 1);
        return !!id && (0, clock_1.equal)(this.id, id);
    }
    /**
     * Sets the point to the absolute start of the string.
     */
    refAbsStart() {
        this.id = this.rga.id;
        this.anchor = constants_1.Anchor.After;
    }
    /**
     * Sets the point to the absolute end of the string.
     */
    refAbsEnd() {
        this.id = this.rga.id;
        this.anchor = constants_1.Anchor.Before;
    }
    /**
     * Sets the point to the relative start of the string.
     */
    refStart() {
        this.refAbsStart();
        this.refBefore();
        return this;
    }
    /**
     * Sets the point to the relative end of the string.
     */
    refEnd() {
        this.refAbsEnd();
        this.refAfter();
        return this;
    }
    /**
     * Modifies the location of the point, such that the view location remains
     * the same, but ensures that it is anchored before a character. Skips any
     * deleted characters (chunks), attaching the point to the next visible
     * character.
     */
    refBefore() {
        const chunk = this.chunk();
        if (!chunk) {
            if (this.isAbsStart()) {
                const id = this.rga.find(0);
                if (id) {
                    this.id = id;
                    this.anchor = constants_1.Anchor.Before;
                    return;
                }
            }
            this.refAbsEnd();
            return;
        }
        if (!chunk.del && this.anchor === constants_1.Anchor.Before)
            return;
        this.anchor = constants_1.Anchor.Before;
        this.id = this.nextId() || this.rga.id;
    }
    /**
     * Modifies the location of the point, such that the view location remains
     * the same, but ensures that it is anchored after a character. Skips any
     * deleted characters (chunks), attaching the point to the next visible
     * character.
     */
    refAfter() {
        const chunk = this.chunk();
        if (!chunk) {
            if (this.isAbsEnd()) {
                const rga = this.rga;
                const length = rga.length();
                if (length !== 0) {
                    const id = rga.find(length - 1);
                    if (id) {
                        this.id = id;
                        this.anchor = constants_1.Anchor.After;
                        return;
                    }
                }
            }
            this.refAbsStart();
            return;
        }
        if (!chunk.del && this.anchor === constants_1.Anchor.After)
            return;
        this.anchor = constants_1.Anchor.After;
        this.id = this.prevId() || this.rga.id;
    }
    /**
     * Modifies the location of the point, such that the spatial location remains
     * the same and tries to preserve anchor location, but ensures that the point
     * references a visible (not deleted) character.
     */
    refVisible() {
        if (this.anchor === constants_1.Anchor.Before)
            this.refBefore();
        else
            this.refAfter();
    }
    /**
     * Moves point past given number of visible characters. Accepts positive
     * and negative distances.
     *
     * @param length How many characters to move by. Positive number moves the
     *     point to the right, negative number moves the point to the left.
     * @returns Returns `true` if the absolute end of the string is reached.
     */
    step(length) {
        if (!length)
            return this.isAbs();
        const anchor = this.anchor;
        if (anchor !== constants_1.Anchor.After)
            this.refAfter();
        if (length > 0) {
            const nextId = this.nextId(length);
            if (!nextId) {
                this.refAbsEnd();
                return true;
            }
            else {
                this.id = nextId;
                if (anchor !== constants_1.Anchor.After)
                    this.refBefore();
            }
        }
        else {
            const prevId = this.prevId(-length);
            if (!prevId) {
                this.refAbsStart();
                return true;
            }
            else {
                this.id = prevId;
                if (anchor !== constants_1.Anchor.After)
                    this.refBefore();
            }
        }
        return false;
    }
    /**
     * Moves the to the next point, which does not necessarily result in a visible
     * character skip.
     *
     * @param length How many points to move by.
     * @returns Returns `true` if the absolute end of the string is reached.
     */
    halfstep(length) {
        this.refVisible();
        const isOdd = !!(length % 2);
        if (isOdd) {
            if (length > 0) {
                length--;
                if (this.anchor === constants_1.Anchor.After)
                    this.refBefore();
                else if (this.isAbs())
                    return true;
                else
                    this.anchor = constants_1.Anchor.After;
            }
            else {
                length++;
                if (this.anchor === constants_1.Anchor.Before)
                    this.refAfter();
                else if (this.isAbs())
                    return true;
                else
                    this.anchor = constants_1.Anchor.Before;
            }
        }
        return this.step(length / 2);
    }
    key() {
        return (0, hash_1.hashId)(this.id) + (this.anchor ? 0 : 1);
    }
    // ----------------------------------------------------------------- Stateful
    refresh() {
        return (0, hash_1.updateId)(this.anchor, this.id);
    }
    // ---------------------------------------------------------------- Printable
    toStringName() {
        return 'Point';
    }
    toString(tab = '', lite) {
        const name = lite ? '' : this.toStringName() + ' ';
        const pos = this.pos();
        const id = (0, clock_1.printTs)(this.id);
        let char = this.char()?.view();
        char = typeof char === 'string' ? JSON.stringify(char) : '▢';
        const anchor = this.anchor === constants_1.Anchor.Before ? '.' + char : char + '.';
        return `${name}{ ${pos === constants_2.Position.AbsEnd ? '∞' : pos}, ${id}, ${anchor} }`;
    }
}
exports.Point = Point;
//# sourceMappingURL=Point.js.map