'use strict';

var helpers = require('../helpers/index');

module.exports = function (Chart) {

    function filterByPosition(array, position) {
        return helpers.where(array, function (v) {
            return v.position === position;
        });
    }

    function sortByWeight(array, reverse) {
        array.forEach(function (v, i) {
            v._tmpIndex_ = i;
            return v;
        });
        array.sort(function (a, b) {
            var v0 = reverse ? b : a;
            var v1 = reverse ? a : b;
            return v0.weight === v1.weight ?
                v0._tmpIndex_ - v1._tmpIndex_ :
                v0.weight - v1.weight;
        });
        array.forEach(function (v) {
            delete v._tmpIndex_;
        });
    }

    /**
     * @interface ILayoutItem
     * @prop {String} position - The position of the item in the chart layout. Possible values are
     * 'left', 'top', 'right', 'bottom', and 'chartArea'
     * @prop {Number} weight - The weight used to sort the item. Higher weights are further away from the chart area
     * @prop {Boolean} fullWidth - if true, and the item is horizontal, then push vertical boxes down
     * @prop {Function} isHorizontal - returns true if the layout item is horizontal (ie. top or bottom)
     * @prop {Function} update - Takes two parameters: width and height. Returns size of item
     * @prop {Function} getPadding -  Returns an object with padding on the edges
     * @prop {Number} width - Width of item. Must be valid after update()
     * @prop {Number} height - Height of item. Must be valid after update()
     * @prop {Number} left - Left edge of the item. Set by layout system and cannot be used in update
     * @prop {Number} top - Top edge of the item. Set by layout system and cannot be used in update
     * @prop {Number} right - Right edge of the item. Set by layout system and cannot be used in update
     * @prop {Number} bottom - Bottom edge of the item. Set by layout system and cannot be used in update
     */

    // The layout service is very self explanatory.  It's responsible for the layout within a chart.
    // Scales, Legends and Plugins all rely on the layout service and can easily register to be placed anywhere they need
    // It is this service's responsibility of carrying out that layout.
    Chart.layoutService = {
        defaults: {},

        /**
         * Register a box to a chart.
         * A box is simply a reference to an object that requires layout. eg. Scales, Legend, Title.
         * @param {Chart} chart - the chart to use
         * @param {ILayoutItem} item - the item to add to be layed out
         */
        addBox: function (chart, item) {
            if (!chart.boxes) {
                chart.boxes = [];
            }

            // initialize item with default values
            item.fullWidth = item.fullWidth || false;
            item.position = item.position || 'top';
            item.weight = item.weight || 0;

            chart.boxes.push(item);
        },

        /**
         * Remove a layoutItem from a chart
         * @param {Chart} chart - the chart to remove the box from
         * @param {Object} layoutItem - the item to remove from the layout
         */
        removeBox: function (chart, layoutItem) {
            var index = chart.boxes ? chart.boxes.indexOf(layoutItem) : -1;
            if (index !== -1) {
                chart.boxes.splice(index, 1);
            }
        },

        /**
         * Sets (or updates) options on the given `item`.
         * @param {Chart} chart - the chart in which the item lives (or will be added to)
         * @param {Object} item - the item to configure with the given options
         * @param {Object} options - the new item options.
         */
        configure: function (chart, item, options) {
            var props = ['fullWidth', 'position', 'weight'];
            var ilen = props.length;
            var i = 0;
            var prop;

            for (; i < ilen; ++i) {
                prop = props[i];
                if (options.hasOwnProperty(prop)) {
                    item[prop] = options[prop];
                }
            }
        },

        /**
         * Fits boxes of the given chart into the given size by having each box measure itself
         * then running a fitting algorithm
         * @param {Chart} chart - the chart
         * @param {Number} width - the width to fit into
         * @param {Number} height - the height to fit into
         */
        update: function (chart, width, height) {
            if (!chart) {
                return;
            }

            var layoutOptions = chart.options.layout || {};
            var padding = helpers.options.toPadding(layoutOptions.padding);
            var leftPadding = padding.left;
            var rightPadding = padding.right;
            var topPadding = padding.top;
            var bottomPadding = padding.bottom;

            var leftBoxes = filterByPosition(chart.boxes, 'left');
            var rightBoxes = filterByPosition(chart.boxes, 'right');
            var topBoxes = filterByPosition(chart.boxes, 'top');
            var bottomBoxes = filterByPosition(chart.boxes, 'bottom');
            var chartAreaBoxes = filterByPosition(chart.boxes, 'chartArea');

            // Sort boxes by weight. A higher weight is further away from the chart area
            sortByWeight(leftBoxes, true);
            sortByWeight(rightBoxes, false);
            sortByWeight(topBoxes, true);
            sortByWeight(bottomBoxes, false);

            // Essentially we now have any number of boxes on each of the 4 sides.
            // Our canvas looks like the following.
            // The areas L1 and L2 are the left axes. R1 is the right axis, T1 is the top axis and
            // B1 is the bottom axis
            // There are also 4 quadrant-like locations (left to right instead of clockwise) reserved for chart overlays
            // These locations are single-box locations only, when trying to register a chartArea location that is already taken,
            // an error will be thrown.
            //
            // |----------------------------------------------------|
            // |                  T1 (Full Width)                   |
            // |----------------------------------------------------|
            // |    |    |                 T2                  |    |
            // |    |----|-------------------------------------|----|
            // |    |    | C1 |                           | C2 |    |
            // |    |    |----|                           |----|    |
            // |    |    |                                     |    |
            // | L1 | L2 |           ChartArea (C0)            | R1 |
            // |    |    |                                     |    |
            // |    |    |----|                           |----|    |
            // |    |    | C3 |                           | C4 |    |
            // |    |----|-------------------------------------|----|
            // |    |    |                 B1                  |    |
            // |----------------------------------------------------|
            // |                  B2 (Full Width)                   |
            // |----------------------------------------------------|
            //
            // What we do to find the best sizing, we do the following
            // 1. Determine the minimum size of the chart area.
            // 2. Split the remaining width equally between each vertical axis
            // 3. Split the remaining height equally between each horizontal axis
            // 4. Give each layout the maximum size it can be. The layout will return it's minimum size
            // 5. Adjust the sizes of each axis based on it's minimum reported size.
            // 6. Refit each axis
            // 7. Position each axis in the final location
            // 8. Tell the chart the final location of the chart area
            // 9. Tell any axes that overlay the chart area the positions of the chart area

            // Step 1
            var chartWidth = width - leftPadding - rightPadding;
            var chartHeight = height - topPadding - bottomPadding;
            var chartAreaWidth = chartWidth / 2; // min 50%
            var chartAreaHeight = chartHeight / 2; // min 50%

            // Step 2
            var verticalBoxWidth = (width - chartAreaWidth) / (leftBoxes.length + rightBoxes.length);

            // Step 3
            var horizontalBoxHeight = (height - chartAreaHeight) / (topBoxes.length + bottomBoxes.length);

            // Step 4
            var maxChartAreaWidth = chartWidth;
            var maxChartAreaHeight = chartHeight;
            var minBoxSizes = [];

            function getMinimumBoxSize(box) {
                var minSize;
                var isHorizontal = box.isHorizontal();

                if (isHorizontal) {
                    minSize = box.update(box.fullWidth ? chartWidth : maxChartAreaWidth, horizontalBoxHeight);
                    maxChartAreaHeight -= minSize.height;
                } else {
                    minSize = box.update(verticalBoxWidth, chartAreaHeight);
                    maxChartAreaWidth -= minSize.width;
                }

                minBoxSizes.push({
                    horizontal: isHorizontal,
                    minSize: minSize,
                    box: box,
                });
            }

            helpers.each(leftBoxes.concat(rightBoxes, topBoxes, bottomBoxes), getMinimumBoxSize);

            // If a horizontal box has padding, we move the left boxes over to avoid ugly charts (see issue #2478)
            var maxHorizontalLeftPadding = 0;
            var maxHorizontalRightPadding = 0;
            var maxVerticalTopPadding = 0;
            var maxVerticalBottomPadding = 0;

            helpers.each(topBoxes.concat(bottomBoxes), function (horizontalBox) {
                if (horizontalBox.getPadding) {
                    var boxPadding = horizontalBox.getPadding();
                    maxHorizontalLeftPadding = Math.max(maxHorizontalLeftPadding, boxPadding.left);
                    maxHorizontalRightPadding = Math.max(maxHorizontalRightPadding, boxPadding.right);
                }
            });

            helpers.each(leftBoxes.concat(rightBoxes), function (verticalBox) {
                if (verticalBox.getPadding) {
                    var boxPadding = verticalBox.getPadding();
                    maxVerticalTopPadding = Math.max(maxVerticalTopPadding, boxPadding.top);
                    maxVerticalBottomPadding = Math.max(maxVerticalBottomPadding, boxPadding.bottom);
                }
            });

            // At this point, maxChartAreaHeight and maxChartAreaWidth are the size the chart area could
            // be if the axes are drawn at their minimum sizes.
            // Steps 5 & 6
            var totalLeftBoxesWidth = leftPadding;
            var totalRightBoxesWidth = rightPadding;
            var totalTopBoxesHeight = topPadding;
            var totalBottomBoxesHeight = bottomPadding;

            // Function to fit a box
            function fitBox(box) {
                var minBoxSize = helpers.findNextWhere(minBoxSizes, function (minBox) {
                    return minBox.box === box;
                });

                if (minBoxSize) {
                    if (box.isHorizontal()) {
                        var scaleMargin = {
                            left: Math.max(totalLeftBoxesWidth, maxHorizontalLeftPadding),
                            right: Math.max(totalRightBoxesWidth, maxHorizontalRightPadding),
                            top: 0,
                            bottom: 0
                        };

                        // Don't use min size here because of label rotation. When the labels are rotated, their rotation highly depends
                        // on the margin. Sometimes they need to increase in size slightly
                        box.update(box.fullWidth ? chartWidth : maxChartAreaWidth, chartHeight / 2, scaleMargin);
                    } else {
                        box.update(minBoxSize.minSize.width, maxChartAreaHeight);
                    }
                }
            }

            // Update, and calculate the left and right margins for the horizontal boxes
            helpers.each(leftBoxes.concat(rightBoxes), fitBox);

            helpers.each(leftBoxes, function (box) {
                totalLeftBoxesWidth += box.width;
            });

            helpers.each(rightBoxes, function (box) {
                totalRightBoxesWidth += box.width;
            });

            // Set the Left and Right margins for the horizontal boxes
            helpers.each(topBoxes.concat(bottomBoxes), fitBox);

            // Figure out how much margin is on the top and bottom of the vertical boxes
            helpers.each(topBoxes, function (box) {
                totalTopBoxesHeight += box.height;
            });

            helpers.each(bottomBoxes, function (box) {
                totalBottomBoxesHeight += box.height;
            });

            function finalFitVerticalBox(box) {
                var minBoxSize = helpers.findNextWhere(minBoxSizes, function (minSize) {
                    return minSize.box === box;
                });

                var scaleMargin = {
                    left: 0,
                    right: 0,
                    top: totalTopBoxesHeight,
                    bottom: totalBottomBoxesHeight
                };

                if (minBoxSize) {
                    box.update(minBoxSize.minSize.width, maxChartAreaHeight, scaleMargin);
                }
            }

            // Let the left layout know the final margin
            helpers.each(leftBoxes.concat(rightBoxes), finalFitVerticalBox);

            // Recalculate because the size of each layout might have changed slightly due to the margins (label rotation for instance)
            totalLeftBoxesWidth = leftPadding;
            totalRightBoxesWidth = rightPadding;
            totalTopBoxesHeight = topPadding;
            totalBottomBoxesHeight = bottomPadding;

            helpers.each(leftBoxes, function (box) {
                totalLeftBoxesWidth += box.width;
            });

            helpers.each(rightBoxes, function (box) {
                totalRightBoxesWidth += box.width;
            });

            helpers.each(topBoxes, function (box) {
                totalTopBoxesHeight += box.height;
            });
            helpers.each(bottomBoxes, function (box) {
                totalBottomBoxesHeight += box.height;
            });

            // We may be adding some padding to account for rotated x axis labels
            var leftPaddingAddition = Math.max(maxHorizontalLeftPadding - totalLeftBoxesWidth, 0);
            totalLeftBoxesWidth += leftPaddingAddition;
            totalRightBoxesWidth += Math.max(maxHorizontalRightPadding - totalRightBoxesWidth, 0);

            var topPaddingAddition = Math.max(maxVerticalTopPadding - totalTopBoxesHeight, 0);
            totalTopBoxesHeight += topPaddingAddition;
            totalBottomBoxesHeight += Math.max(maxVerticalBottomPadding - totalBottomBoxesHeight, 0);

            // Figure out if our chart area changed. This would occur if the dataset layout label rotation
            // changed due to the application of the margins in step 6. Since we can only get bigger, this is safe to do
            // without calling `fit` again
            var newMaxChartAreaHeight = height - totalTopBoxesHeight - totalBottomBoxesHeight;
            var newMaxChartAreaWidth = width - totalLeftBoxesWidth - totalRightBoxesWidth;

            if (newMaxChartAreaWidth !== maxChartAreaWidth || newMaxChartAreaHeight !== maxChartAreaHeight) {
                helpers.each(leftBoxes, function (box) {
                    box.height = newMaxChartAreaHeight;
                });

                helpers.each(rightBoxes, function (box) {
                    box.height = newMaxChartAreaHeight;
                });

                helpers.each(topBoxes, function (box) {
                    if (!box.fullWidth) {
                        box.width = newMaxChartAreaWidth;
                    }
                });

                helpers.each(bottomBoxes, function (box) {
                    if (!box.fullWidth) {
                        box.width = newMaxChartAreaWidth;
                    }
                });

                maxChartAreaHeight = newMaxChartAreaHeight;
                maxChartAreaWidth = newMaxChartAreaWidth;
            }

            // Step 7 - Position the boxes
            var left = leftPadding + leftPaddingAddition;
            var top = topPadding + topPaddingAddition;

            function placeBox(box) {
                if (box.isHorizontal()) {
                    box.left = box.fullWidth ? leftPadding : totalLeftBoxesWidth;
                    box.right = box.fullWidth ? width - rightPadding : totalLeftBoxesWidth + maxChartAreaWidth;
                    box.top = top;
                    box.bottom = top + box.height;

                    // Move to next point
                    top = box.bottom;

                } else {

                    box.left = left;
                    box.right = left + box.width;
                    box.top = totalTopBoxesHeight;
                    box.bottom = totalTopBoxesHeight + maxChartAreaHeight;

                    // Move to next point
                    left = box.right;
                }
            }

            helpers.each(leftBoxes.concat(topBoxes), placeBox);

            // Account for chart width and height
            left += maxChartAreaWidth;
            top += maxChartAreaHeight;

            helpers.each(rightBoxes, placeBox);
            helpers.each(bottomBoxes, placeBox);

            // Step 8
            chart.chartArea = {
                left: totalLeftBoxesWidth,
                top: totalTopBoxesHeight,
                right: totalLeftBoxesWidth + maxChartAreaWidth,
                bottom: totalTopBoxesHeight + maxChartAreaHeight
            };

            // Step 9
            helpers.each(chartAreaBoxes, function (box) {
                box.left = chart.chartArea.left;
                box.top = chart.chartArea.top;
                box.right = chart.chartArea.right;
                box.bottom = chart.chartArea.bottom;

                box.update(maxChartAreaWidth, maxChartAreaHeight);
            });
        }
    };
};
