"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var tslib_1 = require("tslib");
var React = require("react");
var PropTypes = require("prop-types");
var Utilities_1 = require("../../Utilities");
var RESIZE_DELAY = 16;
/**
 * Returns a simple object is able to store measurements with a given key.
 */
exports.getMeasurementCache = function () {
    var measurementsCache = {};
    return {
        /**
         * Checks if the provided data has a cacheKey. If it has a cacheKey and there is a
         * corresponding entry in the measurementsCache, then it will return that value.
         * Returns undefined otherwise.
         */
        getCachedMeasurement: function (data) {
            if (data && data.cacheKey && measurementsCache.hasOwnProperty(data.cacheKey)) {
                return measurementsCache[data.cacheKey];
            }
            return undefined;
        },
        /**
         * Should be called whenever there is a new measurement associated with a given data object.
         * If the data has a cacheKey, store that measurement in the measurementsCache.
         */
        addMeasurementToCache: function (data, measurement) {
            if (data.cacheKey) {
                measurementsCache[data.cacheKey] = measurement;
            }
        }
    };
};
/**
 * Returns a function that is able to compute the next state for the ResizeGroup given the current
 * state and any measurement updates.
 */
exports.getNextResizeGroupStateProvider = function (measurementCache) {
    if (measurementCache === void 0) { measurementCache = exports.getMeasurementCache(); }
    var _measurementCache = measurementCache;
    var _containerWidth;
    /**
     * Gets the width of the data rendered in a hidden div.
     * @param measuredData - The data corresponding to the measurement we wish to take.
     * @param getElementToMeasureWidth - A function that returns the measurement of the rendered data. Only called when the measurement
     * is not in the cache.
     */
    function _getMeasuredWidth(measuredData, getElementToMeasureWidth) {
        var cachedWidth = _measurementCache.getCachedMeasurement(measuredData);
        if (cachedWidth !== undefined) {
            return cachedWidth;
        }
        var measuredWidth = getElementToMeasureWidth();
        _measurementCache.addMeasurementToCache(measuredData, measuredWidth);
        return measuredWidth;
    }
    /**
     * Will get the next IResizeGroupState based on the current data while trying to shrink contents
     * to fit in the container.
     * @param data - The initial data point to start measuring.
     * @param onReduceData - Function that transforms the data into something that should render with less width.
     * @param getElementToMeasureWidth - A function that returns the measurement of the rendered data. Only called when the measurement
     * is not in the cache.
     */
    function _shrinkContentsUntilTheyFit(data, onReduceData, getElementToMeasureWidth) {
        var dataToMeasure = data;
        var measuredWidth = _getMeasuredWidth(data, getElementToMeasureWidth);
        while (measuredWidth > _containerWidth) {
            var nextMeasuredData = onReduceData(dataToMeasure);
            // We don't want to get stuck in an infinite render loop when there are no more
            // scaling steps, so implementations of onReduceData should return undefined when
            // there are no more scaling states to apply.
            if (nextMeasuredData === undefined) {
                return {
                    renderedData: dataToMeasure,
                    resizeDirection: undefined,
                    dataToMeasure: undefined
                };
            }
            measuredWidth = _measurementCache.getCachedMeasurement(nextMeasuredData);
            // If the measurement isn't in the cache, we need to rerender with some data in a hidden div
            if (measuredWidth === undefined) {
                return {
                    dataToMeasure: nextMeasuredData,
                    resizeDirection: 'shrink'
                };
            }
            dataToMeasure = nextMeasuredData;
        }
        return {
            renderedData: dataToMeasure,
            resizeDirection: undefined,
            dataToMeasure: undefined
        };
    }
    /**
     * This function should be called when the state changes in a manner that might allow for more content to fit
     * on the screen, such as the window width growing.
     * @param data - The initial data point to start measuring.
     * @param onGrowData - Function that transforms the data into something that may take up more space when rendering.
     * @param getElementToMeasureWidth - A function that returns the measurement of the rendered data. Only called when the measurement
     * is not in the cache.
     */
    function _growDataUntilItDoesNotFit(data, onGrowData, getElementToMeasureWidth) {
        var dataToMeasure = data;
        var measuredWidth = _getMeasuredWidth(data, getElementToMeasureWidth);
        while (measuredWidth < _containerWidth) {
            var nextMeasuredData = onGrowData(dataToMeasure);
            // We don't want to get stuck in an infinite render loop when there are no more
            // scaling steps, so implementations of onGrowData should return undefined when
            // there are no more scaling states to apply.
            if (nextMeasuredData === undefined) {
                return {
                    renderedData: dataToMeasure,
                    resizeDirection: undefined,
                    dataToMeasure: undefined
                };
            }
            measuredWidth = _measurementCache.getCachedMeasurement(nextMeasuredData);
            // If the measurement isn't in the cache, we need to rerender with some data in a hidden div
            if (measuredWidth === undefined) {
                return {
                    dataToMeasure: nextMeasuredData
                };
            }
            dataToMeasure = nextMeasuredData;
        }
        // Once the loop is done, we should now shrink until the contents fit.
        return {
            dataToMeasure: dataToMeasure,
            resizeDirection: 'shrink'
        };
    }
    /**
     * Handles an update to the container width. Should only be called when we knew the previous container width.
     * @param newWidth - The new width of the container.
     * @param fullWidthData - The initial data passed in as a prop to resizeGroup.
     * @param renderedData - The data that was rendered prior to the container size changing.
     * @param onGrowData - Set to true if the Resize group has an onGrowData function.
     */
    function _updateContainerWidth(newWidth, fullWidthData, renderedData, onGrowData) {
        var nextState;
        if (newWidth > _containerWidth) {
            if (onGrowData) {
                nextState = {
                    resizeDirection: 'grow',
                    dataToMeasure: onGrowData(renderedData)
                };
            }
            else {
                nextState = {
                    resizeDirection: 'shrink',
                    dataToMeasure: fullWidthData
                };
            }
        }
        else {
            nextState = {
                resizeDirection: 'shrink',
                dataToMeasure: renderedData
            };
        }
        _containerWidth = newWidth;
        return tslib_1.__assign({}, nextState, { measureContainer: false });
    }
    function getNextState(props, currentState, getElementToMeasureWidth, newContainerWidth) {
        // If there is no new container width or data to measure, there is no need for a new state update
        if (newContainerWidth === undefined && currentState.dataToMeasure === undefined) {
            return undefined;
        }
        if (newContainerWidth) {
            // If we know what the last container size was and we rendered data at that width, we can do an optimized render
            if (_containerWidth && currentState.renderedData && !currentState.dataToMeasure) {
                return tslib_1.__assign({}, currentState, _updateContainerWidth(newContainerWidth, props.data, currentState.renderedData, props.onGrowData));
            }
            // If we are just setting the container width for the first time, we can't do any optimizations
            _containerWidth = newContainerWidth;
        }
        var nextState = tslib_1.__assign({}, currentState, { measureContainer: false });
        if (currentState.dataToMeasure) {
            if (currentState.resizeDirection === 'grow' && props.onGrowData) {
                nextState = tslib_1.__assign({}, nextState, _growDataUntilItDoesNotFit(currentState.dataToMeasure, props.onGrowData, getElementToMeasureWidth));
            }
            else {
                nextState = tslib_1.__assign({}, nextState, _shrinkContentsUntilTheyFit(currentState.dataToMeasure, props.onReduceData, getElementToMeasureWidth));
            }
        }
        return nextState;
    }
    /** Function that determines if we need to render content for measurement based on the measurement cache contents. */
    function shouldRenderDataForMeasurement(dataToMeasure) {
        if (!dataToMeasure || _measurementCache.getCachedMeasurement(dataToMeasure) !== undefined) {
            return false;
        }
        return true;
    }
    return {
        getNextState: getNextState,
        shouldRenderDataForMeasurement: shouldRenderDataForMeasurement
    };
};
// Provides a context property that (if true) tells any child components that
// they are only being used for measurement purposes and will not be visible.
var MeasuredContext = Utilities_1.provideContext({
    isMeasured: PropTypes.bool
}, function () {
    return { isMeasured: true };
});
var getClassNames = Utilities_1.classNamesFunction();
// Styles for the hidden div used for measurement
var hiddenDivStyles = { position: 'fixed', visibility: 'hidden' };
var ResizeGroupBase = /** @class */ (function (_super) {
    tslib_1.__extends(ResizeGroupBase, _super);
    function ResizeGroupBase(props) {
        var _this = _super.call(this, props) || this;
        _this._nextResizeGroupStateProvider = exports.getNextResizeGroupStateProvider();
        // The root div which is the container inside of which we are trying to fit content.
        _this._root = Utilities_1.createRef();
        // A div that can be used for the initial measurement so that we can avoid mounting a second instance
        // of the component being measured for the initial render.
        _this._initialHiddenDiv = Utilities_1.createRef();
        // A hidden div that is used for mounting a new instance of the component for measurement in a hidden
        // div without unmounting the currently visible content.
        _this._updateHiddenDiv = Utilities_1.createRef();
        // Tracks if any content has been rendered to the user. This enables us to do some performance optimizations
        // for the initial render.
        _this._hasRenderedContent = false;
        _this.state = {
            dataToMeasure: tslib_1.__assign({}, _this.props.data),
            resizeDirection: 'grow',
            measureContainer: true
        };
        return _this;
    }
    ResizeGroupBase.prototype.render = function () {
        var _a = this.props, onRenderData = _a.onRenderData, className = _a.className, styles = _a.styles, theme = _a.theme;
        var _b = this.state, dataToMeasure = _b.dataToMeasure, renderedData = _b.renderedData;
        var divProps = Utilities_1.getNativeProps(this.props, Utilities_1.divProperties, ['data']);
        var classNames = getClassNames(styles, { theme: theme, className: className });
        var dataNeedsMeasuring = this._nextResizeGroupStateProvider.shouldRenderDataForMeasurement(dataToMeasure);
        var isInitialMeasure = !this._hasRenderedContent && dataNeedsMeasuring;
        // We only ever render the final content to the user. All measurements are done in a hidden div.
        // For the initial render, we want this to be as fast as possible, so we need to make sure that we only mount one version of the
        // component for measurement and the final render. For renders that update what is on screen, we want to make sure that
        // there are no jarring effects such as the screen flashing as we apply scaling steps for meassurement. In the update case,
        // we mount a second version of the component just for measurement purposes and leave the rendered content untouched until we know the
        // next state sto show to the user.
        return (React.createElement("div", tslib_1.__assign({}, divProps, { className: classNames.root, ref: this._root }),
            dataNeedsMeasuring &&
                !isInitialMeasure && (React.createElement("div", { style: hiddenDivStyles, ref: this._updateHiddenDiv },
                React.createElement(MeasuredContext, null, onRenderData(dataToMeasure)))),
            React.createElement("div", { ref: this._initialHiddenDiv, style: isInitialMeasure ? hiddenDivStyles : undefined }, isInitialMeasure ? onRenderData(dataToMeasure) : renderedData && onRenderData(renderedData))));
    };
    ResizeGroupBase.prototype.componentDidMount = function () {
        this._afterComponentRendered();
        this._events.on(window, 'resize', this._async.debounce(this._onResize, RESIZE_DELAY, { leading: true }));
    };
    ResizeGroupBase.prototype.componentWillReceiveProps = function (nextProps) {
        this.setState({
            dataToMeasure: tslib_1.__assign({}, nextProps.data),
            resizeDirection: 'grow',
            measureContainer: true // Receiving new props means the parent might rerender and the root width might change
        });
    };
    ResizeGroupBase.prototype.componentDidUpdate = function (prevProps) {
        if (this.state.renderedData) {
            this._hasRenderedContent = true;
            if (this.props.dataDidRender) {
                this.props.dataDidRender(this.state.renderedData);
            }
        }
        this._afterComponentRendered();
    };
    ResizeGroupBase.prototype.remeasure = function () {
        if (this._root.current) {
            this.setState({ measureContainer: true });
        }
    };
    ResizeGroupBase.prototype._afterComponentRendered = function () {
        var _this = this;
        this._async.requestAnimationFrame(function () {
            var containerWidth = undefined;
            if (_this.state.measureContainer && _this._root.current) {
                containerWidth = _this._root.current.getBoundingClientRect().width;
            }
            var nextState = _this._nextResizeGroupStateProvider.getNextState(_this.props, _this.state, function () {
                var refToMeasure = !_this._hasRenderedContent ? _this._initialHiddenDiv : _this._updateHiddenDiv;
                return refToMeasure.current ? refToMeasure.current.scrollWidth : 0;
            }, containerWidth);
            if (nextState) {
                _this.setState(nextState);
            }
        });
    };
    ResizeGroupBase.prototype._onResize = function () {
        if (this._root.current) {
            this.setState({ measureContainer: true });
        }
    };
    return ResizeGroupBase;
}(Utilities_1.BaseComponent));
exports.ResizeGroupBase = ResizeGroupBase;
//# sourceMappingURL=ResizeGroup.base.js.map