/* *
 *
 *  (c) 2019 Torstein Honsi
 *
 *  Item series type for Highcharts
 *
 *  License: www.highcharts.com/license
 *
 * */

'use strict';

import H from '../parts/Globals.js';

import U from '../parts/Utilities.js';
var defined = U.defined,
    isNumber = U.isNumber,
    objectEach = U.objectEach;

import '../parts/Series.js';

var extend = H.extend,
    merge = H.merge,
    piePoint = H.seriesTypes.pie.prototype.pointClass.prototype;

/**
 * The item series type.
 *
 * @requires module:modules/item-series
 *
 * @private
 * @class
 * @name Highcharts.seriesTypes.item
 *
 * @augments Highcharts.seriesTypes.pie
 */
H.seriesType(
    'item',
    // Inherits pie as the most tested non-cartesian series with individual
    // point legend, tooltips etc. Only downside is we need to re-enable
    // marker options.
    'pie',
    /**
     * An item chart is an infographic chart where a number of items are laid
     * out in either a rectangular or circular pattern. It can be used to
     * visualize counts within a group, or for the circular pattern, typically
     * a parliament.
     *
     * The circular layout has much in common with a pie chart. Many of the item
     * series options, like `center`, `size` and data label positioning, are
     * inherited from the pie series and don't apply for rectangular layouts.
     *
     * @sample       highcharts/demo/parliament-chart
     *               Parliament chart (circular item chart)
     * @sample       highcharts/series-item/rectangular
     *               Rectangular item chart
     * @sample       highcharts/series-item/symbols
     *               Infographic with symbols
     *
     * @extends      plotOptions.pie
     * @since        7.1.0
     * @product      highcharts
     * @excluding    borderColor, borderWidth, depth, dragDrop, linecap, shadow,
     *               slicedOffset
     * @optionparent plotOptions.item
     */
    {
        /**
         * In circular view, the end angle of the item layout, in degrees where
         * 0 is up.
         *
         * @sample highcharts/demo/parliament-chart
         *         Parliament chart
         * @type {undefined|number}
         */
        endAngle: undefined,
        /**
         * In circular view, the size of the inner diameter of the circle. Can
         * be a percentage or pixel value. Percentages are relative to the outer
         * perimeter. Pixel values are given as integers.
         *
         * If the `rows` option is set, it overrides the `innerSize` setting.
         *
         * @sample highcharts/demo/parliament-chart
         *         Parliament chart
         * @type {string|number}
         */
        innerSize: '40%',
        /**
         * The padding between the items, given in relative size where the size
         * of the item is 1.
         * @type {number}
         */
        itemPadding: 0.1,
        /**
         * The layout of the items in rectangular view. Can be either
         * `horizontal` or `vertical`.
         * @sample highcharts/series-item/symbols
         *         Horizontal layout
         * @type {string}
         */
        layout: 'vertical',
        /**
         * @extends plotOptions.series.marker
         */
        marker: merge(
            H.defaultOptions.plotOptions.line.marker,
            {
                radius: null
            }
        ),
        /**
         * The number of rows to display in the rectangular or circular view. If
         * the `innerSize` is set, it will be overridden by the `rows` setting.
         *
         * @sample highcharts/series-item/rows-columns
         *         Fixed row count
         * @type {number}
         */
        rows: undefined,
        showInLegend: true,
        /**
         * In circular view, the start angle of the item layout, in degrees
         * where 0 is up.
         *
         * @sample highcharts/demo/parliament-chart
         *         Parliament chart
         * @type {undefined|number}
         */
        startAngle: undefined
    },
    // Prototype members
    {
        translate: function () {
            if (!this.slots) {
                this.slots = [];
            }
            if (
                isNumber(this.options.startAngle) &&
                isNumber(this.options.endAngle)
            ) {
                H.seriesTypes.pie.prototype.translate.call(this);
                this.slots = this.getSlots();
            } else {
                this.generatePoints();
            }
        },

        // Get the semi-circular slots
        getSlots: function () {
            var center = this.center,
                diameter = center[2],
                innerSize = center[3],
                row,
                slots = this.slots,
                x,
                y,
                rowRadius,
                rowLength,
                colCount,
                increment,
                angle,
                col,
                itemSize = 0,
                rowCount,
                fullAngle = this.endAngleRad - this.startAngleRad,
                itemCount = Number.MAX_VALUE,
                finalItemCount,
                rows,
                testRows,
                rowsOption = this.options.rows,
                // How many rows (arcs) should be used
                rowFraction = (diameter - innerSize) / diameter;

            // Increase the itemSize until we find the best fit
            while (itemCount > this.total) {

                finalItemCount = itemCount;

                // Reset
                slots.length = 0;
                itemCount = 0;

                // Now rows is the last successful run
                rows = testRows;
                testRows = [];

                itemSize++;

                // Total number of rows (arcs) from the center to the
                // perimeter
                rowCount = diameter / itemSize / 2;

                if (rowsOption) {
                    innerSize = ((rowCount - rowsOption) / rowCount) * diameter;

                    if (innerSize >= 0) {
                        rowCount = rowsOption;

                    // If innerSize is negative, we are trying to set too
                    // many rows in the rows option, so fall back to
                    // treating it as innerSize 0
                    } else {
                        innerSize = 0;
                        rowFraction = 1;
                    }


                } else {
                    rowCount = Math.floor(rowCount * rowFraction);
                }

                for (row = rowCount; row > 0; row--) {
                    rowRadius = (innerSize + (row / rowCount) *
                        (diameter - innerSize - itemSize)) / 2;
                    rowLength = fullAngle * rowRadius;
                    colCount = Math.ceil(rowLength / itemSize);
                    testRows.push({
                        rowRadius: rowRadius,
                        rowLength: rowLength,
                        colCount: colCount
                    });

                    itemCount += colCount + 1;
                }
            }

            if (!rows) {
                return;
            }

            // We now have more slots than we have total items. Loop over
            // the rows and remove the last slot until the count is correct.
            // For each iteration we sort the last slot by the angle, and
            // remove those with the highest angles.
            var overshoot = finalItemCount - this.total;
            function cutOffRow(item) {
                if (overshoot > 0) {
                    item.row.colCount--;
                    overshoot--;
                }
            }
            while (overshoot > 0) {
                rows
                    // Return a simplified representation of the angle of
                    // the last slot within each row.
                    .map(function (row) {
                        return {
                            angle: row.colCount / row.rowLength,
                            row: row
                        };
                    })
                    // Sort by the angles...
                    .sort(function (a, b) {
                        return b.angle - a.angle;
                    })
                    // ...so that we can ignore the items with the lowest
                    // angles...
                    .slice(
                        0,
                        Math.min(overshoot, Math.ceil(rows.length / 2))
                    )
                    // ...and remove the ones with the highest angles
                    .forEach(cutOffRow);
            }

            rows.forEach(function (row) {
                var rowRadius = row.rowRadius,
                    colCount = row.colCount;
                increment = colCount ? fullAngle / colCount : 0;
                for (col = 0; col <= colCount; col += 1) {
                    angle = this.startAngleRad + col * increment;
                    x = center[0] + Math.cos(angle) * rowRadius;
                    y = center[1] + Math.sin(angle) * rowRadius;
                    slots.push({ x: x, y: y, angle: angle });
                }
            }, this);

            // Sort by angle
            slots.sort(function (a, b) {
                return a.angle - b.angle;
            });

            this.itemSize = itemSize;
            return slots;

        },

        getRows: function () {
            var rows = this.options.rows,
                cols,
                ratio;

            // Get the row count that gives the most square cells
            if (!rows) {
                ratio = this.chart.plotWidth / this.chart.plotHeight;
                rows = Math.sqrt(this.total);

                if (ratio > 1) {
                    rows = Math.ceil(rows);
                    while (rows > 0) {
                        cols = this.total / rows;
                        if (cols / rows > ratio) {
                            break;
                        }
                        rows--;
                    }
                } else {
                    rows = Math.floor(rows);
                    while (rows < this.total) {
                        cols = this.total / rows;
                        if (cols / rows < ratio) {
                            break;
                        }
                        rows++;
                    }
                }
            }
            return rows;
        },

        drawPoints: function () {
            var series = this,
                options = this.options,
                renderer = series.chart.renderer,
                seriesMarkerOptions = options.marker,
                borderWidth = this.borderWidth,
                crisp = borderWidth % 2 ? 0.5 : 1,
                i = 0,
                rows = this.getRows(),
                cols = Math.ceil(this.total / rows),
                cellWidth = this.chart.plotWidth / cols,
                cellHeight = this.chart.plotHeight / rows,
                itemSize = this.itemSize || Math.min(cellWidth, cellHeight);

            /*
            this.slots.forEach(slot => {
                this.chart.renderer.circle(slot.x, slot.y, 6)
                    .attr({
                        fill: 'silver'
                    })
                    .add(this.group);
            });
            //*/

            this.points.forEach(function (point) {
                var attr,
                    graphics,
                    pointAttr,
                    pointMarkerOptions = point.marker || {},
                    symbol = (
                        pointMarkerOptions.symbol ||
                        seriesMarkerOptions.symbol
                    ),
                    r = H.pick(
                        pointMarkerOptions.radius,
                        seriesMarkerOptions.radius
                    ),
                    size = defined(r) ? 2 * r : itemSize,
                    padding = size * options.itemPadding,
                    x,
                    y,
                    width,
                    height;

                point.graphics = graphics = point.graphics || {};

                if (!series.chart.styledMode) {
                    pointAttr = series.pointAttribs(
                        point,
                        point.selected && 'select'
                    );
                }

                if (!point.isNull && point.visible) {

                    if (!point.graphic) {
                        point.graphic = renderer.g('point')
                            .add(series.group);
                    }

                    for (var val = 0; val < point.y; val++) {

                        // Semi-circle
                        if (series.center && series.slots) {

                            // Fill up the slots from left to right
                            var slot = series.slots.shift();
                            x = slot.x - itemSize / 2;
                            y = slot.y - itemSize / 2;

                        } else if (options.layout === 'horizontal') {
                            x = cellWidth * (i % cols);
                            y = cellHeight * Math.floor(i / cols);
                        } else {
                            x = cellWidth * Math.floor(i / rows);
                            y = cellHeight * (i % rows);
                        }

                        x += padding;
                        y += padding;
                        width = Math.round(size - 2 * padding);
                        height = width;

                        if (series.options.crisp) {
                            x = Math.round(x) - crisp;
                            y = Math.round(y) + crisp;
                        }
                        attr = {
                            x: x,
                            y: y,
                            width: width,
                            height: height
                        };
                        if (r !== undefined) {
                            attr.r = r;
                        }


                        if (graphics[val]) {
                            graphics[val].animate(attr);
                        } else {
                            graphics[val] = renderer
                                .symbol(
                                    symbol,
                                    null,
                                    null,
                                    null,
                                    null, {
                                        backgroundSize: 'within'
                                    }
                                )
                                .attr(extend(attr, pointAttr))
                                .add(point.graphic);
                        }
                        graphics[val].isActive = true;


                        i++;
                    }
                }
                objectEach(graphics, function (graphic, key) {
                    if (!graphic.isActive) {
                        graphic.destroy();
                        delete graphics[key];
                    } else {
                        graphic.isActive = false;
                    }
                });
            });
        },
        drawDataLabels: function () {
            if (this.center && this.slots) {
                H.seriesTypes.pie.prototype.drawDataLabels.call(this);

            // else, it's just a dot chart with no natural place to put the
            // data labels
            } else {
                this.points.forEach(function (point) {
                    point.destroyElements({ dataLabel: 1 });
                });
            }
        },

        // Fade in the whole chart
        animate: function (init) {
            if (init) {
                this.group.attr({
                    opacity: 0
                });
            } else {
                this.group.animate({
                    opacity: 1
                }, this.options.animation);
                this.animate = null;
            }
        }
    },
    // Point class
    {
        connectorShapes: piePoint.connectorShapes,
        getConnectorPath: piePoint.getConnectorPath,
        setVisible: piePoint.setVisible,
        getTranslate: piePoint.getTranslate
    }
);


/**
 * An `item` series. If the [type](#series.item.type) option is not specified,
 * it is inherited from [chart.type](#chart.type).
 *
 * @extends   series,plotOptions.item
 * @excluding dataParser, dataURL, stack, xAxis, yAxis
 * @product   highcharts
 * @apioption series.item
 */

/**
 * An array of data points for the series. For the `item` series type,
 * points can be given in the following ways:
 *
 * 1. An array of numerical values. In this case, the numerical values will be
 *    interpreted as `y` options. Example:
 *    ```js
 *    data: [0, 5, 3, 5]
 *    ```
 *
 * 2. An array of objects with named values. The following snippet shows only a
 *    few settings, see the complete options set below. If the total number of
 *    data points exceeds the series'
 *    [turboThreshold](#series.item.turboThreshold),
 *    this option is not available.
 *    ```js
 *    data: [{
 *        y: 1,
 *        name: "Point2",
 *        color: "#00FF00"
 *    }, {
 *        y: 7,
 *        name: "Point1",
 *        color: "#FF00FF"
 *    }]
 *    ```
 *
 * @sample {highcharts} highcharts/chart/reflow-true/
 *         Numerical values
 * @sample {highcharts} highcharts/series/data-array-of-arrays/
 *         Arrays of numeric x and y
 * @sample {highcharts} highcharts/series/data-array-of-arrays-datetime/
 *         Arrays of datetime x and y
 * @sample {highcharts} highcharts/series/data-array-of-name-value/
 *         Arrays of point.name and y
 * @sample {highcharts} highcharts/series/data-array-of-objects/
 *         Config objects
 *
 * @type      {Array<number|Array<string,(number|null)>|null|*>}
 * @extends   series.pie.data
 * @excludes  sliced
 * @product   highcharts
 * @apioption series.item.data
 */

/**
 * The sequential index of the data point in the legend.
 *
 * @type      {number}
 * @product   highcharts
 * @apioption series.pie.data.legendIndex
 */
