/* *
 *
 *  (c) 2019-2026 Highsoft AS
 *  Author: Torstein Hønsi
 *
 *  Item series type for Highcharts
 *
 *  A commercial license may be required depending on use.
 *  See www.highcharts.com/license
 *
 *
 * */
'use strict';
import ItemPoint from './ItemPoint.js';
import ItemSeriesDefaults from './ItemSeriesDefaults.js';
import SeriesRegistry from '../../Core/Series/SeriesRegistry.js';
const { pie: PieSeries } = SeriesRegistry.seriesTypes;
import { defined, extend, fireEvent, isNumber, merge, pick } from '../../Shared/Utilities.js';
/* *
 *
 *  Class
 *
 * */
// 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.
/**
 * The item series type.
 *
 * @requires modules/item-series
 *
 * @private
 * @class
 * @name Highcharts.seriesTypes.item
 *
 * @augments Highcharts.seriesTypes.pie
 */
class ItemSeries extends PieSeries {
    /* *
     *
     *  Functions
     *
     * */
    /**
     * Fade in the whole chart.
     * @private
     */
    animate(init) {
        const group = this.group;
        if (group) {
            if (init) {
                group.attr({
                    opacity: 0
                });
            }
            else {
                group.animate({
                    opacity: 1
                }, this.options.animation);
            }
        }
    }
    drawDataLabels() {
        if (this.center && this.slots) {
            super.drawDataLabels();
            // Or it's just a dot chart with no natural place to put the data labels
        }
        else {
            for (const point of this.points) {
                point.destroyElements({ dataLabel: 1 });
            }
        }
    }
    drawPoints() {
        const series = this, options = this.options, renderer = series.chart.renderer, seriesMarkerOptions = options.marker, borderWidth = this.borderWidth, crisp = borderWidth % 2 ? 0.5 : 1, 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);
        let i = 0;
        /* @todo: remove if not needed
        this.slots.forEach(slot => {
            this.chart.renderer.circle(slot.x, slot.y, 6)
                .attr({
                    fill: 'silver'
                })
                .add(this.group);
        });
        //*/
        for (const point of series.points) {
            const pointMarkerOptions = point.marker || {}, symbol = (pointMarkerOptions.symbol ||
                seriesMarkerOptions.symbol), r = pick(pointMarkerOptions.radius, seriesMarkerOptions.radius), size = defined(r) ? 2 * r : itemSize, padding = size * options.itemPadding;
            let attr, graphics, pointAttr, 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 (let val = 0; val < (point.y || 0); ++val) {
                    // Semi-circle
                    if (series.center && series.slots) {
                        // Fill up the slots from left to right
                        const 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 (typeof r !== 'undefined') {
                        attr.r = r;
                    }
                    // Circles attributes update (#17257)
                    if (pointAttr) {
                        extend(attr, pointAttr);
                    }
                    let graphic = graphics[val];
                    if (graphic) {
                        graphic.animate(attr);
                    }
                    else {
                        graphic = renderer
                            .symbol(symbol, void 0, void 0, void 0, void 0, {
                            backgroundSize: 'within'
                        })
                            .attr(attr)
                            .add(point.graphic);
                    }
                    graphic.isActive = true;
                    graphics[val] = graphic;
                    ++i;
                }
            }
            for (let j = 0; j < graphics.length; j++) {
                const graphic = graphics[j];
                if (!graphic) {
                    return;
                }
                if (!graphic.isActive) {
                    graphic.destroy();
                    graphics.splice(j, 1);
                    j--; // Need to subtract 1 after splice, #19053
                }
                else {
                    graphic.isActive = false;
                }
            }
        }
    }
    getRows() {
        const chart = this.chart, total = this.total || 0;
        let rows = this.options.rows, cols, ratio;
        // Get the row count that gives the most square cells
        if (!rows) {
            ratio = chart.plotWidth / chart.plotHeight;
            rows = Math.sqrt(total);
            if (ratio > 1) {
                rows = Math.ceil(rows);
                while (rows > 1) {
                    cols = total / rows;
                    if (cols / rows > ratio) {
                        break;
                    }
                    rows--;
                }
            }
            else {
                rows = Math.floor(rows);
                while (rows < total) {
                    cols = total / rows;
                    if (cols / rows < ratio) {
                        break;
                    }
                    rows++;
                }
            }
        }
        return rows;
    }
    /**
     * Get the semi-circular slots.
     * @private
     */
    getSlots() {
        const series = this, center = series.center, diameter = center[2], slots = series.slots = series.slots || [], fullAngle = (series.endAngleRad - series.startAngleRad), rowsOption = series.options.rows, isCircle = fullAngle % (2 * Math.PI) === 0, total = series.total || 0;
        let innerSize = center[3], x, y, rowRadius, rowLength, colCount, increment, angle, col, itemSize = 0, rowCount, itemCount = Number.MAX_VALUE, finalItemCount, rows, testRows, 
        // How many rows (arcs) should be used
        rowFraction = (diameter - innerSize) / diameter;
        // Increase the itemSize until we find the best fit
        while (itemCount > total + (rows && isCircle ? rows.length : 0)) {
            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 (let 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.
        let overshoot = finalItemCount - series.total -
            (isCircle ? rows.length : 0);
        /**
         * @private
         * @param {Highcharts.ItemRowContainerObject} item
         * Wrapped object with angle and row
         */
        const 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((row) => ({
                angle: row.colCount / row.rowLength,
                row: row
            }))
                // Sort by the angles...
                .sort((a, b) => (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);
        }
        for (const row of rows) {
            const rowRadius = row.rowRadius, colCount = row.colCount;
            increment = colCount ? fullAngle / colCount : 0;
            for (col = 0; col <= colCount; col += 1) {
                angle = series.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 });
            }
        }
        // Sort by angle
        slots.sort((a, b) => (a.angle - b.angle));
        series.itemSize = itemSize;
        return slots;
    }
    translate(positions) {
        // Initialize chart without setting data, #13379.
        if (this.total === 0 && // Check if that is a (semi-)circle
            isNumber(this.options.startAngle) &&
            isNumber(this.options.endAngle)) {
            this.center = this.getCenter();
        }
        if (!this.slots) {
            this.slots = [];
        }
        if (isNumber(this.options.startAngle) &&
            isNumber(this.options.endAngle)) {
            super.translate(positions);
            this.slots = this.getSlots();
        }
        else {
            this.generatePoints();
            fireEvent(this, 'afterTranslate');
        }
    }
}
/* *
 *
 *  Static Properties
 *
 * */
ItemSeries.defaultOptions = merge(PieSeries.defaultOptions, ItemSeriesDefaults);
extend(ItemSeries.prototype, {
    markerAttribs: void 0,
    pointClass: ItemPoint
});
SeriesRegistry.registerSeriesType('item', ItemSeries);
/* *
 *
 *  Default Export
 *
 * */
export default ItemSeries;
