'use strict';

Object.defineProperty(exports, "__esModule", {
    value: true
});

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _react = require('react');

var _react2 = _interopRequireDefault(_react);

var _plank = require('./plank');

var _plank2 = _interopRequireDefault(_plank);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

/**
 * Container for planks. Handles the sizing, positioning, and responsiveness for all individual planks.
 *
 * @param Object props.options          
 */

var Planks = function (_React$Component) {
    _inherits(Planks, _React$Component);

    function Planks(props) {
        _classCallCheck(this, Planks);

        var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(Planks).call(this, props));

        var breakpointKey = _this.getCurrentBreakpointKey();
        // let planksRendered = {};

        /*planksRendered[breakpointKey] = {
            hiddenPlanksRendered: false,
            heightsSet: false
        };*/

        _this.validateConfigOptions();

        _this.state = {
            plankWidths: {}, // Caches all the plank widths per breakpoint
            plankHeights: {}, // Caches and used to determine when to unhide
            plankPosition: {}, // The calculated positions for each plank
            containerWidth: 0, // Used to determine widths of planks based on current screen breakpoint
            containerHeights: {}, // Must set height because container element is absolute
            breakpointKey: breakpointKey, // Current responsive breakpoint
            allPlanksRendered: {} // Caches when to unhide by breakpoint
        };
        return _this;
    }

    /**
     * When this component first mounts, no data has been loaded yet. Therefore, no dimensions of the children
     * components can be known. The objective here is to simply get the widths of the child planks so that they can
     * be quickly rendered.
     */


    _createClass(Planks, [{
        key: 'componentDidMount',
        value: function componentDidMount() {
            // console.log('[PLANKS CONTAINER] COMPONENT DID MOUNT...');
            var containerWidth = this.getPlankContainerWidth();
            var plankWidths = this.getSinglePlankWidths(containerWidth);
            var planksRendered = this.state.allPlanksRendered;

            // console.log('[PLANKS CONTAINER] INITIAL CONTAINER WIDTH: ' + containerWidth);
            // console.log('[PLANKS CONTAINER] INITIAL PLANK WIDTH: ' + plankWidths[this.state.breakpointKey]);

            // Event listener to calculate dimensions and positions on re-size. Caches results for faster subsequent
            // rendering.
            window.addEventListener('resize', this.handleResize.bind(this));

            this.setState({
                plankWidths: plankWidths,
                containerWidth: containerWidth,
                allPlanksRendered: planksRendered
            });

            // Force update so that all children's state gets synced with the new width.
            // console.log('[PLANKS CONTAINER] FORCE UPDATE');
            this.forceUpdate();
        }

        /**
         * There are two phases of rendering individual child planks:
         *      1. When the width is first set and they are rendered hidden and without positioning.
         *      2. When all their heights have been received and positions have been calculated.
         */

    }, {
        key: 'shouldComponentUpdate',
        value: function shouldComponentUpdate() {
            // console.log('[PLANKS CONTAINER] SHOULD COMPONENT UPDATE?');
            if (!this.state.allPlanksRendered[this.state.containerWidth] || !this.state.allPlanksRendered[this.state.containerWidth].hiddenPlanksRendered) {
                // console.log('[PLANKS CONTAINER] YES, INVISIBLE PLANKS HAVE NOT BEEN RENDERED YET');
                return true;
            }
            if (!this.state.allPlanksRendered[this.state.containerWidth] || !this.state.allPlanksRendered[this.state.containerWidth].heightsSet) {
                // console.log('[PLANKS CONTAINER] NO, ALL HEIGHTS HAVE NOT BEEN RECEIVED. BYPASSING UPDATE');
                return false;
            }
            return true;
        }
    }, {
        key: 'componentWillUnmount',
        value: function componentWillUnmount() {}

        /**
         * TODO -- move this into it's own module that you include outside of Plank's scope.
         * 
         * Checks to make sure the options passed in as this.props of this component are valid. Issue warnings and use
         * defaults if they are not valid.
         */

    }, {
        key: 'validateConfigOptions',
        value: function validateConfigOptions() {}
        // TODO -- check to make sure keys are screen widths in numbers. issue a warning if they aren't. they don't
        // have to be in order.  


        /**
         * @return Number   the current width of the referenced container element.
         */

    }, {
        key: 'getPlankContainerWidth',
        value: function getPlankContainerWidth() {
            return this._planksContainer.offsetWidth;
        }

        /**
         * Returns the current breakpoint key. Use this only when absolutely necessary such as on initial page load and 
         * when a responsive screen re-size has occured.
         */

    }, {
        key: 'getCurrentBreakpointKey',
        value: function getCurrentBreakpointKey() {
            var screenWidth = document.body.offsetWidth;
            var breakpointKey = void 0;
            var optionKeys = [];

            // Since for...in loops do not guarantee order, extract the keys
            // into an array that can then be sorted in ascending order. If the current screen size is larger than any
            // defined key, the column count will be associated to the largest key.
            for (var key in this.props.options.breakpoints) {
                optionKeys.push(key);
            }

            optionKeys.sort(function (a, b) {
                return +a - +b;
            });
            breakpointKey = optionKeys[optionKeys.length - 1];

            // If optionKeys is length 0, then we will use the largest key as assigned by the expression above,
            // otherwise take the 0 index value as that by definition is the nearest breakpoint to the current screen size.
            optionKeys = optionKeys.filter(function (item) {
                return screenWidth < item;
            });
            if (optionKeys.length > 0) {
                breakpointKey = optionKeys[0];
            }

            return breakpointKey;
        }

        /**
         * Checks if width has previously been calculated and returns the cached value. Otherwise this calculates the width
         * taking into account the current breakpoint, number of columns associated to the breakpoint, and horizontal
         * padding.
         *
         * @param   Number      containerWidth      the current container width
         * @param   String      nextBreakpointKey   hacky optional param to set the next value when a resize occurs
         * @return  Object      plankWidths         cache of breakpoint keys to plank widths
         */

    }, {
        key: 'getSinglePlankWidths',
        value: function getSinglePlankWidths(containerWidth, nextBreakpointKey) {
            var plankWidths = this.state.plankWidths;
            var breakpointKey = nextBreakpointKey || this.state.breakpointKey;
            var numColumns = void 0;
            var singlePlankWidth = void 0;

            // Check for cached value
            if (plankWidths[containerWidth] && plankWidths[containerWidth].length > 0) {
                return plankWidths[containerWidth];
            }

            // To get the width of a single plank, the horizontal padding must be accounted for.
            numColumns = this.props.options.breakpoints[breakpointKey + ''];

            // 16px === 1rem
            var hPadding = this.props.options.horizontalPadding;
            var unitRatio = this.props.options.unitType === 'rem' ? 16 : 1;
            singlePlankWidth = (containerWidth - (numColumns - 1) * hPadding * unitRatio) / numColumns / unitRatio;

            // Cache the current value against the breakpoint key
            plankWidths[containerWidth] = singlePlankWidth;

            return plankWidths;
        }

        /**
         * For optimization we need a flag to determine when all the hidden planks have been rendered. This allows us to
         * skip re-rendering until all the hidden planks have been rendered.
         */

    }, {
        key: 'handleAllHiddenPlanksRendered',
        value: function handleAllHiddenPlanksRendered() {
            var planksRendered = this.state.allPlanksRendered;

            // console.log('[PLANKS CONTAINER] LAST HIDDEN PLANK RENDERED');
            if (!planksRendered[this.state.containerWidth]) {
                planksRendered[this.state.containerWidth] = {};
            }
            planksRendered[this.state.containerWidth].hiddenPlanksRendered = true;
            this.setState({ allPlanksRendered: planksRendered });
            return true;
        }

        /**
         * Screen resizes must handle the re-ordering of cards. If this ordering has already occurred for the existing
         * data set, use a locally cached version of that particular ordering.
         * 
         * TODO -- handle orientationchange event for mobile
         */

    }, {
        key: 'handleResize',
        value: function handleResize() {
            var containerWidth = this.getPlankContainerWidth();

            // Screen widths alone do not determine when a responsive change has occured. Responsiveness is a set of fixed
            // container widths that exist over ranges of screen sizes. Handling resize is more optimal when
            // calculations only occur when container width changes. Additionally, setting the current
            // breakpointKey should only occur here and in the constructor.
            if (this.state.containerWidth !== containerWidth) {
                // console.log('[PLANKS CONTAINER] RESIZE DETECTED...');
                var breakpointKey = this.getCurrentBreakpointKey();
                // console.log('breakpointKey: ', breakpointKey);
                // console.log('containerWidth: ', containerWidth);
                var planksRendered = this.state.allPlanksRendered;
                var plankWidths = this.getSinglePlankWidths(containerWidth, breakpointKey);

                // console.log('planksWidths', plankWidths);
                // console.log('planksRendered');
                console.table(planksRendered);
                if (planksRendered[breakpointKey] !== undefined) {
                    this.setState({
                        breakpointKey: breakpointKey,
                        containerWidth: containerWidth
                    });
                    return; // Cache exists. Do nothing.
                }

                planksRendered[breakpointKey] = {
                    hiddenPlanksRendered: false,
                    heightsSet: false
                };

                this.setState({
                    breakpointKey: breakpointKey,
                    plankWidths: plankWidths,
                    containerWidth: containerWidth,
                    allPlanksRendered: planksRendered
                });
            }
        }

        /**
         * A callback that receives the hidden child element's height. When all heights are received, we can then calculate
         * their correct positioning and set their visibility. This callback should execute one time per element render. 
         * This means that responsiveness should trigger a new render. However, we have to also account for cached values.
         */

    }, {
        key: 'receiveChildHeight',
        value: function receiveChildHeight(key, childHeight) {
            var plankHeights = this.state.plankHeights;
            var planksRendered = this.state.allPlanksRendered;
            var renderAllPlanks = void 0;

            // console.log('[PLANKS CONTAINER] RECEIVING CHILD HEIGHT:');
            // console.log('[PLANKS CONTAINER] KEY: ' + key, 'HEIGHT: ' + childHeight);

            if (plankHeights[this.state.containerWidth] === undefined) {
                plankHeights[this.state.containerWidth] = {};
            }

            // It is very normal for updated heights to come back to us. This could indicated another image loaded, an
            // image failing to load, or even new content. In this case we must force update if all the other heights
            // have already been received.
            var heightExists = plankHeights[this.state.containerWidth][key] || false;

            plankHeights[this.state.containerWidth][key] = childHeight;

            this.setState({ plankHeights: plankHeights });

            if (heightExists && this.state.allPlanksRendered[this.state.containerWidth].heightsSet) {
                // console.log('[PLANKS CONTAINER] UPDATING EXISTING HEIGHT. FORCE UPDATE');
                this.setPlankPositioning();
                this.forceUpdate();
            }

            // Render planks when all the heights have been received.
            renderAllPlanks = Object.keys(plankHeights[this.state.containerWidth]).length === this.props.children.length;

            // Initialize the rendering flags that corresponds to the two phases of child plank rendering.
            if (!planksRendered[this.state.containerWidth]) {
                planksRendered[this.state.containerWidth] = {
                    hiddenPlanksRendered: false,
                    heightsSet: false
                };
            }

            // once we have all the heights of each plank, we can now calculate their absolute positioning.
            if (renderAllPlanks) {
                this.setPlankPositioning();
                planksRendered[this.state.containerWidth].heightsSet = true;
            }

            this.setState({
                allPlanksRendered: planksRendered
            });
        }

        /**
         * Set the plank positioning. Position sorting occurs here.
         */

    }, {
        key: 'setPlankPositioning',
        value: function setPlankPositioning() {
            var plankPosition = this.state.plankPosition;

            if (!plankPosition[this.state.containerWidth]) {
                plankPosition[this.state.containerWidth] = {};
            }

            var numColumns = this.props.options.breakpoints[this.state.breakpointKey];
            var columnHeights = [];
            var colHeightPadding = []; // Keeps track of the accumulated vertical padding;

            for (var _i = 0; _i < numColumns; _i++) {
                columnHeights[_i] = colHeightPadding[_i] = 0;
            }

            // iterate from left col to right col looking for the shortest col.
            // if all cols are equal height, place in the left most col.
            var i = 0;
            var col = 0;
            var left = 0;

            var plankWidth = this.state.plankWidths[this.state.containerWidth];
            var hPadding = this.props.options.horizontalPadding;
            var vPadding = this.props.options.verticalPadding;
            var unitRatio = this.props.options.unitType === 'rem' ? 16 : 1;
            for (; i < this.props.children.length; i++) {
                var plankHeight = this.state.plankHeights[this.state.containerWidth][i];
                var positionStyle = {};
                var shortestColHeight = Math.min.apply(Math, columnHeights);
                var shortestColIndex = columnHeights.indexOf(Math.min.apply(Math, columnHeights));
                var leftProperty = shortestColIndex * plankWidth + shortestColIndex * hPadding;
                var topProperty = shortestColHeight / unitRatio + colHeightPadding[shortestColIndex];

                positionStyle = {
                    left: leftProperty,
                    top: topProperty
                };

                plankPosition[this.state.containerWidth][i] = positionStyle;
                columnHeights[shortestColIndex] += plankHeight;
                colHeightPadding[shortestColIndex] += vPadding;

                if (col === numColumns - 1) {
                    col = left = 0;
                }
            }

            var greatestHeight = columnHeights.reduce(function (prev, cur) {
                return prev > cur ? prev : cur;
            });
            var containerHeights = this.state.containerHeights;

            greatestHeight += Math.max.apply(Math, colHeightPadding) * unitRatio;
            containerHeights[this.state.containerWidth] = greatestHeight;

            this.setState({
                plankPosition: plankPosition,
                containerHeights: containerHeights
            });
        }
    }, {
        key: 'getPlankStyles',
        value: function getPlankStyles(index) {
            if (!this.state.allPlanksRendered[this.state.containerWidth] || !this.state.allPlanksRendered[this.state.containerWidth].heightsSet) {
                return {
                    position: 'absolute',
                    visibility: 'hidden',
                    width: this.state.plankWidths[this.state.containerWidth] + this.props.options.unitType
                };
            } else {
                var positionStyles = this.state.plankPosition[this.state.containerWidth][index + ''];

                return {
                    position: 'absolute',
                    visibility: 'visible',
                    width: this.state.plankWidths[this.state.containerWidth] + this.props.options.unitType,
                    left: positionStyles.left + 'rem',
                    top: positionStyles.top + 'rem'
                };
            }
        }

        /**
         * Child styles are fetched upon rendering in order to allow for responsiveness. These styles have already been
         * calculated and are simply set here to render based on the current responsive breakpoint.
         */

    }, {
        key: 'render',
        value: function render() {
            var _this2 = this;

            // console.log('[PLANKS CONTAINER] CONTAINER RENDERING...');
            // console.log('[PLANKS CONTAINER] CURRENT CHILD PLANK WIDTH: ' + this.state.plankWidths[this.state.breakpointKey]);

            var containerStyle = {
                height: this.state.containerHeights[this.state.containerWidth],
                position: 'relative'
            };
            var planks = void 0;

            if (_react2.default.Children.count(this.props.children) > 1) {
                planks = this.props.children.map(function (plank, index) {
                    var styles = _this2.getPlankStyles(index);
                    var lastPlankHandler = index === _react2.default.Children.count(_this2.props.children) - 1 ? _this2.handleAllHiddenPlanksRendered.bind(_this2) : function () {
                        return null;
                    };

                    return _react2.default.createElement(
                        _plank2.default,
                        {
                            key: index,
                            index: index,
                            plankStyles: styles,
                            plankWidth: _this2.state.plankWidths[_this2.state.containerWidth],
                            updateChildHeight: _this2.receiveChildHeight.bind(_this2),
                            handleLastPlank: lastPlankHandler
                        },
                        plank
                    );
                });
            } else {
                planks = this.props.children;
            }

            return _react2.default.createElement(
                'div',
                { style: containerStyle, ref: function ref(c) {
                        return _this2._planksContainer = c;
                    } },
                planks
            );
        }
    }]);

    return Planks;
}(_react2.default.Component);

exports.default = Planks;


Planks.propTypes = { options: _react2.default.PropTypes.object };
Planks.defaultProps = {
    options: {
        'breakpoints': {
            '544': 1,
            '768': 2,
            '992': 3,
            '1200': 4
        },
        'horizontalPadding': 1,
        'verticalPadding': 1,
        'unitType': 'rem'
    }
};