var __extends = (this && this.__extends) || function (d, b) {
    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
    function __() { this.constructor = d; }
    d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
define(["require", "exports", 'react', '../../common/BaseComponent', '../../utilities/css', '../../utilities/object'], function (require, exports, React, BaseComponent_1, css_1, object_1) {
    "use strict";
    var MIN_SCROLL_UPDATE_DELAY = 50;
    var MAX_SCROLL_UPDATE_DELAY = 200;
    var DEFAULT_ITEMS_PER_PAGE = 10;
    var DEFAULT_PAGE_HEIGHT = 30;
    var DEFAULT_RENDERED_WINDOWS_BEHIND = 2;
    var DEFAULT_RENDERED_WINDOWS_AHEAD = 2;
    var _instance = 0;
    var EMPTY_RECT = {
        top: -1,
        bottom: -1,
        left: -1,
        right: -1,
        width: 0,
        height: 0
    };
    // Naming expensive measures so that they're named in profiles.
    var _measurePageRect = function (element) { return element.getBoundingClientRect(); };
    var _measureSurfaceRect = _measurePageRect;
    var _measureScrollRect = _measurePageRect;
    /**
     * The List renders virtualized pages of items. Each page's item count is determined by the getItemCountForPage callback if
     * provided by the caller, or 10 as default. Each page's height is determined by the getPageHeight callback if provided by
     * the caller, or by cached measurements if available, or by a running average, or a default fallback.
     *
     * The algorithm for rendering pages works like this:
     *
     * 1. Predict visible pages based on "current measure data" (page heights, surface position, visible window)
     * 2. If changes are necessary, apply changes (add/remove pages)
     * 3. For pages that are added, measure the page heights if we need to using getBoundingClientRect
     * 4. If measurements don't match predictions, update measure data and goto step 1 asynchronously
     *
     * Measuring too frequently can pull performance down significantly. To compensate, we cache measured values so that
     * we can avoid re-measuring during operations that should not alter heights, like scrolling.
     *
     * However, certain operations can make measure data stale. For example, resizing the list, or passing in new props,
     * or forcing an update change cause pages to shrink/grow. When these operations occur, we increment a measureVersion
     * number, which we associate with cached measurements and use to determine if a remeasure should occur.
     */
    var List = (function (_super) {
        __extends(List, _super);
        function List(props) {
            _super.call(this, props);
            this.state = {
                pages: []
            };
            this._id = _instance++;
            this._estimatedPageHeight = 0;
            this._totalEstimates = 0;
            // Track the measure version for everything.
            this._measureVersion = 0;
            // Ensure that scrolls are lazy updated.
            this._onAsyncScroll = this._async.debounce(this._onAsyncScroll, MIN_SCROLL_UPDATE_DELAY, {
                leading: false,
                maxWait: MAX_SCROLL_UPDATE_DELAY
            });
            this._cachedPageHeights = {};
            this._estimatedPageHeight = 0;
            this._focusedIndex = -1;
            this._scrollingToIndex = -1;
        }
        List.prototype.componentDidMount = function () {
            var scrollableElements = this._getScrollableElements();
            this._scrollableElement = scrollableElements[0];
            this._updatePages();
            this._measureVersion++;
            this._events.on(window, 'resize', this._onResize);
            this._events.on(this.refs.root, 'focus', this._onFocus, true);
            this._registerScrollListener();
        };
        List.prototype._registerScrollListener = function () {
            var scrollElement = this.refs.root;
            while (scrollElement) {
                if (scrollElement.getAttribute('data-is-scrollable') === 'true') {
                    this._events.on(scrollElement, 'scroll', this._onAsyncScroll);
                    break;
                }
                if (scrollElement === document.body) {
                    this._events.on(window, 'scroll', this._onAsyncScroll, true);
                    break;
                }
                scrollElement = scrollElement.parentNode;
            }
        };
        List.prototype.componentWillReceiveProps = function (newProps) {
            if (newProps.items !== this.props.items ||
                newProps.renderCount !== this.props.renderCount ||
                newProps.startIndex !== this.props.startIndex) {
                this._updatePages(newProps);
            }
        };
        List.prototype.shouldComponentUpdate = function (newProps, newState) {
            var oldPages = this.state.pages;
            var newPages = newState.pages, measureVersion = newState.measureVersion;
            var shouldComponentUpdate = false;
            if (this._measureVersion === measureVersion &&
                newProps.renderedWindowsAhead === this.props.renderedWindowsAhead &&
                newProps.renderedWindowsBehind === this.props.renderedWindowsBehind &&
                newProps.items === this.props.items &&
                oldPages.length === newPages.length) {
                for (var i = 0; i < oldPages.length; i++) {
                    var oldPage = oldPages[i];
                    var newPage = newPages[i];
                    if ((oldPage.key !== newPage.key ||
                        oldPage.itemCount !== newPage.itemCount)) {
                        shouldComponentUpdate = true;
                        break;
                    }
                }
            }
            else {
                shouldComponentUpdate = true;
            }
            return shouldComponentUpdate;
        };
        List.prototype.forceUpdate = function () {
            // Ensure that when the list is force updated we update the pages first before render.
            this._updateVisibleRect();
            this._updatePages();
            this._measureVersion++;
            _super.prototype.forceUpdate.call(this);
        };
        List.prototype.scrollTo = function (index) {
            var isIndexRendered = this._isIndexRendered(index);
            var didScroll = false;
            if (!isIndexRendered) {
                // Identify where the page would be.
                var pageTop = 0;
                var itemCount = 0;
                var items = this.props.items;
                var surfaceRect = _measureSurfaceRect(this.refs.surface);
                for (var itemIndex = 0; items && itemIndex < items.length; itemIndex += itemCount) {
                    itemCount = this._getItemCountForPage(itemIndex, surfaceRect);
                    if (itemIndex <= index && (itemIndex + itemCount) > index) {
                        var allScrollables = this._getScrollableElements();
                        var scrollElement = allScrollables[0] || document.body;
                        for (var _i = 0, allScrollables_1 = allScrollables; _i < allScrollables_1.length; _i++) {
                            var scrollable = allScrollables_1[_i];
                            if (scrollable.clientHeight < scrollable.scrollHeight) {
                                scrollElement = scrollable;
                                break;
                            }
                        }
                        var scrollRect = _measureScrollRect(scrollElement);
                        didScroll = true;
                        scrollElement.scrollTop = surfaceRect.top - scrollRect.top + scrollElement.scrollTop + pageTop;
                        break;
                    }
                    pageTop += this._getPageHeight(itemIndex, itemCount, surfaceRect);
                }
            }
            return didScroll;
        };
        List.prototype.render = function () {
            var className = this.props.className;
            var pages = this.state.pages;
            var pageElements = [];
            for (var i = 0; i < pages.length; i++) {
                pageElements.push(this._renderPage(pages[i]));
            }
            return (React.createElement("div", {ref: 'root', className: css_1.css('ms-List', className)}, React.createElement("div", {ref: 'surface', className: 'ms-List-surface'}, pageElements)));
        };
        List.prototype._renderPage = function (page) {
            var onRenderCell = this.props.onRenderCell;
            var cells = [];
            for (var i = 0; page.items && i < page.items.length; i++) {
                var item = page.items[i];
                var itemKey = (item ? item.key : null) || (page.startIndex + i);
                cells.push(React.createElement("div", {className: 'ms-List-cell', key: itemKey, "data-list-index": i + page.startIndex, "data-automationid": 'ListCell'}, onRenderCell(item, page.startIndex + i)));
            }
            return (React.createElement("div", {className: 'ms-List-page', key: page.key, ref: page.key, style: page.style}, cells));
        };
        /** Track the last item index focused so that we ensure we keep it rendered. */
        List.prototype._onFocus = function (ev) {
            var target = ev.target;
            while (target !== this.refs.surface) {
                var indexString = target.getAttribute('data-list-index');
                if (indexString) {
                    this._focusedIndex = Number(indexString);
                    break;
                }
                target = target.parentElement;
            }
        };
        List.prototype._isIndexRendered = function (index) {
            var pages = this.state.pages;
            var isIndexRendered = false;
            for (var _i = 0, pages_1 = pages; _i < pages_1.length; _i++) {
                var page = pages_1[_i];
                if (page.items && page.startIndex <= index && (page.startIndex + page.itemCount) > index) {
                    isIndexRendered = true;
                    break;
                }
            }
            return isIndexRendered;
        };
        List.prototype._onAsyncScroll = function () {
            this._updateVisibleRect();
            // Only update pages when the visible rect falls outside of the materialized rect.
            if (!this._materializedRect || !_isContainedWithin(this._visibleRect, this._materializedRect)) {
                this._updatePages();
            }
        };
        List.prototype._onResize = function () {
            this.forceUpdate();
        };
        List.prototype._updatePages = function (props) {
            // console.log('updating pages');
            var _this = this;
            var _a = (props || this.props), items = _a.items, startIndex = _a.startIndex, renderCount = _a.renderCount;
            renderCount = renderCount || (items ? items.length - startIndex : 0);
            if (!this._visibleRect) {
                this._updateVisibleRect(props);
            }
            var newListState = this._buildPages(items, startIndex, renderCount);
            var oldListPages = this.state.pages;
            this.setState(newListState, function () {
                // If measured version is invalid since we've updated the DOM
                var heightsChanged = _this._updatePageMeasurements(oldListPages, newListState.pages);
                if (heightsChanged) {
                    // console.log('re-enqueuing update', 'heightsChanged = ' + heightsChanged);
                    _this._materializedRect = null;
                    _this._onAsyncScroll();
                }
            });
        };
        List.prototype._updatePageMeasurements = function (oldPages, newPages) {
            var renderedIndexes = {};
            var heightChanged = false;
            for (var i = 0; i < oldPages.length; i++) {
                var page = oldPages[i];
                if (page.items) {
                    renderedIndexes[page.startIndex] = page;
                }
            }
            for (var i = 0; i < newPages.length; i++) {
                var page = newPages[i];
                if (page.items) {
                    heightChanged = this._measurePage(page) || heightChanged;
                    if (!renderedIndexes[page.startIndex]) {
                        this._onPageAdded(page);
                    }
                    else {
                        delete renderedIndexes[page.startIndex];
                    }
                }
            }
            for (var index in renderedIndexes) {
                if (renderedIndexes.hasOwnProperty(index)) {
                    this._onPageRemoved(renderedIndexes[index]);
                }
            }
            return heightChanged;
        };
        /**
         * Given a page, measure its dimensions, update cache.
         * @returns True if the height has changed.
         */
        List.prototype._measurePage = function (page) {
            var hasChangedHeight = false;
            var pageElement = this.refs[page.key];
            var cachedHeight = this._cachedPageHeights[page.startIndex];
            // // console.log('   * measure attempt', page.startIndex, cachedHeight);
            if (pageElement && (!cachedHeight || cachedHeight.measureVersion !== this._measureVersion)) {
                var newClientRect = _measurePageRect(pageElement);
                hasChangedHeight = page.height !== newClientRect.height;
                // console.warn(' *** expensive page measure', page.startIndex, page.height, newClientRect.height);
                page.height = newClientRect.height;
                this._cachedPageHeights[page.startIndex] = {
                    height: newClientRect.height,
                    measureVersion: this._measureVersion
                };
                this._estimatedPageHeight = Math.round(((this._estimatedPageHeight * this._totalEstimates) + newClientRect.height) /
                    (this._totalEstimates + 1));
                this._totalEstimates++;
            }
            return hasChangedHeight;
        };
        /** Called when a page has been added to the DOM. */
        List.prototype._onPageAdded = function (page) {
            var onPageAdded = this.props.onPageAdded;
            // console.log('page added', page.startIndex, this.state.pages.map(page=>page.key).join(', '));
            if (onPageAdded) {
                onPageAdded(page);
            }
        };
        /** Called when a page has been removed from the DOM. */
        List.prototype._onPageRemoved = function (page) {
            var onPageRemoved = this.props.onPageRemoved;
            // console.log('  --- page removed', page.startIndex, this.state.pages.map(page=>page.key).join(', '));
            if (onPageRemoved) {
                onPageRemoved(page);
            }
        };
        /** Build up the pages that should be rendered. */
        List.prototype._buildPages = function (items, startIndex, renderCount) {
            var visibleRect = this._visibleRect;
            //  let surfaceRect = this._surfaceRect;
            var materializedRect = object_1.assign({}, EMPTY_RECT);
            var itemsPerPage = 1;
            var pages = [];
            var pageTop = 0;
            var currentSpacer = null;
            var focusedIndex = this._focusedIndex;
            var endIndex = startIndex + renderCount;
            // First render is very important to track; when we render cells, we have no idea of estimated page height.
            // So we should default to rendering only the first page so that we can get information.
            // However if the user provides a measure function, let's just assume they know the right heights.
            var isFirstRender = this._estimatedPageHeight === 0 && !this.props.getPageHeight;
            for (var itemIndex = startIndex; itemIndex < endIndex; itemIndex += itemsPerPage) {
                itemsPerPage = this._getItemCountForPage(itemIndex, visibleRect);
                var pageHeight = this._getPageHeight(itemIndex, itemsPerPage, visibleRect);
                var pageBottom = pageTop + pageHeight - 1;
                var isPageVisible = !isFirstRender && pageBottom >= visibleRect.top && pageTop <= visibleRect.bottom;
                var isPageFocused = focusedIndex >= itemIndex && focusedIndex < (itemIndex + itemsPerPage);
                var isFirstPage = itemIndex === startIndex;
                // Only render whats visible, focused, or first page.
                if (isPageVisible || isPageFocused || isFirstPage) {
                    if (currentSpacer) {
                        pages.push(currentSpacer);
                        currentSpacer = null;
                    }
                    var itemsInPage = Math.min(itemsPerPage, endIndex - itemIndex);
                    var newPage = this._createPage(null, items.slice(itemIndex, itemIndex + itemsInPage), itemIndex);
                    newPage.top = pageTop;
                    newPage.height = pageHeight;
                    pages.push(newPage);
                    if (isPageVisible) {
                        _mergeRect(materializedRect, {
                            top: pageTop,
                            bottom: pageBottom,
                            height: pageHeight,
                            left: visibleRect.left,
                            right: visibleRect.right,
                            width: visibleRect.width
                        });
                    }
                }
                else {
                    if (!currentSpacer) {
                        currentSpacer = this._createPage('spacer-' + itemIndex, null, itemIndex, 0);
                    }
                    currentSpacer.style.height = (currentSpacer.style.height || 0) + (pageBottom - pageTop) + 1;
                    currentSpacer.itemCount += itemsPerPage;
                }
                pageTop += (pageBottom - pageTop + 1);
            }
            if (currentSpacer) {
                currentSpacer.key = 'spacer-end';
                pages.push(currentSpacer);
            }
            this._materializedRect = materializedRect;
            return {
                pages: pages,
                measureVersion: this._measureVersion
            };
        };
        /**
         * Get the pixel height of a give page. Will use the props getPageHeight first, and if not provided, fallback to
         * cached height, or estimated page height, or default page height.
         */
        List.prototype._getPageHeight = function (itemIndex, itemsPerPage, visibleRect) {
            if (this.props.getPageHeight) {
                return this.props.getPageHeight(itemIndex, visibleRect);
            }
            else {
                var cachedHeight = (this._cachedPageHeights[itemIndex]);
                return cachedHeight ? cachedHeight.height : (this._estimatedPageHeight || DEFAULT_PAGE_HEIGHT);
            }
        };
        List.prototype._getItemCountForPage = function (itemIndex, visibileRect) {
            return this.props.getItemCountForPage ? this.props.getItemCountForPage(itemIndex, visibileRect) : DEFAULT_ITEMS_PER_PAGE;
        };
        List.prototype._getScrollableElements = function () {
            var el = this.refs.root;
            var elements = [];
            while (el && el !== document.body) {
                var style = getComputedStyle(el);
                if (style.overflowY === 'auto' || style.overflowY === 'scroll') {
                    elements.push(el);
                }
                el = el.parentElement;
            }
            return elements;
        };
        List.prototype._createPage = function (pageKey, items, startIndex, count, style) {
            pageKey = pageKey || ('page-' + startIndex);
            // Fill undefined cells because array.map will ignore undefined cells.
            if (items) {
                for (var i = 0; i < items.length; i++) {
                    items[i] = items[i] || null;
                }
            }
            return {
                key: pageKey,
                startIndex: startIndex === undefined ? -1 : startIndex,
                itemCount: (count === undefined) ? (items ? items.length : 0) : count,
                items: items,
                style: style || {},
                top: 0,
                height: 0
            };
        };
        /** Calculate the visible rect within the list where top: 0 and left: 0 is the top/left of the list. */
        List.prototype._updateVisibleRect = function (props) {
            var _a = (props || this.props), renderedWindowsAhead = _a.renderedWindowsAhead, renderedWindowsBehind = _a.renderedWindowsBehind;
            var containerRect = {
                top: -window.innerHeight * renderedWindowsBehind,
                left: 0,
                bottom: window.innerHeight * (renderedWindowsAhead + 1),
                right: window.innerWidth,
                width: window.innerWidth,
                height: window.innerHeight * (renderedWindowsAhead + renderedWindowsBehind + 1)
            };
            // WARNING: EXPENSIVE CALL!
            // console.warn('expensive measure of surface');
            var surfaceRect = this._surfaceRect = _measureSurfaceRect(this.refs.surface);
            // If the surface is above the container top or below the container bottom, return empty rect.
            if (surfaceRect.bottom < containerRect.top ||
                surfaceRect.top > containerRect.bottom) {
                return EMPTY_RECT;
            }
            // Clamp the dimensions so that we constrain visible range to the real range.
            var visibleTop = Math.max(0, containerRect.top - surfaceRect.top);
            var visibleLeft = Math.max(0, containerRect.left - surfaceRect.left);
            this._visibleRect = {
                top: visibleTop,
                left: visibleLeft,
                bottom: visibleTop + containerRect.height - 1,
                right: visibleLeft + surfaceRect.width - 1,
                width: surfaceRect.width,
                height: containerRect.height
            };
        };
        List.defaultProps = {
            startIndex: 0,
            onRenderCell: function (item, index, containsFocus) { return (React.createElement("div", null, (item && item.name) || '')); },
            renderedWindowsAhead: DEFAULT_RENDERED_WINDOWS_AHEAD,
            renderedWindowsBehind: DEFAULT_RENDERED_WINDOWS_BEHIND
        };
        return List;
    }(BaseComponent_1.BaseComponent));
    exports.List = List;
    /*
    function _areEqualSize(rect1: ClientRect, rect2: ClientRect) {
      return !!(rect1 && rect2 && rect1.width === rect2.width && rect1.height === rect2.height);
    }
    */
    function _isContainedWithin(innerRect, outerRect) {
        return (innerRect.top >= outerRect.top &&
            innerRect.left >= outerRect.left &&
            innerRect.bottom <= outerRect.bottom &&
            innerRect.right <= outerRect.right);
    }
    function _mergeRect(targetRect, newRect) {
        targetRect.top = (newRect.top < targetRect.top || targetRect.top === -1) ? newRect.top : targetRect.top;
        targetRect.left = (newRect.left < targetRect.left || targetRect.left === -1) ? newRect.left : targetRect.left;
        targetRect.bottom = (newRect.bottom > targetRect.bottom || targetRect.bottom === -1) ? newRect.bottom : targetRect.bottom;
        targetRect.right = (newRect.right > targetRect.right || targetRect.right === -1) ? newRect.right : targetRect.right;
        targetRect.width = targetRect.right - targetRect.left + 1;
        targetRect.height = targetRect.bottom - targetRect.top + 1;
        return targetRect;
    }
});
