"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
/*
Copyright 2019 Adobe
All Rights Reserved.

NOTICE: Adobe permits you to use, modify, and distribute this file in
accordance with the terms of the Adobe license agreement accompanying
it. If you have received this file from a source other than Adobe,
then your use, modification, or distribution of it requires the prior
written permission of Adobe.
*/
require("../common/ResizeObserver");
const ResizeObserver_1 = __importDefault(require("../common/ResizeObserver"));
const Margin_1 = __importStar(require("../common/Margin"));
const getStableArray_1 = __importDefault(require("./getStableArray"));
const react_1 = __importDefault(require("react"));
const lerp_1 = __importDefault(require("../common/lerp"));
const initialVisibleItemCount = 30;
class ClassProperties {
    constructor(type) {
        //  we mark this on all class properties after a container resize
        //  so we can try to reload them if we still have
        //  components of their class present
        this.valid = false;
        this.column = null;
        //  these fields are used to estimate the size of future elements of this class
        this.totalItemWidth = 0;
        this.totalItemHeight = 0;
        this.totalItemCount = 0;
        this.type = type;
    }
    invalidate() {
        this.valid = false;
        this.totalItemCount = 0;
        this.totalItemWidth = 0;
        this.totalItemHeight = 0;
    }
    //  this function stores and tracks an average item size of this type.
    //  this helps us more accurately estimate scroll location when doing flow layout.
    setItemSize(width, height) {
        this.totalItemWidth += width;
        this.totalItemHeight += height;
        this.totalItemCount++;
        // recalculate width/height
        this.width = Math.round(this.totalItemWidth / this.totalItemCount);
        this.height = Math.round(this.totalItemHeight / this.totalItemCount);
    }
    setFromCss(css) {
        if (this.cssInline == null) {
            //  we only record the first css display.
            //  chrome is changing inline-block to block occasionally
            //  this does mean clients cannot change display
            //  with responsive media queries.
            this.cssInline = (css.display || "").indexOf("inline") >= 0;
        }
        this.cssMargin = Margin_1.default.fromCssMargin(css);
        let columnCountCss = css.getPropertyValue("--column-count-self").trim();
        let column = null;
        if (columnCountCss) {
            let count = columnCountCss === "auto" ? null : parseInt(columnCountCss);
            let optimumWidth = null;
            if (count == null) {
                optimumWidth = (0, Margin_1.parseCssSize)(css.getPropertyValue("--optimum-width"));
                if (optimumWidth === 0) {
                    let minWidth = (0, Margin_1.parseCssSize)(css.minWidth);
                    let maxWidth = (0, Margin_1.parseCssSize)(css.maxWidth);
                    if (minWidth === 0 || maxWidth === 0) {
                        throw new Error("Expected either --optimum-width or min-width and max-width");
                    }
                    optimumWidth = Math.ceil((minWidth + maxWidth) / 2);
                }
            }
            column = { count, optimumWidth };
        }
        this.column = column;
    }
}
class ItemProperties {
    constructor(type) {
        this.x = 0;
        this.y = 0;
        this.width = 0;
        this.height = 0;
        this.rendered = false;
        this.type = type;
    }
}
function isHTMLElement(node) {
    return node != null && node.nodeType === 1;
}
//  returns a memoized pixel css position.
const px = (() => {
    const cache = [];
    return (value) => {
        let cached = cache[value];
        if (cached === undefined) {
            cached = cache[value] = `${value}px`;
        }
        return cached;
    };
})();
/**
 * This represents state which we cache permanently on the container.
 * Changes to this DO NOT force a react re-render.
 * By contrast changes to our react state does force a re-render.
 */
class VirtualManager {
    constructor(props) {
        this.itemProperties = {};
        this.classProperties = new Map();
        this.containerPadding = new Margin_1.default(0);
        this.scrollAnchor = null;
        this.isScrolling = false;
        //  TODO: This needs to be fixed for horizontal mode. Somehow we missed this.
        this.lastScrollTop = 0;
        this.scrollDirection = 0; // 1 = down, -1 = up, 0 = none
        this.scrollDuration = 0.5;
        this.scrollWaitAfterFinish = 0.2;
        this.container = props.container;
        this.containerWidth = this.container.clientWidth;
        this.container[VirtualManager.symbol] = this;
        this.horizontal = props.horizontal;
        this.itemLookup = new Map();
        this.itemKey = props.itemKey;
        this.itemType = props.itemType;
        this.itemRect = props.itemRect;
        this.onresize = this.onresize.bind(this);
        this.resizeObserver = new ResizeObserver_1.default(this.onresize);
        this.updateAndLayout = this.updateAndLayout.bind(this);
        this.container.addEventListener("scroll", (e) => {
            this.updateAndLayout();
        });
        this.container.addEventListener("wheel", (e) => {
            this.cancelScrollAnimation();
        });
        this.placeholder = document.createElement("div");
        this.container.appendChild(this.placeholder);
        this.update(props);
    }
    update(props) {
        this.items = props.items;
        this.renderKeys = props.renderKeys;
        this.setRenderKeys = props.setRenderKeys;
        this.onLayout = props.onLayout;
        if (!this.isScrolling) {
            // we skip these time consuming steps if we are scrolling.
            // since they will be done at the end of the call chain anyways.
            let checkForDuplicates = new Set();
            for (let item of this.items) {
                let key = this.itemKey(item);
                if (checkForDuplicates.has(key)) {
                    console.error(`Duplicate items:`, this.itemLookup.get(key), item);
                    throw new Error(`Item keys must be unique. Found duplicate: ${key}`);
                }
                checkForDuplicates.add(key);
                this.itemLookup.set(key, item);
            }
            this.updateAndLayout(true);
        }
        return this.renderKeys;
    }
    static getReactElements(items, renderKeys, itemKey, itemType, factory, cache) {
        const itemsByKey = new Map();
        for (let item of items) {
            itemsByKey.set(itemKey(item), item);
        }
        return renderKeys.map((key, elementIndex) => {
            const elementKey = String(elementIndex);
            const item = itemsByKey.get(key);
            if (item == null) {
                // if the item is not found it may have been temporarily hidden
                // we will try to render it from cached element.
                const cached = cache[key];
                if (cached) {
                    return cached;
                }
                /// console.warn("item missing: ", { key, elementIndex, items })
                return null;
            }
            return (cache[key] = react_1.default.cloneElement(factory(item), {
                key: elementKey,
                "data-key": key,
                "data-type": itemType(item),
            }));
        });
    }
    getItemRect(item, key) {
        if (item == null && key != null) {
            item = this.itemLookup.get(key) || null;
        }
        if (item != null) {
            let rect = this.itemRect != null ? this.itemRect(item) : null;
            if (rect != null) {
                if (rect.x == null ||
                    rect.y == null ||
                    rect.width == null ||
                    rect.height == null) {
                    throw new Error(`Item rect missing required properties (x,y,width,height): ${JSON.stringify(rect)}`);
                }
                return rect;
            }
            if (key == null) {
                key = this.itemKey(item);
            }
            if (key != null) {
                let props = this.itemProperties[key];
                if (props != null) {
                    return props;
                }
            }
        }
        return null;
    }
    updateAndLayout(forceLayout = false) {
        this.isScrolling = true;
        let needsLayout = this.updateIndexes();
        this.ensureElementsObservedAndSized();
        if (needsLayout || forceLayout) {
            this.layoutChildren();
            // fire layout event
            if (this.onLayout) {
                this.onLayout();
            }
        }
        this.isScrolling = false;
    }
    layoutChildren() {
        let containerCss = window.getComputedStyle(this.container);
        let columnGap = (0, Margin_1.parseCssSize)(containerCss.getPropertyValue("--column-gap"));
        let rowGap = (0, Margin_1.parseCssSize)(containerCss.getPropertyValue("--row-gap"));
        // create quick element lookup by key.
        let elementLookup = this.getElementLookupByKey();
        let x = 0, y = 0, width = this.containerWidth - this.containerPadding.horizontal, height = 0;
        function newLine() {
            x = 0;
            y = height + rowGap;
        }
        // now iterate items
        for (let item of this.items) {
            let key = this.itemKey(item);
            let element = elementLookup.get(key);
            if (this.itemRect != null) {
                let rect = this.getItemRect(item, key);
                if (element) {
                    const { style } = element;
                    style.position = `absolute`;
                    style.left = px(rect.x);
                    style.top = px(rect.y);
                    style.width = px(rect.width);
                    style.height = px(rect.height);
                }
                height = rect.y + rect.height;
                continue;
            }
            let type = this.itemType(item);
            let properties = this.getItemProperties(key, type);
            let classProps = this.classProperties.get(type);
            if (!classProps) {
                // console.warn("missing class properties: " + type);
                break;
            }
            let margin = classProps.cssMargin;
            let inline = classProps.cssInline;
            let elementWidth = properties.width + margin.horizontal;
            let elementHeight = properties.height + margin.vertical;
            let left, top;
            // new column layout
            let { column } = classProps;
            if (column) {
                let count = column.count ||
                    Math.max(1, Math.round(width / (column.optimumWidth + columnGap)));
                let index = Math.round(x / (elementWidth + columnGap));
                elementWidth = Math.floor((width - columnGap * (count - 1)) / count);
                // now we will tweak the x position to make sure we right align
                // and distribute extra pixels fairly evenly across all columns
                let l2r = index * (elementWidth + columnGap);
                let r2l = width -
                    (count - index) * (elementWidth + columnGap) +
                    columnGap;
                let alpha = count == 1 ? 0 : index / (count - 1);
                x = Math.round((0, lerp_1.default)(l2r, r2l, alpha));
            }
            if (x > 0 &&
                !this.horizontal &&
                (!inline || x + elementWidth > width)) {
                newLine();
            }
            left = x + this.containerPadding.left;
            top = y + this.containerPadding.top;
            if (this.horizontal) {
                width = Math.max(width, x + elementWidth);
            }
            height = Math.max(height, y + elementHeight);
            if (inline) {
                x += elementWidth + columnGap;
            }
            else {
                y += elementHeight + rowGap;
            }
            // store on properties
            properties.x = left;
            properties.y = top;
            if (element) {
                // track that this element has actually been rendered
                properties.rendered = true;
                // now absolutely position if there is an element
                const { style } = element;
                if (style.position !== "absolute") {
                    style.position = "absolute";
                }
                style.left = px(left);
                style.top = px(top);
                if (column != null) {
                    style.width = px(elementWidth);
                }
                if (!inline) {
                    style.width = px(width - margin.horizontal);
                }
            }
        }
        //  we do this immediately after a layout of children.
        //  there is logic in the scrollAnimationCallback that can correct
        //  for an updated measurement of children which could cause visual jump
        //  calling this immediately fixes it immediately so there will be no visual jump
        //  what can cause the jump is as follows:
        //      We estimate a scroll location at 10,000px.
        //      We scroll to that location.
        //      Upon actual sizing of some *estimated* elements, it turns out we need to be at 10,050 px.
        //      So we want to immediately correct the position rather than either
        //      being 50 pixels off or waiting a full animation frame to correct it.
        this.scrollAnimationCallback();
        // increase the placeholders height to match our layed out height.
        if (this.horizontal) {
            this.placeholder.style.width = px(width);
        }
        else {
            this.placeholder.style.height = px(height);
        }
    }
    getElementLookupByKey() {
        let elementLookup = new Map();
        let index = 0;
        for (let child = this.container.firstChild; child != null; child = child.nextSibling) {
            if (isHTMLElement(child)) {
                let key = child.dataset.key;
                if (key) {
                    elementLookup.set(key, child);
                }
                else if (child.previousSibling != null) {
                    // the first child is the height placeholder element
                    // every other element should have a data-key property
                    // if not that means they used a function or class component
                    //  that is not passing ...otherProps to it's rendered element.
                    let item = this.items[index];
                    throw new Error(`Virtualizer item error: Function and Class components must pass through ...otherProps to rendered element. ie: <div {...otherProps}></div>. Check element created for ${JSON.stringify(item)}`);
                }
            }
            index++;
        }
        return elementLookup;
    }
    ensureElementsObservedAndSized() {
        this.resizeObserver.observe(this.container);
        for (let child = this.container.firstChild; child != null; child = child.nextSibling) {
            if (child.nodeType === 1) {
                this.resizeObserver.observe(child);
                // also make sure we've sized it if available.
                // Browser ResizeObserver does not send resize events for inline elements.
                this.onElementSized(child);
            }
        }
    }
    getVisibleRect() {
        let x = this.container.scrollLeft;
        let y = this.container.scrollTop;
        let width = this.container.clientWidth;
        let height = this.container.clientHeight;
        return { x, y, width, height };
    }
    get pageSize() {
        return this.container[this.horizontal ? "clientWidth" : "clientHeight"];
    }
    get prerenderOtherDirection() {
        return 100;
    }
    get prerenderScrollDirection() {
        return 500;
    }
    get isManualLayout() {
        return this.itemRect != null;
    }
    getRenderItemIndices(scrollDirection) {
        let { horizontal } = this;
        let beginning = Math.max(0, this.container[horizontal ? "scrollLeft" : "scrollTop"]);
        let { pageSize, prerenderScrollDirection, prerenderOtherDirection } = this;
        if (scrollDirection === 0) {
            // if we aren't scrolling then each direction is other.
            prerenderScrollDirection = prerenderOtherDirection;
        }
        let end = beginning + pageSize;
        let totalPagesSize = pageSize + prerenderScrollDirection + prerenderOtherDirection;
        // now expand top
        beginning = Math.max(0, beginning -
            (scrollDirection >= 0
                ? prerenderOtherDirection
                : prerenderScrollDirection));
        // then expand bottom by whatever is remaining. (if this is larger than content area, that is fine)
        end = beginning + totalPagesSize;
        const renderKeys = new Set();
        const existingKeys = new Set();
        // keys which we are adding so that we can pre-size them.
        // only used without manual layout.
        // This pre-sizing code ensures that we always contain at least one of each
        // item type in the document so that we can capture sizing and css class properties.
        const presize = !this.isManualLayout;
        const presizeTypes = presize ? new Map() : null;
        let size = horizontal ? "width" : "height";
        let position = horizontal ? "x" : "y";
        for (let index = 0; index < this.items.length; index++) {
            const item = this.items[index];
            const key = this.itemKey(item);
            existingKeys.add(key);
            const rect = this.getItemRect(item, key);
            if (rect &&
                rect[position] + rect[size] > beginning &&
                rect[position] <= end) {
                renderKeys.add(key);
            }
            else if (presizeTypes != null) {
                let type = this.itemType(item);
                if (!presizeTypes.has(type)) {
                    presizeTypes.set(type, key);
                }
            }
        }
        // we have to render *some* items since we may only get their accurate reported positions after they are rendered.
        if (renderKeys.size === 0) {
            for (let i = 0; i < initialVisibleItemCount && i < this.items.length; i++) {
                renderKeys.add(this.itemKey(this.items[i]));
            }
        }
        // we also want to always keep rendering a focused item so add this too
        const focusedKey = this.getFocusedItemKey();
        if (focusedKey) {
            renderKeys.add(focusedKey);
        }
        // finally, if we are pre-sizing elements we need to add some of each type.
        if (presizeTypes && presizeTypes.size > 0) {
            for (let key of presizeTypes.values()) {
                renderKeys.add(key);
            }
        }
        return [renderKeys, existingKeys];
    }
    /**
     * Clip scrollTop - UXP should do this automatically, but it doesn't due to bug UXP-10612
     * Can remove this once UXP bug is fixed
     * @returns number between 0 the container area - total area
     */
    getClippedScrollTarget(scrollTarget) {
        const scrollPx = this.container[this.horizontal ? "scrollWidth" : "scrollHeight"];
        const clientPx = this.container[this.horizontal ? "clientWidth" : "clientHeight"];
        return Math.max(0, Math.min(scrollTarget, scrollPx - clientPx));
    }
    getFocusedItemKey() {
        for (let element = document.activeElement; element != null; element = element.parentNode) {
            if (isHTMLElement(element)) {
                const key = element.dataset.key;
                if (key) {
                    return key;
                }
            }
        }
        return null;
    }
    updateIndexes() {
        if (this.pageSize === 0) {
            return;
        }
        const scrollTop = this.getClippedScrollTarget(this.container.scrollTop);
        if (this.container.scrollTop !== scrollTop) {
            this.container.scrollTop = scrollTop;
        }
        const scrollDelta = scrollTop - this.lastScrollTop;
        this.lastScrollTop = scrollTop;
        const previousItems = this.previousItems || this.items;
        this.previousItems = this.items;
        const itemsProbablyChanged = this.items.length !== previousItems.length;
        if (itemsProbablyChanged) {
            this.scrollDirection = 0;
        }
        else if (scrollDelta !== 0) {
            this.scrollDirection = Math.sign(scrollDelta);
        }
        // Get a set of the items we want to render, and a set of all the items
        // (do this in one method, so we only need to iterate through all the items once)
        const [renderKeys, existingKeys] = this.getRenderItemIndices(this.scrollDirection);
        const getType = (key) => {
            if (key != null) {
                const item = this.itemLookup.get(key);
                if (item) {
                    return this.itemType(item);
                }
            }
            return null;
        };
        //  We use keys since even with additions/removals, keys are stable
        let oldKeys = this.renderKeys;
        let newKeys = (0, getStableArray_1.default)(oldKeys, renderKeys, getType, getType);
        const elementLookup = this.getElementLookupByKey();
        newKeys.forEach((key) => {
            let element = elementLookup.get(key);
            if (element) {
                const deleted = !existingKeys.has(key);
                //  we HAVE to use display: none
                //  if we use visibility: hidden then the hidden elements
                //  still count for scroll size
                element.style.display = deleted ? "none" : "";
            }
        });
        // console.log({ existingKeys, renderKeys, newKeys })
        // we do a deep compare as we don't want to cause react re-rendering unless our rendered item renderKeys have changed.
        if (oldKeys.length !== newKeys.length ||
            JSON.stringify(oldKeys) !== JSON.stringify(newKeys)) {
            // const debug = true;
            // if (debug) {
            //     let counts: any = {};
            //     for (let key of newKeys) {
            //         let type = getType(key)!;
            //         counts[type] = (counts[type] || 0) + 1;
            //     }
            //     console.log(JSON.stringify(counts, null, 2));
            // }
            this.setRenderKeys(newKeys);
            // this.renderKeys won't change till react re-renders and then calls VirtualManager.update().
            return true;
        }
        return false;
    }
    onElementSized(element) {
        if (element === this.container) {
            this.containerPadding = Margin_1.default.fromCssPadding(getComputedStyle(element));
        }
        let { key, type } = element.dataset;
        if (key && type) {
            //  cache element size
            let properties = this.itemProperties[key];
            if (properties == null) {
                properties = this.getItemProperties(key, type);
            }
            // we REALLY only want to test these when absolutely necessary
            // since reading them forces immediate layout.
            const width = element.offsetWidth;
            const height = element.offsetHeight;
            if (width > 0 && height > 0) {
                let changed = width !== properties.width || height !== properties.height;
                properties.width = width;
                properties.height = height;
                //  cache ComputedCssProperties by className
                if (changed) {
                    let classProps = this.classProperties.get(type);
                    if (classProps == null || !classProps.valid) {
                        let css = getComputedStyle(element);
                        if (classProps == null) {
                            classProps = new ClassProperties(type);
                            this.classProperties.set(type, classProps);
                        }
                        classProps.setFromCss(css);
                    }
                    classProps.setItemSize(width, height);
                }
            }
        }
    }
    onresize(entries) {
        //  if entries contains the container then we mark computed css obsolete
        //  that way we can recompute while traversing elements.
        for (let entry of entries) {
            if (entry.target === this.container) {
                this.containerWidth = this.container.clientWidth;
                for (let classProps of this.classProperties.values()) {
                    classProps.invalidate();
                }
                // also reset placeholder size
                this.placeholder.style.width = "1px";
                this.placeholder.style.height = "1px";
                // TODO: Find currently visible items and use ScrollAnchor to restore them to same position after relayout.
                // also, clear all cached item sizes.
                for (let key in this.itemProperties) {
                    let itemProps = this.itemProperties[key];
                    itemProps.width = 0;
                    itemProps.height = 0;
                }
            }
            else {
                this.onElementSized(entry.target);
            }
        }
        this.updateAndLayout(true);
    }
    getItemTargetScrollPosition(anchor) {
        var _a, _b;
        let item = this.itemLookup.get(anchor.itemKey);
        if (item) {
            let itemPin = (_a = anchor.itemPin) !== null && _a !== void 0 ? _a : 0;
            let windowPin = (_b = anchor.windowPin) !== null && _b !== void 0 ? _b : 0;
            const bounds = this.getItemRect(item);
            if (bounds) {
                const clientSize = this.container[this.horizontal ? "clientWidth" : "clientHeight"];
                const targetScrollPosition = bounds[this.horizontal ? "x" : "y"] +
                    itemPin * bounds[this.horizontal ? "width" : "height"] -
                    windowPin * clientSize;
                return targetScrollPosition;
            }
        }
        return null;
    }
    scrollToItem(key, options) {
        var _a, _b, _c;
        // correct for legacy .behavior option
        if (options != null &&
            options.behavior != null &&
            options.duration == null) {
            options.duration =
                options.behavior === "smooth" ? 0.5 : 0.0;
        }
        const itemPin = (_a = options === null || options === void 0 ? void 0 : options.position) !== null && _a !== void 0 ? _a : 0;
        const windowPin = (_b = options === null || options === void 0 ? void 0 : options.position) !== null && _b !== void 0 ? _b : 0;
        const duration = (_c = options === null || options === void 0 ? void 0 : options.duration) !== null && _c !== void 0 ? _c : 0.5;
        const scrollAnchor = { itemKey: key, itemPin, windowPin, duration };
        const top = this.getItemTargetScrollPosition(scrollAnchor);
        if (top != null) {
            this.startScrollAnimation(scrollAnchor);
        }
    }
    startScrollAnimation(scrollAnchor) {
        this.scrollAnchor = scrollAnchor;
        this.scrollStartTime = Date.now();
        this.scrollStartPosition =
            this.container[this.horizontal ? "scrollLeft" : "scrollTop"];
        this.scrollDuration = scrollAnchor.duration;
        this.scrollAnimationCallback();
    }
    cancelScrollAnimation() {
        if (this.scrollAnimating != null) {
            if (typeof cancelAnimationFrame === "function") {
                cancelAnimationFrame(this.scrollAnimating);
            }
            delete this.scrollAnimating;
        }
        this.scrollAnchor = null;
    }
    scrollAnimationCallback() {
        let continueAnimating = false;
        delete this.scrollAnimating;
        if (this.scrollAnchor != null) {
            let targetPosition = this.getItemTargetScrollPosition(this.scrollAnchor);
            if (targetPosition != null) {
                let startDelta = targetPosition - this.scrollStartPosition;
                //  we are going to try to smooth scroll... BUT if we are scrolling a very long way
                //  we are first going to jump much closer to the target location.
                //  let's say, not more than 2000 pixels away, maximum.
                const maxDelta = 2000;
                if (Math.abs(startDelta) > maxDelta) {
                    startDelta = Math.sign(startDelta) * maxDelta;
                }
                let time = (Date.now() - this.scrollStartTime) / 1000;
                let elapsed = Math.min(time, this.scrollDuration);
                let alpha = this.scrollDuration > 0
                    ? elapsed / this.scrollDuration
                    : 1.0;
                let animatedTopNow = targetPosition - (1 - alpha) * startDelta;
                this.container[this.horizontal ? "scrollLeft" : "scrollTop"] =
                    this.getClippedScrollTarget(animatedTopNow);
                //  keep animating for twice the scroll duration.
                //  this provides extra time after scroll for resizing of
                //  nearby element positions to get fixed to the anchor point.
                continueAnimating =
                    time < this.scrollDuration + this.scrollWaitAfterFinish;
                // console.log({ animatedTopNow, targetTop, alpha, elapsed, time, startDelta, startTop: this.scrollStartPosition, continueAnimating });
            }
        }
        if (continueAnimating) {
            if (this.scrollAnimating == null &&
                typeof requestAnimationFrame === "function") {
                this.scrollAnimating = requestAnimationFrame(this.scrollAnimationCallback.bind(this));
            }
        }
        else {
            this.cancelScrollAnimation();
        }
    }
    getItemProperties(key, type) {
        let props = this.itemProperties[key];
        if (props == null) {
            props = this.itemProperties[key] = new ItemProperties(type);
        }
        let classProps = this.classProperties.get(type);
        if (classProps) {
            if (props.width === 0) {
                props.width = classProps.width;
            }
            if (props.height === 0) {
                props.height = classProps.height;
            }
        }
        return props;
    }
    static instance(container, props) {
        let manager = container[VirtualManager.symbol];
        if (manager == null && props != null) {
            manager = new VirtualManager(props);
        }
        return manager;
    }
    static update(props) {
        let manager = VirtualManager.instance(props.container, props);
        manager.update(props);
        return manager.renderKeys;
    }
}
exports.default = VirtualManager;
VirtualManager.symbol = Symbol("VirtualManager.symbol");
