"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.LocalReferenceCollection = exports.filterLocalReferencePositions = exports.anyLocalReferencePosition = exports.createDetachedLocalReferencePosition = exports.SlidingPreference = void 0;
const common_utils_1 = require("@fluidframework/common-utils");
const container_utils_1 = require("@fluidframework/container-utils");
const collections_1 = require("./collections");
const mergeTreeTracking_1 = require("./mergeTreeTracking");
const ops_1 = require("./ops");
const properties_1 = require("./properties");
const referencePositions_1 = require("./referencePositions");
/**
 * Dictates the preferential direction for a {@link ReferencePosition} to slide
 * in a merge-tree
 */
exports.SlidingPreference = {
    /**
     * Prefer sliding towards the start of the tree
     */
    BACKWARD: 0,
    /**
     * Prefer sliding towards the end of the tree
     */
    FORWARD: 1,
};
/**
 * @internal
 */
function _validateReferenceType(refType) {
    let exclusiveCount = 0;
    if ((0, referencePositions_1.refTypeIncludesFlag)(refType, ops_1.ReferenceType.Transient)) {
        ++exclusiveCount;
    }
    if ((0, referencePositions_1.refTypeIncludesFlag)(refType, ops_1.ReferenceType.SlideOnRemove)) {
        ++exclusiveCount;
    }
    if ((0, referencePositions_1.refTypeIncludesFlag)(refType, ops_1.ReferenceType.StayOnRemove)) {
        ++exclusiveCount;
    }
    if (exclusiveCount > 1) {
        throw new container_utils_1.UsageError("Reference types can only be one of Transient, SlideOnRemove, and StayOnRemove");
    }
}
/**
 * @privateRemarks This should not be exported outside merge tree.
 * @internal
 */
class LocalReference {
    constructor(refType = ops_1.ReferenceType.Simple, properties, slidingPreference = exports.SlidingPreference.FORWARD) {
        this.refType = refType;
        this.slidingPreference = slidingPreference;
        this.offset = 0;
        _validateReferenceType(refType);
        this.properties = properties;
    }
    get trackingCollection() {
        var _a;
        return ((_a = this._trackingCollection) !== null && _a !== void 0 ? _a : (this._trackingCollection = new mergeTreeTracking_1.TrackingGroupCollection(this)));
    }
    link(segment, offset, listNode) {
        var _a, _b;
        if (listNode !== this.listNode && this.listNode !== undefined) {
            (_b = (_a = this.segment) === null || _a === void 0 ? void 0 : _a.localRefs) === null || _b === void 0 ? void 0 : _b.removeLocalRef(this);
        }
        this.listNode = listNode;
        if (segment !== this.segment) {
            const groups = [];
            this.trackingCollection.trackingGroups.forEach((tg) => {
                tg.unlink(this);
                groups.push(tg);
            });
            this.segment = segment;
            groups.forEach((tg) => tg.link(this));
        }
        this.offset = offset;
    }
    isLeaf() {
        return false;
    }
    addProperties(newProps, op) {
        this.properties = (0, properties_1.addProperties)(this.properties, newProps, op);
    }
    getSegment() {
        return this.segment;
    }
    getOffset() {
        return this.offset;
    }
    getListNode() {
        return this.listNode;
    }
    getProperties() {
        return this.properties;
    }
}
function createDetachedLocalReferencePosition(refType) {
    return new LocalReference(refType, undefined);
}
exports.createDetachedLocalReferencePosition = createDetachedLocalReferencePosition;
function assertLocalReferences(lref) {
    (0, common_utils_1.assert)(lref instanceof LocalReference, 0x2e0 /* "lref not a Local Reference" */);
}
/**
 * @returns true if `func` returns true for any position within the collection
 */
function anyLocalReferencePosition(collection, func) {
    for (const pos of collection) {
        if (func(pos)) {
            return true;
        }
    }
    return false;
}
exports.anyLocalReferencePosition = anyLocalReferencePosition;
/**
 * @returns only the local reference positions for which the `predicate` returns
 * true
 */
function* filterLocalReferencePositions(collection, predicate) {
    for (const pos of collection) {
        if (predicate(pos)) {
            yield pos;
        }
    }
}
exports.filterLocalReferencePositions = filterLocalReferencePositions;
/**
 * Represents a collection of {@link LocalReferencePosition}s associated with one segment in a merge-tree.
 */
class LocalReferenceCollection {
    /**
     *
     * @internal
     */
    constructor(
    /** Segment this `LocalReferenceCollection` is associated to. */
    segment, initialRefsByfOffset = new Array(segment.cachedLength)) {
        this.segment = segment;
        /**
         * @remarks This method should only be called by mergeTree.
         * @internal
         */
        this.hierRefCount = 0;
        this.refCount = 0;
        // Since javascript arrays are sparse the above won't populate any of the
        // indices, but it will ensure the length property of the array matches
        // the length of the segment.
        this.refsByOffset = initialRefsByfOffset;
    }
    static append(seg1, seg2) {
        if (seg2.localRefs && !seg2.localRefs.empty) {
            if (!seg1.localRefs) {
                seg1.localRefs = new LocalReferenceCollection(seg1);
            }
            (0, common_utils_1.assert)(seg1.localRefs.refsByOffset.length === seg1.cachedLength, 0x2be /* "LocalReferences array contains a gap" */);
            seg1.localRefs.append(seg2.localRefs);
        }
        else if (seg1.localRefs) {
            // Since creating the LocalReferenceCollection, we may have appended
            // segments that had no local references. Account for them now by padding the array.
            seg1.localRefs.refsByOffset.length += seg2.cachedLength;
        }
    }
    /**
     * @remarks This method should only be called by mergeTree.
     * @internal
     */
    [Symbol.iterator]() {
        const subiterators = [];
        for (const refs of this.refsByOffset) {
            if (refs) {
                if (refs.before) {
                    subiterators.push(refs.before[Symbol.iterator]());
                }
                if (refs.at) {
                    subiterators.push(refs.at[Symbol.iterator]());
                }
                if (refs.after) {
                    subiterators.push(refs.after[Symbol.iterator]());
                }
            }
        }
        const iterator = {
            next() {
                while (subiterators.length > 0) {
                    const next = subiterators[0].next();
                    if (next.done === true) {
                        subiterators.shift();
                    }
                    else {
                        return { done: next.done, value: next.value.data };
                    }
                }
                return { value: undefined, done: true };
            },
            [Symbol.iterator]() {
                return this;
            },
        };
        return iterator;
    }
    /**
     * @remarks This method should only be called by mergeTree.
     * @internal
     */
    clear() {
        this.refCount = 0;
        this.hierRefCount = 0;
        const detachSegments = (refs) => {
            if (refs) {
                for (const r of refs) {
                    this.removeLocalRef(r.data);
                }
            }
        };
        for (let i = 0; i < this.refsByOffset.length; i++) {
            const refsAtOffset = this.refsByOffset[i];
            if (refsAtOffset) {
                detachSegments(refsAtOffset.before);
                detachSegments(refsAtOffset.at);
                detachSegments(refsAtOffset.after);
                this.refsByOffset[i] = undefined;
            }
        }
    }
    /**
     * @remarks This method should only be called by mergeTree.
     * @internal
     */
    get empty() {
        return this.refCount === 0;
    }
    /**
     * @remarks This method should only be called by mergeTree.
     * @internal
     */
    createLocalRef(offset, refType, properties, slidingPreference) {
        const ref = new LocalReference(refType, properties, slidingPreference);
        ref.link(this.segment, offset, undefined);
        if (!(0, referencePositions_1.refTypeIncludesFlag)(ref, ops_1.ReferenceType.Transient)) {
            this.addLocalRef(ref, offset);
        }
        return ref;
    }
    /**
     * @remarks This method should only be called by mergeTree.
     * @internal
     */
    addLocalRef(lref, offset) {
        var _a, _b;
        assertLocalReferences(lref);
        (0, common_utils_1.assert)(offset < this.segment.cachedLength, 0x348 /* offset cannot be beyond segment length */);
        if ((0, referencePositions_1.refTypeIncludesFlag)(lref, ops_1.ReferenceType.Transient)) {
            lref.link(this.segment, offset, undefined);
        }
        else {
            const refsAtOffset = (this.refsByOffset[offset] = (_a = this.refsByOffset[offset]) !== null && _a !== void 0 ? _a : {
                at: new collections_1.List(),
            });
            const atRefs = (refsAtOffset.at = (_b = refsAtOffset.at) !== null && _b !== void 0 ? _b : new collections_1.List());
            lref.link(this.segment, offset, atRefs.push(lref).last);
            if ((0, referencePositions_1.refHasRangeLabels)(lref) || (0, referencePositions_1.refHasTileLabels)(lref)) {
                this.hierRefCount++;
            }
            this.refCount++;
        }
    }
    /**
     * @remarks This method should only be called by mergeTree.
     * @internal
     */
    removeLocalRef(lref) {
        var _a;
        if (this.has(lref)) {
            assertLocalReferences(lref);
            const node = lref.getListNode();
            (_a = node === null || node === void 0 ? void 0 : node.list) === null || _a === void 0 ? void 0 : _a.remove(node);
            lref.link(lref.getSegment(), lref.getOffset(), undefined);
            if ((0, referencePositions_1.refHasRangeLabels)(lref) || (0, referencePositions_1.refHasTileLabels)(lref)) {
                this.hierRefCount--;
            }
            this.refCount--;
            return lref;
        }
    }
    /**
     *
     * Called by 'append()' implementations to append local refs from the given 'other' segment to the
     * end of 'this' segment.
     *
     * Note: This method should be invoked after the caller has ensured that segments can be merged,
     * but before 'this' segment's cachedLength has changed, or the adjustment to the local refs
     * will be incorrect.
     *
     * @remarks This method should only be called by mergeTree.
     * @internal
     */
    append(other) {
        if (!other || other.empty) {
            return;
        }
        this.hierRefCount += other.hierRefCount;
        this.refCount += other.refCount;
        other.hierRefCount = 0;
        other.refCount = 0;
        for (const lref of other) {
            assertLocalReferences(lref);
            lref.link(this.segment, lref.getOffset() + this.refsByOffset.length, lref.getListNode());
        }
        this.refsByOffset.push(...other.refsByOffset);
    }
    /**
     * Returns true of the local reference is in the collection, otherwise false.
     *
     * @remarks This method should only be called by mergeTree.
     * @internal
     */
    has(lref) {
        var _a, _b, _c;
        if (!(lref instanceof LocalReference) ||
            (0, referencePositions_1.refTypeIncludesFlag)(lref, ops_1.ReferenceType.Transient)) {
            return false;
        }
        const seg = lref.getSegment();
        if (seg !== this.segment) {
            return false;
        }
        // we should be able to optimize finding the
        // list head
        const listNode = lref.getListNode();
        if (listNode === undefined) {
            return false;
        }
        const offset = lref.getOffset();
        const refsAtOffset = this.refsByOffset[offset];
        if (((_a = refsAtOffset === null || refsAtOffset === void 0 ? void 0 : refsAtOffset.before) === null || _a === void 0 ? void 0 : _a.includes(listNode)) ||
            ((_b = refsAtOffset === null || refsAtOffset === void 0 ? void 0 : refsAtOffset.at) === null || _b === void 0 ? void 0 : _b.includes(listNode)) ||
            ((_c = refsAtOffset === null || refsAtOffset === void 0 ? void 0 : refsAtOffset.after) === null || _c === void 0 ? void 0 : _c.includes(listNode))) {
            return true;
        }
        return false;
    }
    /**
     * Splits this `LocalReferenceCollection` into the intervals [0, offset) and [offset, originalLength).
     * Local references in the former half of this split will remain associated with the segment used on construction.
     * Local references in the latter half of this split will be transferred to `splitSeg`,
     * and its `localRefs` field will be set.
     * @param offset - Offset into the original segment at which the collection should be split
     * @param splitSeg - Split segment which originally corresponded to the indices [offset, originalLength)
     * before splitting.
     *
     * @remarks This method should only be called by mergeTree.
     * @internal
     */
    split(offset, splitSeg) {
        if (!this.empty) {
            const localRefs = new LocalReferenceCollection(splitSeg, this.refsByOffset.splice(offset, this.refsByOffset.length - offset));
            splitSeg.localRefs = localRefs;
            for (const lref of localRefs) {
                assertLocalReferences(lref);
                lref.link(splitSeg, lref.getOffset() - offset, lref.getListNode());
                if ((0, referencePositions_1.refHasRangeLabels)(lref) || (0, referencePositions_1.refHasTileLabels)(lref)) {
                    this.hierRefCount--;
                    localRefs.hierRefCount++;
                }
                this.refCount--;
                localRefs.refCount++;
            }
        }
        else {
            // shrink the offset array when empty and splitting
            this.refsByOffset.length = offset;
        }
    }
    /**
     * @remarks This method should only be called by mergeTree.
     * @internal
     */
    addBeforeTombstones(...refs) {
        var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
        var _m;
        const beforeRefs = (_b = (_a = this.refsByOffset[0]) === null || _a === void 0 ? void 0 : _a.before) !== null && _b !== void 0 ? _b : new collections_1.List();
        if (((_c = this.refsByOffset[0]) === null || _c === void 0 ? void 0 : _c.before) === undefined) {
            const refsAtOffset = ((_d = (_m = this.refsByOffset)[0]) !== null && _d !== void 0 ? _d : (_m[0] = { before: beforeRefs }));
            (_e = refsAtOffset.before) !== null && _e !== void 0 ? _e : (refsAtOffset.before = beforeRefs);
        }
        let precedingRef;
        for (const iterable of refs) {
            for (const lref of iterable) {
                assertLocalReferences(lref);
                if ((0, referencePositions_1.refTypeIncludesFlag)(lref, ops_1.ReferenceType.StayOnRemove)) {
                    continue;
                }
                else if ((0, referencePositions_1.refTypeIncludesFlag)(lref, ops_1.ReferenceType.SlideOnRemove)) {
                    (_g = (_f = lref.callbacks) === null || _f === void 0 ? void 0 : _f.beforeSlide) === null || _g === void 0 ? void 0 : _g.call(_f, lref);
                    precedingRef =
                        precedingRef === undefined
                            ? (_h = beforeRefs.unshift(lref)) === null || _h === void 0 ? void 0 : _h.first
                            : (_j = beforeRefs.insertAfter(precedingRef, lref)) === null || _j === void 0 ? void 0 : _j.first;
                    lref.link(this.segment, 0, precedingRef);
                    if ((0, referencePositions_1.refHasRangeLabels)(lref) || (0, referencePositions_1.refHasTileLabels)(lref)) {
                        this.hierRefCount++;
                    }
                    this.refCount++;
                    (_l = (_k = lref.callbacks) === null || _k === void 0 ? void 0 : _k.afterSlide) === null || _l === void 0 ? void 0 : _l.call(_k, lref);
                }
                else {
                    lref.link(undefined, 0, undefined);
                }
            }
        }
    }
    /**
     * @remarks This method should only be called by mergeTree.
     * @internal
     */
    addAfterTombstones(...refs) {
        var _a, _b, _c, _d, _e, _f, _g, _h, _j;
        var _k;
        const lastOffset = this.segment.cachedLength - 1;
        const afterRefs = (_b = (_a = this.refsByOffset[lastOffset]) === null || _a === void 0 ? void 0 : _a.after) !== null && _b !== void 0 ? _b : new collections_1.List();
        if (((_c = this.refsByOffset[lastOffset]) === null || _c === void 0 ? void 0 : _c.after) === undefined) {
            const refsAtOffset = ((_d = (_k = this.refsByOffset)[lastOffset]) !== null && _d !== void 0 ? _d : (_k[lastOffset] = { after: afterRefs }));
            (_e = refsAtOffset.after) !== null && _e !== void 0 ? _e : (refsAtOffset.after = afterRefs);
        }
        for (const iterable of refs) {
            for (const lref of iterable) {
                assertLocalReferences(lref);
                if ((0, referencePositions_1.refTypeIncludesFlag)(lref, ops_1.ReferenceType.StayOnRemove)) {
                    continue;
                }
                else if ((0, referencePositions_1.refTypeIncludesFlag)(lref, ops_1.ReferenceType.SlideOnRemove)) {
                    (_g = (_f = lref.callbacks) === null || _f === void 0 ? void 0 : _f.beforeSlide) === null || _g === void 0 ? void 0 : _g.call(_f, lref);
                    afterRefs.push(lref);
                    lref.link(this.segment, lastOffset, afterRefs.last);
                    if ((0, referencePositions_1.refHasRangeLabels)(lref) || (0, referencePositions_1.refHasTileLabels)(lref)) {
                        this.hierRefCount++;
                    }
                    this.refCount++;
                    (_j = (_h = lref.callbacks) === null || _h === void 0 ? void 0 : _h.afterSlide) === null || _j === void 0 ? void 0 : _j.call(_h, lref);
                }
                else {
                    lref.link(undefined, 0, undefined);
                }
            }
        }
    }
    /**
     * @remarks This method should only be called by mergeTree.
     * @internal
     */
    isAfterTombstone(lref) {
        var _a;
        const after = (_a = this.refsByOffset[lref.getOffset()]) === null || _a === void 0 ? void 0 : _a.after;
        if (after) {
            assertLocalReferences(lref);
            return after.includes(lref.getListNode());
        }
        return false;
    }
    /**
     * @remarks This method should only be called by mergeTree.
     * @internal
     */
    walkReferences(visitor, start, forward = true) {
        var _a, _b, _c, _d, _e, _f, _g;
        if (start !== undefined) {
            if (!this.has(start)) {
                throw new container_utils_1.UsageError("start must be in collection");
            }
            assertLocalReferences(start);
        }
        let offset = (_a = start === null || start === void 0 ? void 0 : start.getOffset()) !== null && _a !== void 0 ? _a : (forward ? 0 : this.segment.cachedLength - 1);
        const offsetPositions = new collections_1.List();
        offsetPositions.push((_b = this.refsByOffset[offset]) === null || _b === void 0 ? void 0 : _b.before, (_c = this.refsByOffset[offset]) === null || _c === void 0 ? void 0 : _c.at, (_d = this.refsByOffset[offset]) === null || _d === void 0 ? void 0 : _d.after);
        const startNode = start === null || start === void 0 ? void 0 : start.getListNode();
        const startList = startNode === null || startNode === void 0 ? void 0 : startNode.list;
        if (startList !== undefined) {
            if (forward) {
                while (!offsetPositions.empty && offsetPositions.first !== startNode) {
                    offsetPositions.shift();
                }
            }
            else {
                while (!offsetPositions.empty && offsetPositions.last !== startNode) {
                    offsetPositions.pop();
                }
            }
        }
        const listWalker = (pos) => {
            return (0, collections_1.walkList)(pos, (node) => visitor(node.data), startList === pos ? startNode : undefined, forward);
        };
        const increment = forward ? 1 : -1;
        while (offset >= 0 && offset < this.refsByOffset.length) {
            while (offsetPositions.length > 0) {
                const offsetPos = forward ? offsetPositions.shift() : offsetPositions.pop();
                if ((offsetPos === null || offsetPos === void 0 ? void 0 : offsetPos.data) !== undefined) {
                    if (listWalker(offsetPos.data) === false) {
                        return false;
                    }
                }
            }
            offset += increment;
            offsetPositions.push((_e = this.refsByOffset[offset]) === null || _e === void 0 ? void 0 : _e.before, (_f = this.refsByOffset[offset]) === null || _f === void 0 ? void 0 : _f.at, (_g = this.refsByOffset[offset]) === null || _g === void 0 ? void 0 : _g.after);
        }
        return true;
    }
}
exports.LocalReferenceCollection = LocalReferenceCollection;
//# sourceMappingURL=localReference.js.map