/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert } from "@fluidframework/common-utils";
import { UsageError } from "@fluidframework/container-utils";
import { ListMakeHead, ListRemoveEntry } from "./collections";
import { ReferenceType } from "./ops";
import { addProperties } from "./properties";
import { minReferencePosition, maxReferencePosition, compareReferencePositions, refHasTileLabels, refHasRangeLabels, refGetRangeLabels, refGetTileLabels, refHasRangeLabel, refHasTileLabel, refTypeIncludesFlag, } from "./referencePositions";
/**
 * @internal
 */
export function _validateReferenceType(refType) {
    let exclusiveCount = 0;
    if (refTypeIncludesFlag(refType, ReferenceType.Transient)) {
        ++exclusiveCount;
    }
    if (refTypeIncludesFlag(refType, ReferenceType.SlideOnRemove)) {
        ++exclusiveCount;
    }
    if (refTypeIncludesFlag(refType, ReferenceType.StayOnRemove)) {
        ++exclusiveCount;
    }
    if (exclusiveCount > 1) {
        throw new UsageError("Reference types can only be one of Transient, SlideOnRemove, and StayOnRemove");
    }
}
/**
 * @deprecated - Use LocalReferencePosition
 */
export class LocalReference {
    /**
     * @deprecated - use createReferencePosition
     */
    constructor(client, initSegment, 
    /**
     * @deprecated - use getOffset
     */
    offset = 0, refType = ReferenceType.Simple, properties) {
        this.client = client;
        this.offset = offset;
        this.refType = refType;
        _validateReferenceType(refType);
        this.segment = initSegment;
        this.properties = properties;
    }
    /**
     * @deprecated - use minReferencePosition
     */
    min(b) {
        return minReferencePosition(this, b);
    }
    /**
     * @deprecated - use maxReferencePosition
     */
    max(b) {
        return maxReferencePosition(this, b);
    }
    /**
     * @deprecated - use compareReferencePositions
     */
    compare(b) {
        return compareReferencePositions(this, b);
    }
    /**
     * @deprecated - use getLocalReferencePosition
     */
    toPosition() {
        return this.getClient().localReferencePositionToPosition(this);
    }
    /**
     * @deprecated - use refHasTileLabels
     */
    hasTileLabels() {
        return refHasTileLabels(this);
    }
    /**
     * @deprecated - use refHasRangeLabels
     */
    hasRangeLabels() {
        return refHasRangeLabels(this);
    }
    /**
     * @deprecated - use refHasTileLabel
     */
    hasTileLabel(label) {
        return refHasTileLabel(this, label);
    }
    /**
     * @deprecated - use refHasRangeLabel
     */
    hasRangeLabel(label) {
        return refHasRangeLabel(this, label);
    }
    /**
     * @deprecated - use refGetTileLabels
     */
    getTileLabels() {
        return refGetTileLabels(this);
    }
    /**
     * @deprecated - use refGetRangeLabels
     */
    getRangeLabels() {
        return refGetRangeLabels(this);
    }
    isLeaf() {
        return false;
    }
    addProperties(newProps, op) {
        this.properties = addProperties(this.properties, newProps, op);
    }
    /**
     * @deprecated - no longer supported
     */
    getClient() {
        return this.client;
    }
    getSegment() {
        return this.segment;
    }
    getOffset() {
        return this.offset;
    }
    getProperties() {
        return this.properties;
    }
}
/**
 * @deprecated - use DetachedReferencePosition
 */
LocalReference.DetachedPosition = -1;
function assertLocalReferences(lref) {
    assert(lref instanceof LocalReference, 0x2e0 /* "lref not a Local Reference" */);
}
/**
 * Represents a collection of {@link ReferencePosition}s associated with one segment in a merge-tree.
 */
export class LocalReferenceCollection {
    /**
     *
     * @internal - this method should only be called by mergeTree
     */
    constructor(
    /** Segment this `LocalReferenceCollection` is associated to. */
    segment, initialRefsByfOffset = new Array(segment.cachedLength)) {
        this.segment = segment;
        /**
         *
         * @internal - this method should only be called by mergeTree
         */
        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);
            }
            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;
        }
    }
    /**
     *
     * @internal - 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 next;
                    }
                }
                return { value: undefined, done: true };
            },
            [Symbol.iterator]() {
                return this;
            },
        };
        return iterator;
    }
    /**
     *
     * @internal - this method should only be called by mergeTree
     */
    clear() {
        this.refCount = 0;
        this.hierRefCount = 0;
        const detachSegments = (refs) => {
            if (refs) {
                for (const r of refs) {
                    if (r.segment === this.segment) {
                        r.segment = undefined;
                    }
                }
            }
        };
        for (let i = 0; i < this.refsByOffset.length; i++) {
            const refsAtOffset = this.refsByOffset[i];
            if (refsAtOffset) {
                detachSegments(refsAtOffset.before);
                detachSegments(refsAtOffset.at);
                detachSegments(refsAtOffset.before);
                this.refsByOffset[i] = undefined;
            }
        }
    }
    /**
     *
     * @internal - this method should only be called by mergeTree
     */
    get empty() {
        return this.refCount === 0;
    }
    /**
     *
     * @internal - this method should only be called by mergeTree
     */
    createLocalRef(offset, refType, properties, client) {
        const ref = new LocalReference(client, this.segment, offset, refType, properties);
        if (!refTypeIncludesFlag(ref, ReferenceType.Transient)) {
            this.addLocalRef(ref);
        }
        return ref;
    }
    /**
     *
     * @internal - this method should only be called by mergeTree
     */
    addLocalRef(lref) {
        var _a, _b;
        assert(!refTypeIncludesFlag(lref, ReferenceType.Transient), 0x2df /* "transient references cannot be bound to segments" */);
        assertLocalReferences(lref);
        const refsAtOffset = this.refsByOffset[lref.offset] =
            (_a = this.refsByOffset[lref.offset]) !== null && _a !== void 0 ? _a : { at: ListMakeHead() };
        const atRefs = refsAtOffset.at =
            (_b = refsAtOffset.at) !== null && _b !== void 0 ? _b : ListMakeHead();
        atRefs.enqueue(lref);
        if (refHasRangeLabels(lref) || refHasTileLabels(lref)) {
            this.hierRefCount++;
        }
        this.refCount++;
    }
    /**
     *
     * @internal - this method should only be called by mergeTree
     */
    removeLocalRef(lref) {
        assertLocalReferences(lref);
        const tryRemoveRef = (refs) => {
            if (refs) {
                let node = refs;
                do {
                    node = node.next;
                    if (node.data === lref) {
                        ListRemoveEntry(node);
                        if (refHasRangeLabels(lref) || refHasTileLabels(lref)) {
                            this.hierRefCount--;
                        }
                        this.refCount--;
                        return lref;
                    }
                } while (!node.isHead);
            }
        };
        const refAtOffset = this.refsByOffset[lref.offset];
        if (refAtOffset !== undefined) {
            let ref = tryRemoveRef(refAtOffset.before);
            if (ref) {
                return ref;
            }
            ref = tryRemoveRef(refAtOffset.at);
            if (ref) {
                return ref;
            }
            ref = tryRemoveRef(refAtOffset.after);
            if (ref) {
                return ref;
            }
        }
    }
    /**
     * @internal - this method should only be called by mergeTree
     *
     * 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.
     */
    append(other) {
        if (!other || other.empty) {
            return;
        }
        this.hierRefCount += other.hierRefCount;
        this.refCount += other.refCount;
        other.hierRefCount = 0;
        for (const lref of other) {
            lref.segment = this.segment;
            lref.offset += this.refsByOffset.length;
        }
        this.refsByOffset.push(...other.refsByOffset);
    }
    /**
     * @internal - this method should only be called by mergeTree
     *
     * 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.
     */
    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) {
                lref.segment = splitSeg;
                lref.offset -= offset;
                if (refHasRangeLabels(lref) || refHasTileLabels(lref)) {
                    this.hierRefCount--;
                    localRefs.hierRefCount++;
                }
                this.refCount--;
                localRefs.refCount++;
            }
        }
        else {
            // shrink the offset array when empty and splitting
            this.refsByOffset.length = offset;
        }
    }
    addBeforeTombstones(...refs) {
        var _a, _b, _c, _d, _e;
        const beforeRefs = (_b = (_a = this.refsByOffset[0]) === null || _a === void 0 ? void 0 : _a.before) !== null && _b !== void 0 ? _b : ListMakeHead();
        for (const iterable of refs) {
            for (const lref of iterable) {
                assertLocalReferences(lref);
                if (refTypeIncludesFlag(lref, ReferenceType.SlideOnRemove)) {
                    beforeRefs.push(lref);
                    lref.segment = this.segment;
                    lref.offset = 0;
                    if (refHasRangeLabels(lref) || refHasTileLabels(lref)) {
                        this.hierRefCount++;
                    }
                    this.refCount++;
                }
                else {
                    lref.segment = undefined;
                }
            }
        }
        if (!beforeRefs.empty() && ((_c = this.refsByOffset[0]) === null || _c === void 0 ? void 0 : _c.before) === undefined) {
            const refsAtOffset = this.refsByOffset[0] =
                (_d = this.refsByOffset[0]) !== null && _d !== void 0 ? _d : { before: beforeRefs };
            refsAtOffset.before = (_e = refsAtOffset.before) !== null && _e !== void 0 ? _e : beforeRefs;
        }
    }
    addAfterTombstones(...refs) {
        var _a, _b, _c, _d, _e;
        const lastOffset = this.refsByOffset.length - 1;
        const afterRefs = (_b = (_a = this.refsByOffset[lastOffset]) === null || _a === void 0 ? void 0 : _a.after) !== null && _b !== void 0 ? _b : ListMakeHead();
        for (const iterable of refs) {
            for (const lref of iterable) {
                assertLocalReferences(lref);
                if (refTypeIncludesFlag(lref, ReferenceType.SlideOnRemove)) {
                    afterRefs.push(lref);
                    lref.segment = this.segment;
                    lref.offset = this.segment.cachedLength - 1;
                    if (refHasRangeLabels(lref) || refHasTileLabels(lref)) {
                        this.hierRefCount++;
                    }
                    this.refCount++;
                }
                else {
                    lref.segment = undefined;
                }
            }
        }
        if (!afterRefs.empty() && ((_c = this.refsByOffset[lastOffset]) === null || _c === void 0 ? void 0 : _c.after) === undefined) {
            const refsAtOffset = this.refsByOffset[lastOffset] =
                (_d = this.refsByOffset[lastOffset]) !== null && _d !== void 0 ? _d : { after: afterRefs };
            refsAtOffset.after = (_e = refsAtOffset.after) !== null && _e !== void 0 ? _e : afterRefs;
        }
    }
}
//# sourceMappingURL=localReference.js.map