/* *
 *
 *  (c) 2009-2026 Highsoft AS
 *
 *  Integration of this software requires a license.
 *  - For commercial use, see www.highcharts.com/license
 *  - For non-commercial, see www.highcharts.com/license-eula
 *
 *
 *  Authors:
 *  - Gøran Slettemark
 *  - Wojciech Chmiel
 *  - Sebastian Bochan
 *  - Sophie Bremer
 *  - Dawid Draguła
 *
 * */
'use strict';
import Component from '../Component.js';
import DataConverter from '../../../Data/Converters/DataConverter.js';
import DataTable from '../../../Data/DataTable.js';
import Globals from '../../Globals.js';
import HighchartsSyncs from './HighchartsSyncs/HighchartsSyncs.js';
import HighchartsComponentDefaults from './HighchartsComponentDefaults.js';
import DataConverterUtils from '../../../Data/Converters/DataConverterUtils.js';
import DU from '../../Utilities.js';
import { createElement, diffObjects, isString, merge, splat } from '../../../Shared/Utilities.js';
const { deepClone } = DU;
/* *
 *
 *  Class
 *
 * */
/**
 *
 * Class that represents a Highcharts component.
 *
 */
class HighchartsComponent extends Component {
    /* *
     *
     *  Constructor
     *
     * */
    /**
     * Creates a Highcharts component in the cell.
     *
     * @param options
     * The options for the component.
     */
    constructor(cell, options, board) {
        options = merge(HighchartsComponent.defaultOptions, options);
        super(cell, options, board);
        /**
         * An object of series IDs and their connector handlers.
         */
        this.seriesFromConnector = {};
        this.options = options;
        this.chartConstructor = this.options.chartConstructor || 'chart';
        this.type = 'Highcharts';
        this.chartContainer = createElement('figure', void 0, void 0, this.contentElement, true);
        this.setOptions();
        this.chartOptions = merge((this.options.chartOptions ||
            { chart: {} }), {
            tooltip: {} // Temporary fix for #18876
        });
        this.innerResizeTimeouts = [];
    }
    onTableChanged() {
        this.updateSeries();
    }
    /* *
     *
     *  Functions
     *
     * */
    async load() {
        this.emit({ type: 'load' });
        await super.load();
        this.emit({ type: 'afterLoad' });
        return this;
    }
    render() {
        const hcComponent = this;
        super.render();
        hcComponent.chart = hcComponent.getChart();
        hcComponent.updateSeries();
        hcComponent.emit({ type: 'afterRender' });
        hcComponent.setupConnectorUpdate();
        this.sync.start();
        return this;
    }
    resize(width, height) {
        this.resizeDynamicContent(width, height);
        while (this.innerResizeTimeouts.length) {
            const timeoutID = this.innerResizeTimeouts.pop();
            if (timeoutID) {
                clearTimeout(timeoutID);
            }
        }
        this.innerResizeTimeouts.push(setTimeout(() => {
            if (this.chart && this.chart.container) {
                const heightOffset = this.contentElement.offsetHeight -
                    this.chart?.container.offsetHeight;
                this.chart.setSize(null, (Math.abs(heightOffset) > 1) ?
                    this.contentElement.offsetHeight : null, false);
            }
        }, 33));
        return this;
    }
    /**
     * Adds call update value in store, when chart's point is updated.
     * @private
     */
    setupConnectorUpdate() {
        const { connectorHandlers, chart } = this;
        if (!chart || !this.options.allowConnectorUpdate) {
            return;
        }
        const seriesLength = chart.series.length;
        for (let i = 0, iEnd = connectorHandlers.length; i < iEnd; i++) {
            const connectorHandler = connectorHandlers[i];
            for (let j = 0; j < seriesLength; j++) {
                const series = chart.series[j];
                series.update({
                    point: {
                        events: {
                            update: (e) => {
                                this.onChartUpdate(e.target, connectorHandler);
                            }
                        }
                    }
                }, false);
            }
        }
    }
    /**
     * Update the store, when the point is being dragged.
     * @param point Dragged point.
     * @param connectorHandler Connector handler with data to update.
     */
    onChartUpdate(point, connectorHandler) {
        const table = connectorHandler.dataTable;
        const columnAssignment = connectorHandler.columnAssignment;
        const seriesId = point.series.options.id;
        const converter = new DataConverter();
        const valueToSet = DataConverterUtils.asNumber(point.y, converter.decimalRegExp);
        if (!table) {
            return;
        }
        let columnId;
        if (columnAssignment && seriesId) {
            const data = columnAssignment.find((s) => s.seriesId === seriesId)?.data;
            if (isString(data)) {
                columnId = data;
            }
            else if (Array.isArray(data)) {
                columnId = data[1];
            }
            else if (data) {
                columnId = data.y ?? data.value;
            }
        }
        if (!columnId) {
            columnId = seriesId ?? point.series.name;
        }
        table.setCell(columnId, point.index, valueToSet);
    }
    /**
     * Internal method for handling option updates.
     * @internal
     */
    setOptions() {
        if (this.options.chartClassName) {
            this.chartContainer.classList.value =
                HighchartsComponentDefaults.className + ' ' +
                    this.options.chartClassName;
        }
        if (this.options.chartID) {
            this.chartContainer.id = this.options.chartID;
        }
    }
    /**
     * Handles updating via options.
     *
     * @param options
     * The options to apply.
     */
    async update(options, shouldRerender = true) {
        await super.update(options, false);
        this.setOptions();
        if (this.options.chartConstructor !== this.chartConstructor) {
            this.chartConstructor = this.options.chartConstructor || 'chart';
            this.chartOptions = this.options.chartOptions || {};
            this.chart?.destroy();
            delete this.chart;
        }
        else {
            this.chart?.update(merge(this.options.chartOptions) || {});
        }
        this.emit({ type: 'afterUpdate' });
        shouldRerender && this.render();
    }
    /**
     * Updates chart's series when the data table is changed.
     * @private
     */
    updateSeries() {
        const { chart } = this;
        const connectorHandlers = this.connectorHandlers;
        if (!chart) {
            return;
        }
        const newSeriesIds = [];
        for (const connectorHandler of connectorHandlers) {
            const options = connectorHandler.options;
            let columnAssignment = options.columnAssignment;
            if (!columnAssignment && connectorHandler.dataTable) {
                columnAssignment = this.getDefaultColumnAssignment(connectorHandler.dataTable.getColumnIds(), connectorHandler.dataTable);
            }
            if (columnAssignment) {
                connectorHandler.columnAssignment = columnAssignment;
                for (const { seriesId } of columnAssignment) {
                    if (seriesId) {
                        newSeriesIds.push(seriesId);
                    }
                }
            }
        }
        const seriesArray = Object.keys(this.seriesFromConnector);
        // Remove series that were added in the previous update and are not
        // present in the new columnAssignment.
        for (let i = 0, iEnd = seriesArray.length; i < iEnd; ++i) {
            const oldSeriesId = seriesArray[i];
            if (newSeriesIds.some((newSeriesId) => newSeriesId === oldSeriesId)) {
                continue;
            }
            const series = chart.get(oldSeriesId);
            if (series) {
                series.destroy();
            }
        }
        this.seriesFromConnector = {};
        for (const connectorHandler of connectorHandlers) {
            this.updateSeriesFromConnector(connectorHandler);
        }
        chart.redraw();
    }
    /**
     * Updates the series based on the connector from each connector handler.
     * @param connectorHandler The connector handler.
     * @private
     */
    updateSeriesFromConnector(connectorHandler) {
        const chart = this.chart;
        if (!connectorHandler.connector ||
            !chart ||
            !connectorHandler.dataTable) {
            return;
        }
        const table = connectorHandler.dataTable.getModified();
        const modifierOptions = connectorHandler.dataTable.getModifier()?.options;
        const columnAssignment = connectorHandler.columnAssignment ?? [];
        // Create the series or update the existing ones.
        for (let i = 0, iEnd = columnAssignment.length; i < iEnd; ++i) {
            const assignment = columnAssignment[i];
            const dataStructure = assignment.data;
            const series = chart.get(assignment.seriesId);
            const seriesOptions = {};
            // Prevent dragging on series, which were created out of a
            // columns which are created by MathModifier.
            const adjustDraggableOptions = (compare) => {
                if (modifierOptions?.type === 'Math' &&
                    modifierOptions
                        .columnFormulas?.some((formula) => compare(formula.column))) {
                    seriesOptions.dragDrop = {
                        draggableY: false
                    };
                }
            };
            // Set the series data based on the column assignment data structure
            // type.
            if (isString(dataStructure)) {
                const column = table.getColumn(dataStructure);
                if (column) {
                    seriesOptions.data = column.slice();
                }
                adjustDraggableOptions((columnId) => (columnId === dataStructure));
            }
            else if (Array.isArray(dataStructure)) {
                const seriesTable = new DataTable({
                    columns: table.getColumns(dataStructure)
                });
                seriesOptions.data = seriesTable.getRows();
                adjustDraggableOptions((columnId) => (dataStructure.some((name) => name === columnId)));
            }
            else {
                const keys = Object.keys(dataStructure);
                const columnIds = [];
                for (let j = 0, jEnd = keys.length; j < jEnd; ++j) {
                    columnIds.push(dataStructure[keys[j]]);
                }
                const seriesTable = new DataTable({
                    columns: table.getColumns(columnIds)
                });
                seriesOptions.keys = keys;
                seriesOptions.data = seriesTable.getRows();
                adjustDraggableOptions((columnId) => (columnIds.some((name) => name === columnId)));
            }
            if (!series) {
                chart.addSeries({
                    name: assignment.seriesId,
                    id: assignment.seriesId,
                    ...seriesOptions
                }, false);
            }
            else {
                series.update(seriesOptions, false);
            }
            this.seriesFromConnector[assignment.seriesId] = connectorHandler;
        }
    }
    /**
     * Destroy chart and create a new one.
     *
     * @returns
     * The chart.
     *
     * @private
     *
     */
    getChart() {
        return this.chart || this.createChart();
    }
    /**
     * Destroys the highcharts component.
     */
    destroy() {
        // Cleanup references in the global Highcharts scope
        // Destroy chart before destroying the component element
        // to ensure chart has access to its renderTo element
        if (this.chart && this.chart.renderTo && this.chart.renderer) {
            try {
                this.chart.destroy();
            }
            catch (e) {
                // Chart may already be destroyed or renderTo/renderer
                // eslint-disable-next-line no-console
                console.warn('Error destroying chart:', e);
            }
            this.chart = void 0;
        }
        super.destroy();
    }
    /**
     * Creates default mapping when columnAssignment is not declared.
     * @param  { Array<string>} columnIds all columns returned from dataTable.
     *
     * @returns
     * The record of mapping
     *
     * @private
     *
     */
    getDefaultColumnAssignment(columnIds = [], presentationTable) {
        const result = [];
        const firstColumn = presentationTable.getColumn(columnIds[0]);
        if (firstColumn && isString(firstColumn[0])) {
            for (let i = 1, iEnd = columnIds.length; i < iEnd; ++i) {
                result.push({
                    seriesId: columnIds[i],
                    data: [columnIds[0], columnIds[i]]
                });
            }
            return result;
        }
        for (let i = 0, iEnd = columnIds.length; i < iEnd; ++i) {
            result.push({
                seriesId: columnIds[i],
                data: columnIds[i]
            });
        }
        return result;
    }
    /**
     * Creates chart.
     *
     * @returns
     * The chart.
     *
     * @private
     *
     */
    createChart() {
        const charter = HighchartsComponent.charter || Globals.win.Highcharts;
        if (!this.chartConstructor) {
            this.chartConstructor = 'chart';
        }
        const Factory = charter[this.chartConstructor];
        if (Factory) {
            try {
                if (this.chartConstructor === 'chart') {
                    return charter.Chart.chart(this.chartContainer, this.chartOptions);
                }
                return new Factory(this.chartContainer, this.chartOptions);
            }
            catch (e) {
                throw new Error(`The Highcharts component in cell '${this.cell.id}' ` +
                    'is misconfigured. \n____________\n' +
                    String(e));
            }
        }
        if (typeof charter.chart !== 'function') {
            throw new Error('Chart constructor not found');
        }
        return this.chart;
    }
    getOptionsOnDrop(sidebar) {
        const connectorsIds = sidebar.editMode.board.dataPool.getConnectorIds();
        let options = {
            type: 'Highcharts',
            chartOptions: {
                chart: {
                    animation: false,
                    type: 'column',
                    zooming: {}
                }
            }
        };
        if (connectorsIds.length) {
            options = {
                ...options,
                connector: {
                    id: connectorsIds[0]
                }
            };
        }
        return options;
    }
    /**
     * Get the HighchartsComponent component's options.
     * @returns
     * HighchartsComponent component's options.
     *
     * @internal
     *
     */
    getOptions() {
        return {
            ...diffObjects(this.options, HighchartsComponent.defaultOptions),
            type: 'Highcharts'
        };
    }
    /**
     * Retrieves editable options for the chart.
     *
     * @returns
     * The editable options for the chart and its values.
     */
    getEditableOptions() {
        const component = this;
        const componentOptions = component.options;
        const chart = component.chart;
        const chartOptions = chart && chart.options;
        const chartType = chartOptions?.chart?.type || 'line';
        return deepClone(merge({
            chartOptions
        }, {
            chartOptions: {
                yAxis: splat(chart && chart.yAxis[0].options),
                xAxis: splat(chart && chart.xAxis[0].options),
                plotOptions: {
                    series: ((chartOptions && chartOptions.plotOptions) ||
                        {})[chartType]
                }
            }
        }, componentOptions), ['dataTable', 'points', 'series', 'data', 'editableOptions']);
    }
    getEditableOptionValue(propertyPath) {
        const component = this;
        if (!propertyPath) {
            return;
        }
        if (propertyPath.length === 1 && propertyPath[0] === 'chartOptions') {
            return JSON.stringify(component.options.chartOptions, null, 2);
        }
        return super.getEditableOptionValue.call(this, propertyPath);
    }
}
/**
 * Predefined sync config for Highcharts component.
 */
HighchartsComponent.predefinedSyncConfig = HighchartsSyncs;
/**
 * Default options of the Highcharts component.
 */
HighchartsComponent.defaultOptions = merge(Component.defaultOptions, HighchartsComponentDefaults);
/* *
 *
 *  Default Export
 *
 * */
export default HighchartsComponent;
