"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.iterateListValuesWhile = exports.walkList = exports.DoublyLinkedList = void 0;
const internal_1 = require("@fluidframework/telemetry-utils/internal");
class HeadNode {
    constructor(list) {
        this._next = this;
        this._prev = this;
        this.headNode = this;
        if (list) {
            this._list = list;
        }
    }
    get next() {
        return this._next === this.headNode ? undefined : this._next;
    }
    get prev() {
        return this._prev === this.headNode ? undefined : this._prev;
    }
    get list() {
        return this.headNode._list;
    }
}
// The any is needed for use in the remove function, where the nodes are defined with a generic type.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const DeadHead = new HeadNode(undefined);
class DataNode extends HeadNode {
    constructor(headNode, data) {
        super(undefined);
        this.data = data;
        this.headNode = headNode;
    }
}
function insertAfter(node, items) {
    let previousNode = node;
    const oldNext = previousNode._next;
    let newRange;
    for (const n of items) {
        const newNode = new DataNode(node.headNode, n);
        if (newRange === undefined) {
            newRange = { first: newNode, last: newNode };
        }
        else {
            newRange.last = newNode;
        }
        newNode._prev = previousNode;
        previousNode._next = newNode;
        previousNode = newNode;
    }
    oldNext._prev = previousNode;
    previousNode._next = oldNext;
    // explicitly prevent newRange from being undefined without casting,
    // and without additional conditionals, as this is used in some perf critical areas.
    // i could have just asserted, but that throws a non-user friendly error,
    // so i went with a more user-friendly error, which describes the
    // only condition that could lead to this being undefined in the current code.
    if (newRange === undefined) {
        throw new internal_1.UsageError("items must not be empty");
    }
    return newRange;
}
class DoublyLinkedList {
    constructor(values) {
        this._len = 0;
        this.headNode = new HeadNode(this);
        if (values !== undefined) {
            this.push(...values);
        }
    }
    find(predicate) {
        let found;
        walkList(this, (node) => {
            if (predicate(node, this)) {
                found = node;
                return false;
            }
        });
        return found;
    }
    map(callbackfn) {
        let node = this.first;
        const iterator = {
            next() {
                if (node === undefined) {
                    return { done: true, value: undefined };
                }
                const rtn = { value: callbackfn(node), done: false };
                node = node.next;
                return rtn;
            },
            [Symbol.iterator]() {
                return this;
            },
        };
        return iterator;
    }
    insertAfter(preceding, ...items) {
        if (!this._includes(preceding)) {
            throw new Error("preceding not in list");
        }
        this._len += items.length;
        return insertAfter(preceding, items);
    }
    pop() {
        return this.remove(this.last);
    }
    push(...items) {
        this._len += items.length;
        const start = this.headNode._prev;
        return insertAfter(start, items);
    }
    /**
     * Remove and return the first element
     */
    shift() {
        return this.remove(this.first);
    }
    /**
     * Insert `items` at start of list
     */
    unshift(...items) {
        this._len += items.length;
        return insertAfter(this.headNode, items);
    }
    /**
     * Remove nodes starting at `start` until either the `end` node is reached
     * or until `count` nodes have been removed. Returns the removed nodes as
     * a separate linked list
     */
    splice(start, countOrEnd) {
        const newList = new DoublyLinkedList();
        walkList(this, (node) => {
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            const removedNode = this._remove(node);
            // whats special here is we preserve the node
            // this allow looking up the old node in the new list
            // when something preserves a reference
            removedNode.headNode = newList.headNode;
            removedNode._next = newList.headNode;
            removedNode._prev = newList.headNode._prev;
            newList.headNode._prev._next = removedNode;
            newList.headNode._prev = removedNode;
            newList._len++;
            if (node === countOrEnd || newList.length === countOrEnd) {
                return false;
            }
        }, start);
        return newList;
    }
    includes(node) {
        return this._includes(node);
    }
    _includes(node) {
        return node instanceof DataNode && node.headNode === this.headNode;
    }
    _remove(node) {
        if (this._includes(node)) {
            node._prev._next = node._next;
            node._next._prev = node._prev;
            // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
            node.headNode = node._next = node._prev = DeadHead;
            this._len--;
            return node;
        }
        return undefined;
    }
    remove(node) {
        return this._remove(node);
    }
    [Symbol.iterator]() {
        let value = this.first;
        const iterator = {
            next() {
                if (value !== undefined) {
                    const rtn = { value, done: false };
                    value = value.next;
                    return rtn;
                }
                return { value: undefined, done: true };
            },
            [Symbol.iterator]() {
                return this;
            },
        };
        return iterator;
    }
    get length() {
        return this._len;
    }
    get empty() {
        return this._len === 0;
    }
    get first() {
        return this.headNode.next;
    }
    get last() {
        return this.headNode.prev;
    }
}
exports.DoublyLinkedList = DoublyLinkedList;
function walkList(list, visitor, start, forward = true) {
    let current;
    if (start) {
        if (!list.includes(start)) {
            throw new internal_1.UsageError("start must be in the provided list");
        }
        current = start;
    }
    else {
        current = forward ? list.first : list.last;
    }
    // cache the next node, incase the visitor mutates the list
    // need this to support splice
    let next = forward ? current?.next : current?.prev;
    while (current !== undefined) {
        if (visitor(current) === false) {
            return false;
        }
        current = next;
        next = forward ? next?.next : next?.prev;
    }
    return true;
}
exports.walkList = walkList;
/**
 * Creates a lazily evaluated iterable which returns values while the predicate returns true,
 * and stops iterating at the first value where the predicate is false.
 * @param start - the node to start the iteration from
 * @param includePredicate - determine if the current value be included in the iteration or stop if iteration
 */
function iterateListValuesWhile(start, includePredicate) {
    let next = start;
    const iterator = {
        next: () => {
            if (next !== undefined) {
                const current = next;
                next = current.next;
                if (includePredicate(current) === true) {
                    return { value: current.data, done: false };
                }
            }
            return { done: true, value: undefined };
        },
        [Symbol.iterator]() {
            return this;
        },
    };
    return iterator;
}
exports.iterateListValuesWhile = iterateListValuesWhile;
//# sourceMappingURL=list.js.map