"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.setValidateRefCount = exports.filterLocalReferencePositions = exports.anyLocalReferencePosition = exports.createDetachedLocalReferencePosition = exports.SlidingPreference = void 0;
const internal_1 = require("@fluidframework/core-utils/internal");
const internal_2 = require("@fluidframework/telemetry-utils/internal");
const index_js_1 = require("./collections/index.js");
const mergeTreeTracking_js_1 = require("./mergeTreeTracking.js");
const ops_js_1 = require("./ops.js");
const properties_js_1 = require("./properties.js");
const referencePositions_js_1 = require("./referencePositions.js");
/**
 * Dictates the preferential direction for a {@link ReferencePosition} to slide
 * in a merge-tree
 * @legacy
 * @alpha
 */
exports.SlidingPreference = {
    /**
     * Prefer sliding towards the start of the tree
     */
    BACKWARD: 0,
    /**
     * Prefer sliding towards the end of the tree
     */
    FORWARD: 1,
};
function _validateReferenceType(refType) {
    let exclusiveCount = 0;
    if ((0, referencePositions_js_1.refTypeIncludesFlag)(refType, ops_js_1.ReferenceType.Transient)) {
        ++exclusiveCount;
    }
    if ((0, referencePositions_js_1.refTypeIncludesFlag)(refType, ops_js_1.ReferenceType.SlideOnRemove)) {
        ++exclusiveCount;
    }
    if ((0, referencePositions_js_1.refTypeIncludesFlag)(refType, ops_js_1.ReferenceType.StayOnRemove)) {
        ++exclusiveCount;
    }
    if (exclusiveCount > 1) {
        throw new internal_2.UsageError("Reference types can only be one of Transient, SlideOnRemove, and StayOnRemove");
    }
}
/**
 * @privateRemarks This should not be exported outside merge tree.
 * @internal
 */
class LocalReference {
    get trackingCollection() {
        return (this._trackingCollection ??= new mergeTreeTracking_js_1.TrackingGroupCollection(this));
    }
    constructor(refType = ops_js_1.ReferenceType.Simple, properties, slidingPreference = exports.SlidingPreference.FORWARD, canSlideToEndpoint) {
        this.refType = refType;
        this.slidingPreference = slidingPreference;
        this.canSlideToEndpoint = canSlideToEndpoint;
        this.offset = 0;
        _validateReferenceType(refType);
        this.properties = properties;
    }
    link(segment, offset, listNode) {
        if (listNode !== this.listNode && this.listNode !== undefined) {
            this.segment?.localRefs?.removeLocalRef(this);
        }
        this.listNode = listNode;
        if (segment !== this.segment) {
            const groups = [];
            for (const tg of this.trackingCollection.trackingGroups) {
                tg.unlink(this);
                groups.push(tg);
            }
            this.segment = segment;
            for (const tg of groups)
                tg.link(this);
        }
        this.offset = offset;
    }
    isLeaf() {
        return false;
    }
    addProperties(newProps) {
        this.properties = (0, properties_js_1.addProperties)(this.properties, newProps);
    }
    getSegment() {
        return this.segment;
    }
    getOffset() {
        return this.offset;
    }
    getListNode() {
        return this.listNode;
    }
    getProperties() {
        return this.properties;
    }
}
/**
 * Creates a new detached local reference.
 * @internal
 */
function createDetachedLocalReferencePosition(slidingPreference, refType) {
    return new LocalReference(refType, undefined, slidingPreference);
}
exports.createDetachedLocalReferencePosition = createDetachedLocalReferencePosition;
function assertLocalReferences(lref) {
    (0, internal_1.assert)(lref instanceof LocalReference, 0x2e0 /* "lref not a Local Reference" */);
}
/**
 * Determines if the given function is 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;
/**
 * Finds the local reference positions that satisfy the given predicate.
 */
function* filterLocalReferencePositions(collection, predicate) {
    for (const pos of collection) {
        if (predicate(pos)) {
            yield pos;
        }
    }
}
exports.filterLocalReferencePositions = filterLocalReferencePositions;
/**
 * Injectable hook for validating that the refCount property matches the
 * expected value
 */
let validateRefCount;
function setValidateRefCount(cb) {
    validateRefCount = cb;
}
exports.setValidateRefCount = setValidateRefCount;
/**
 * Represents a collection of {@link LocalReferencePosition}s associated with
 * one segment in a merge-tree.
 * Represents a collection of {@link LocalReferencePosition}s associated with one segment in a merge-tree.
 * @sealed
 *
 * @internal
 */
class LocalReferenceCollection {
    static append(seg1, seg2) {
        if (seg2.localRefs && !seg2.localRefs.empty) {
            if (!seg1.localRefs) {
                seg1.localRefs = new LocalReferenceCollection(seg1);
            }
            (0, internal_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;
        }
        validateRefCount?.(seg1.localRefs);
        validateRefCount?.(seg2.localRefs);
    }
    static setOrGet(segment) {
        return (segment.localRefs ??= new LocalReferenceCollection(segment));
    }
    constructor(
    /**
     * The segment this `LocalReferenceCollection` is associated with.
     */
    segment, initialRefsByfOffset = Array.from({
        length: segment.cachedLength,
    })) {
        this.segment = segment;
        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;
    }
    /**
     * Returns an iterator over this LocalReferenceCollection.
     * @remarks This method should only be called by mergeTree.
     */
    [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;
    }
    /**
     * Determines if the collection has no references in it.
     * @remarks This method should only be called by mergeTree.
     */
    get empty() {
        validateRefCount?.(this);
        return this.refCount === 0;
    }
    /**
     * Creates a new local reference.
     * @remarks This method should only be called by mergeTree.
     */
    createLocalRef(offset, refType, properties, slidingPreference, canSlideToEndpoint) {
        const ref = new LocalReference(refType, properties, slidingPreference, canSlideToEndpoint);
        ref.link(this.segment, offset, undefined);
        if (!(0, referencePositions_js_1.refTypeIncludesFlag)(ref, ops_js_1.ReferenceType.Transient)) {
            this.addLocalRef(ref, offset);
        }
        validateRefCount?.(this);
        return ref;
    }
    /**
     * Adds a local reference to the collection.
     * @remarks This method should only be called by mergeTree.
     */
    addLocalRef(lref, offset) {
        assertLocalReferences(lref);
        (0, internal_1.assert)(offset < this.segment.cachedLength, 0x348 /* offset cannot be beyond segment length */);
        if ((0, referencePositions_js_1.refTypeIncludesFlag)(lref, ops_js_1.ReferenceType.Transient)) {
            lref.link(this.segment, offset, undefined);
        }
        else {
            const refsAtOffset = (this.refsByOffset[offset] = this.refsByOffset[offset] ?? {
                at: new index_js_1.DoublyLinkedList(),
            });
            const atRefs = (refsAtOffset.at = refsAtOffset.at ?? new index_js_1.DoublyLinkedList());
            lref.link(this.segment, offset, atRefs.push(lref).last);
            this.refCount++;
        }
        validateRefCount?.(this);
    }
    /**
     * Removes a local reference from the collection.
     * @remarks This method should only be called by mergeTree.
     */
    removeLocalRef(lref) {
        if (this.has(lref)) {
            assertLocalReferences(lref);
            const node = lref.getListNode();
            node?.list?.remove(node);
            lref.link(undefined, 0, undefined);
            this.refCount--;
            validateRefCount?.(this);
            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.
     */
    append(other) {
        if (!other || other.empty) {
            return;
        }
        this.refCount += other.refCount;
        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);
        other.refsByOffset.length = 0;
    }
    /**
     * Returns true of the local reference is in the collection, otherwise false.
     *
     * @remarks This method should only be called by mergeTree.
     */
    has(lref) {
        if (!(lref instanceof LocalReference) ||
            (0, referencePositions_js_1.refTypeIncludesFlag)(lref, ops_js_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 (!!refsAtOffset?.before?.includes(listNode) ||
            !!refsAtOffset?.at?.includes(listNode) ||
            !!refsAtOffset?.after?.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.
     */
    split(offset, splitSeg) {
        if (this.empty) {
            // shrink the offset array when empty and splitting
            this.refsByOffset.length = offset;
        }
        else {
            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());
                this.refCount--;
                localRefs.refCount++;
            }
        }
        validateRefCount?.(this);
    }
    /**
     * Insert a reference before tombstoned references.
     * @remarks This method should only be called by mergeTree.
     */
    addBeforeTombstones(...refs) {
        const beforeRefs = this.refsByOffset[0]?.before ?? new index_js_1.DoublyLinkedList();
        if (this.refsByOffset[0]?.before === undefined) {
            const refsAtOffset = (this.refsByOffset[0] ??= { before: beforeRefs });
            refsAtOffset.before ??= beforeRefs;
        }
        let precedingRef;
        for (const iterable of refs) {
            for (const lref of iterable) {
                assertLocalReferences(lref);
                if ((0, referencePositions_js_1.refTypeIncludesFlag)(lref, ops_js_1.ReferenceType.StayOnRemove)) {
                    continue;
                }
                else if ((0, referencePositions_js_1.refTypeIncludesFlag)(lref, ops_js_1.ReferenceType.SlideOnRemove)) {
                    lref.callbacks?.beforeSlide?.(lref);
                    precedingRef =
                        precedingRef === undefined
                            ? beforeRefs.unshift(lref)?.first
                            : beforeRefs.insertAfter(precedingRef, lref)?.first;
                    lref.link(this.segment, 0, precedingRef);
                    this.refCount++;
                    lref.callbacks?.afterSlide?.(lref);
                }
                else {
                    lref.link(undefined, 0, undefined);
                }
            }
        }
        validateRefCount?.(this);
    }
    /**
     * Insert a reference after tombstoned references.
     * @remarks This method should only be called by mergeTree.
     */
    addAfterTombstones(...refs) {
        const lastOffset = this.segment.cachedLength - 1;
        const afterRefs = this.refsByOffset[lastOffset]?.after ?? new index_js_1.DoublyLinkedList();
        if (this.refsByOffset[lastOffset]?.after === undefined) {
            const refsAtOffset = (this.refsByOffset[lastOffset] ??= { after: afterRefs });
            refsAtOffset.after ??= afterRefs;
        }
        for (const iterable of refs) {
            for (const lref of iterable) {
                assertLocalReferences(lref);
                if ((0, referencePositions_js_1.refTypeIncludesFlag)(lref, ops_js_1.ReferenceType.StayOnRemove)) {
                    continue;
                }
                else if ((0, referencePositions_js_1.refTypeIncludesFlag)(lref, ops_js_1.ReferenceType.SlideOnRemove)) {
                    lref.callbacks?.beforeSlide?.(lref);
                    afterRefs.push(lref);
                    lref.link(this.segment, lastOffset, afterRefs.last);
                    this.refCount++;
                    lref.callbacks?.afterSlide?.(lref);
                }
                else {
                    lref.link(undefined, 0, undefined);
                }
            }
        }
        validateRefCount?.(this);
    }
    /**
     * Determines if a reference is after tombstoned references.
     * @remarks This method should only be called by mergeTree.
     */
    isAfterTombstone(lref) {
        const after = this.refsByOffset[lref.getOffset()]?.after;
        if (after) {
            assertLocalReferences(lref);
            return after.includes(lref.getListNode());
        }
        return false;
    }
    /**
     * Walks all of the references in a collection.
     * @remarks This method should only be called by mergeTree.
     */
    walkReferences(visitor, start, forward = true) {
        if (start !== undefined) {
            if (!this.has(start)) {
                throw new internal_2.UsageError("start must be in collection");
            }
            assertLocalReferences(start);
        }
        let offset = start?.getOffset() ?? (forward ? 0 : this.segment.cachedLength - 1);
        const offsetPositions = new index_js_1.DoublyLinkedList();
        offsetPositions.push(this.refsByOffset[offset]?.before, this.refsByOffset[offset]?.at, this.refsByOffset[offset]?.after);
        const startNode = start?.getListNode();
        const startList = 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, index_js_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?.data !== undefined && listWalker(offsetPos.data) === false) {
                    return false;
                }
            }
            offset += increment;
            offsetPositions.push(this.refsByOffset[offset]?.before, this.refsByOffset[offset]?.at, this.refsByOffset[offset]?.after);
        }
        return true;
    }
}
exports.LocalReferenceCollection = LocalReferenceCollection;
//# sourceMappingURL=localReference.js.map