/*
 * Copyright (c) 2010, 2025 BSI Business Systems Integration AG
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 */
import {
  Action, arrays, Column, DateColumn, Event, EventListener, IconDesc, icons, InitModelOf, keys, KeyStrokeContext, NumberColumn, objects, scout, scrollbars, strings, styles, TabbableCoordinator, TabbableItem, Table, TableControl,
  TableMatrix, TableMatrixDateGroup, TableMatrixKeyAxis, TableMatrixNumberGroup, TableMatrixResult, tooltips
} from '@eclipse-scout/core';
import {Chart, ChartTableControlEventMap, ChartTableControlLayout, ChartTableControlModel, ChartTableUserFilter, FocusFirstChartTypeKeyStroke} from '../../index';
import $ from 'jquery';
import {BubbleDataPoint, ChartData, ChartType as ChartJsType} from 'chart.js';
import {ChartConfig, ClickObject} from '../../chart/Chart';

export class ChartTableControl extends TableControl implements ChartTableControlModel {
  declare model: ChartTableControlModel;
  declare eventMap: ChartTableControlEventMap;
  declare self: ChartTableControl;

  chartAggregation: TableControlChartAggregation;
  chartGroup1: TableControlChartGroup;
  chartGroup2: TableControlChartGroup;
  chartType: TableControlChartType;
  oldChartType: TableControlChartType;
  chart: Chart;
  chartColorScheme: string;
  xAxis: TableMatrixKeyAxis;
  yAxis: TableMatrixKeyAxis;
  dateGroup: (TableMatrixDateGroup | string)[][];

  $chartSelect: JQuery;
  $axisSelectContainer: JQuery;
  $xAxisSelect: JQuery;
  $yAxisSelect: JQuery;
  $dataSelect: JQuery;
  protected _chartTypeMap: Record<TableControlChartType, Action>;
  protected _chartTypeTabbableCoordinator: TabbableCoordinator;
  protected _aggregationMap: Record<string, JQuery>;
  protected _aggregationTabbableCoordinator: TabbableCoordinator;
  protected _chartGroup1Map: Record<string, JQuery>;
  protected _chartGroup1TabbableCoordinator: TabbableCoordinator;
  protected _chartGroup2Map: Record<string, JQuery>;
  protected _chartGroup2TabbableCoordinator: TabbableCoordinator;
  protected _tableUpdatedHandler: (e: Event<Table>) => void;
  protected _tableColumnStructureChangedHandler: () => void;
  protected _chartValueClickedHandler: () => void;
  protected _filterRemovedListener: EventListener;
  protected _tableUpdatedTimeOutId: number;

  constructor() {
    super();
    this.iconId = icons.CHART;
    this.tooltipText = '${textKey:ui.Chart}';
    this.chartAggregation = {
      modifier: TableMatrix.NumberGroup.COUNT
    };
    this.chartType = Chart.Type.BAR;
    this.oldChartType = null;
    this.chart = null;
    this.chartColorScheme = 'chart-table-control';

    /** chart config selection */
    this.$chartSelect = null;
    this.$xAxisSelect = null;
    this.$yAxisSelect = null;
    this.$dataSelect = null;

    this.xAxis = null;
    this.yAxis = null;

    this.dateGroup = null;

    this._chartTypeTabbableCoordinator = scout.create(TabbableCoordinator, {parent: this, autoRegisterKeyStrokes: false, orientation: 'vertical'});
    this._chartGroup1TabbableCoordinator = scout.create(TabbableCoordinator, {parent: this, autoRegisterKeyStrokes: false, orientation: 'vertical'});
    this._chartGroup2TabbableCoordinator = scout.create(TabbableCoordinator, {parent: this, autoRegisterKeyStrokes: false, orientation: 'vertical'});
    this._aggregationTabbableCoordinator = scout.create(TabbableCoordinator, {parent: this, autoRegisterKeyStrokes: false, orientation: 'vertical'});
    this._tableUpdatedHandler = this._onTableUpdated.bind(this);
    this._tableColumnStructureChangedHandler = this._onTableColumnStructureChanged.bind(this);
    this._chartValueClickedHandler = this._onChartValueClick.bind(this);
  }

  static DATE_GROUP_FLAG = 0x100;
  static MAX_AXIS_COUNT = 100;

  protected override _init(model: InitModelOf<this>) {
    super._init(model);
    this.table.on('columnStructureChanged', this._tableColumnStructureChangedHandler);

    this.chart = scout.create(Chart, {
      parent: this
    });
  }

  protected override _destroy() {
    this.table.off('columnStructureChanged', this._tableColumnStructureChangedHandler);
    super._destroy();
  }

  protected override _initKeyStrokeContext() {
    super._initKeyStrokeContext();
    this.tableControlKeyStrokeContext.registerKeyStroke(new FocusFirstChartTypeKeyStroke(this));
  }

  protected override _computeEnabled(inheritAccessibility: boolean, parentEnabled: boolean): boolean {
    if (!this._hasColumns() && !this.selected) {
      return false;
    }
    return super._computeEnabled(inheritAccessibility, parentEnabled);
  }

  protected _renderChart() {
    if (this.chart) {
      this.chart.render(this.$contentContainer);
      this.chart.$container.addClass(this.denseClass);
    }
  }

  protected override _createLayout(): ChartTableControlLayout {
    return new ChartTableControlLayout(this);
  }

  protected _renderChartType() {
    this._selectChartType();
    this.$yAxisSelect.toggleClass('hide', this.chartType !== Chart.Type.BUBBLE);
    this.$yAxisSelect.toggleClass('animated', scout.isOneOf(Chart.Type.BUBBLE, this.chartType, this.oldChartType) && !!this.oldChartType);
    this.$yAxisSelect.data('scroll-shadow').setVisible(false);
    this.$yAxisSelect.oneAnimationEnd(() => {
      scrollbars.update(this.$yAxisSelect);
      this.$yAxisSelect.data('scroll-shadow').setVisible(true);
      this.$yAxisSelect.removeClass('animated');
    });
    this._chartGroup2TabbableCoordinator.setItems(this.$yAxisSelect.hasClass('hide')
      ? []
      : this.$yAxisSelect.children('.select-axis').map((i, item) => new TabbableItem($(item))).get());

    if (this.contentRendered) {
      this.chart.$container.animateAVCSD('opacity', 0, () => {
        this.chart.$container.css('opacity', 1);
        this._drawChart();
      });
    }
  }

  protected _selectChartType() {
    objects.values(this._chartTypeMap).forEach(action => {
      action.setSelected(false);
    });
    this._chartTypeMap[this.chartType].setSelected(true);
  }

  protected _renderChartGroup1() {
    this._renderChartGroup(1);
  }

  protected _renderChartGroup2() {
    this._renderChartGroup(2);
  }

  protected _renderChartGroup(groupId: 1 | 2) {
    if (!this._hasColumns()) {
      return;
    }
    let groupName = 'chartGroup' + groupId;
    let map = '_' + groupName + 'Map';
    let chartGroup = this[groupName];
    if (chartGroup) {
      let $element = this[map][chartGroup.id];
      $element.siblings('.select-axis')
        .setSelected(false)
        .animateAVCSD('height', 30);
      $element.setSelected(true);

      if (chartGroup.modifier > 0) {
        let dateGroupIndex = chartGroup.modifier ^ ChartTableControl.DATE_GROUP_FLAG;
        $element.animateAVCSD('height', 42);
        $element.children('.select-axis-group').text(this.dateGroup[dateGroupIndex][1]);
      }
      if (this.contentRendered) {
        this._drawChart();
      }
    }
  }

  protected _renderChartAggregation() {
    let $element = this._aggregationMap[this.chartAggregation.id || 'all'];
    if ($element) {
      $element.siblings('.select-data').setSelected(false);
      $element
        .setSelected(true)
        .removeClass('data-sum')
        .removeClass('data-avg');
      $element.addClass(this._getAggregationCssClass());
      if (this.contentRendered) {
        this._drawChart();
      }
    }
  }

  protected _getAggregationCssClass(): string {
    switch (this.chartAggregation.modifier) {
      case TableMatrix.NumberGroup.COUNT:
        return 'data-count';
      case TableMatrix.NumberGroup.SUM:
        return 'data-sum';
      case TableMatrix.NumberGroup.AVG:
        return 'data-avg';
      default:
        return null;
    }
  }

  protected _renderChartSelect(cssClass: string, chartType: TableControlChartType, iconId: string, tooltipText: string) {
    let action = scout.create(Action, {
      parent: this,
      iconId,
      cssClass,
      tooltipText
    });
    action.render(this.$chartSelect);
    action.$container.addClass('chart-type menu-item').data('chartType', chartType);
    action.on('action', this._onChartTypeAction.bind(this));
    action.setEnabled(this._hasColumns());
    this.$contentContainer.one('remove', () => action.destroy());

    this._chartTypeMap[chartType] = action;
  }

  /**
   * Appends a chart selection divs to this.$contentContainer and sets the this.$chartSelect property.
   **/
  protected _renderChartSelectContainer() {
    // create container
    this.$chartSelect = this.$contentContainer.appendDiv('chart-select');

    // create chart types for selection
    this._chartTypeMap = {} as Record<TableControlChartType, Action>;

    let supportedChartTypes = this._getSupportedChartTypes();

    if (scout.isOneOf(Chart.Type.BAR, supportedChartTypes)) {
      this._renderChartSelect('chart-bar', Chart.Type.BAR, icons.DIAGRAM_BARS_VERTICAL, this.session.text('ui.ChartTypeBar'));
    }
    if (scout.isOneOf(Chart.Type.BAR_HORIZONTAL, supportedChartTypes)) {
      this._renderChartSelect('chart-stacked', Chart.Type.BAR_HORIZONTAL, icons.DIAGRAM_BARS_HORIZONTAL, this.session.text('ui.ChartTypeBarHorizontal'));
    }
    if (scout.isOneOf(Chart.Type.LINE, supportedChartTypes)) {
      this._renderChartSelect('chart-line', Chart.Type.LINE, icons.DIAGRAM_LINE, this.session.text('ui.ChartTypeLine'));
    }
    if (scout.isOneOf(Chart.Type.PIE, supportedChartTypes)) {
      this._renderChartSelect('chart-pie', Chart.Type.PIE, icons.DIAGRAM_PIE, this.session.text('ui.ChartTypePie'));
    }
    if (scout.isOneOf(Chart.Type.BUBBLE, supportedChartTypes)) {
      this._renderChartSelect('chart-bubble', Chart.Type.BUBBLE, icons.DIAGRAM_SCATTER, this.session.text('ui.ChartTypeBubble'));
    }
    let keyStrokeContext = new KeyStrokeContext({$bindTarget: this.$chartSelect, $scopeTarget: this.$chartSelect});
    this.session.keyStrokeManager.installKeyStrokeContext(keyStrokeContext);
    this._chartTypeTabbableCoordinator.registerKeyStrokes(this, keyStrokeContext);
    this._chartTypeTabbableCoordinator.setItems(Object.values(this._chartTypeMap));
  }

  protected _getSupportedChartTypes(): TableControlChartType[] {
    return [
      Chart.Type.BAR,
      Chart.Type.BAR_HORIZONTAL,
      Chart.Type.LINE,
      Chart.Type.PIE,
      Chart.Type.BUBBLE
    ];
  }

  protected _onChartTypeAction(event: Event<Action>) {
    const chartType = event.source.$container.data('chartType');
    this.setChartType(chartType);
  }

  protected _onChartGroupKeyDown(event: JQuery.KeyDownEvent) {
    if (scout.isOneOf(event.which, keys.ENTER, keys.SPACE)) {
      this._doChartGroupAction($(event.currentTarget));
      event.stopPropagation();
      event.preventDefault(); // Don't scroll when pressing space
    }
  }

  protected _onChartGroupClick(event: JQuery.ClickEvent) {
    this._doChartGroupAction($(event.currentTarget));
  }

  protected _doChartGroupAction($target: JQuery) {
    let groupId = $target.parent().data('groupId');
    let column = $target.data('column');
    let origModifier = $target.data('modifier');

    // do nothing when item is disabled
    if (!$target.isEnabled()) {
      return;
    }

    let modifier = $target.isSelected() ? this._nextDateModifier(origModifier) : origModifier;
    $target.data('modifier', modifier);

    let config = {
      id: column ? column.id : null,
      modifier: modifier
    };

    this._setChartGroup(groupId, config);
  }

  protected _onAggregationKeyDown(event: JQuery.KeyDownEvent) {
    if (scout.isOneOf(event.which, keys.ENTER, keys.SPACE)) {
      this._doAggregationAction($(event.currentTarget));
      event.stopPropagation();
    }
  }

  protected _onAggregationClick(event: JQuery.ClickEvent) {
    this._doAggregationAction($(event.currentTarget));
  }

  protected _doAggregationAction($target: JQuery) {
    // update modifier
    let origModifier = $target.data('modifier');
    let modifier = $target.isSelected() ? this._nextModifier(origModifier) : origModifier;
    $target.data('modifier', modifier);

    let column = $target.data('column');
    let aggregation = {
      id: column ? column.id : null,
      modifier: modifier
    };

    this._setChartAggregation(aggregation);
  }

  protected _nextDateModifier(modifier: TableMatrixDateGroup): TableMatrixDateGroup {
    switch (modifier) {
      case TableMatrix.DateGroup.DATE:
        return TableMatrix.DateGroup.MONTH;
      case TableMatrix.DateGroup.MONTH:
        return TableMatrix.DateGroup.WEEKDAY;
      case TableMatrix.DateGroup.WEEKDAY:
        return TableMatrix.DateGroup.YEAR;
      case TableMatrix.DateGroup.YEAR:
        return TableMatrix.DateGroup.DATE;
      default:
        return modifier;
    }
  }

  protected _nextModifier(modifier: TableMatrixNumberGroup): TableMatrixNumberGroup {
    switch (modifier) {
      case TableMatrix.NumberGroup.SUM:
        return TableMatrix.NumberGroup.AVG;
      case TableMatrix.NumberGroup.AVG:
        return TableMatrix.NumberGroup.SUM;
      default:
        return modifier;
    }
  }

  setChartAggregation(chartAggregation: TableControlChartAggregation) {
    this.setProperty('chartAggregation', chartAggregation);
  }

  protected _setChartAggregation(chartAggregation: TableControlChartAggregation) {
    if (chartAggregation === this.chartAggregation) {
      return;
    }
    this._setProperty('chartAggregation', chartAggregation);
    if (this.contentRendered) {
      this._renderChartAggregation();
    }
  }

  setChartGroup1(chartGroup: TableControlChartGroup) {
    this.setProperty('chartGroup1', chartGroup);
  }

  protected _setChartGroup1(chartGroup: TableControlChartGroup) {
    this._setChartGroup(1, chartGroup);
  }

  setChartGroup2(chartGroup: TableControlChartGroup) {
    this.setProperty('chartGroup2', chartGroup);
  }

  protected _setChartGroup2(chartGroup: TableControlChartGroup) {
    this._setChartGroup(2, chartGroup);
  }

  protected _setChartGroup(groupId: 1 | 2, chartGroup: TableControlChartGroup) {
    let propertyName = 'chartGroup' + groupId;
    this._changeProperty(propertyName, chartGroup);
  }

  protected _changeProperty(prop: string, value: any) {
    if (value === this[prop]) {
      return;
    }
    this._setProperty(prop, value);
    if (this.contentRendered) {
      this['_render' + prop.charAt(0).toUpperCase() + prop.slice(1)]();
    }
  }

  setChartType(chartType: TableControlChartType) {
    this.oldChartType = this.chartType;
    this.setProperty('chartType', chartType);
  }

  protected _hasColumns(): boolean {
    return this._columns().length > 0;
  }

  protected _columns(): Column[] {
    return new TableMatrix(this.table, this.session).columns();
  }

  protected _axisCount(columnCount: (number | Column<any>)[][], column: Column<any>): number {
    let tmpColumn;
    for (let i = 0; i < columnCount.length; i++) {
      tmpColumn = columnCount[i][0];
      if (tmpColumn === column) {
        return columnCount[i][1] as number;
      }
    }
    return 0;
  }

  protected _plainAxisText(column: Column<any>, text: string): string {
    if (column.headerHtmlEnabled) {
      let plainText = strings.plainText(text);
      return plainText.replace(/\n/g, ' ');
    }
    return text;
  }

  protected override _renderContent($parent: JQuery) {
    this.$contentContainer = $parent.appendDiv('chart-container')
      .attr('tabindex', '-1');
    this.$contentContainer[0].focus(); // Pressing tab should select first chart type action

    // scrollbars
    this._installScrollbars();

    this._renderChartSelectContainer();

    // group functions for dates
    this.dateGroup = [
      [TableMatrix.DateGroup.YEAR, this.session.text('ui.groupedByYear')],
      [TableMatrix.DateGroup.MONTH, this.session.text('ui.groupedByMonth')],
      [TableMatrix.DateGroup.WEEKDAY, this.session.text('ui.groupedByWeekday')],
      [TableMatrix.DateGroup.DATE, this.session.text('ui.groupedByDate')]
    ];

    // listeners
    this._filterRemovedListener = this.table.on('filterRemoved', event => {
      if (!(event.filter instanceof ChartTableUserFilter)) {
        return;
      }
      this.chart.setCheckedItems([]);
    });

    this._addListeners();

    this._renderAxisSelectorsContainer();
    let columnCount = this._renderAxisSelectors();

    // draw first chart
    this._renderChart();

    this._initializeSelection(columnCount);

    this._renderChartParts();

    this._drawChart();
  }

  protected _addListeners() {
    this.table.on('rowsInserted', this._tableUpdatedHandler);
    this.table.on('rowsDeleted', this._tableUpdatedHandler);
    this.table.on('allRowsDeleted', this._tableUpdatedHandler);
    this.chart.on('valueClick', this._chartValueClickedHandler);
  }

  protected _renderAxisSelectorsContainer() {
    this.$axisSelectContainer = this.$contentContainer
      .appendDiv('axis-select-container');
  }

  protected _renderAxisSelectors(): (number | Column<any>)[][] {
    // create container for x/y-axis
    this.$xAxisSelect = this.$axisSelectContainer
      .appendDiv('xaxis-select')
      .data('groupId', 1);
    scrollbars.install(this.$xAxisSelect, {
      parent: this,
      session: this.session,
      axis: 'y'
    });

    this.$yAxisSelect = this.$axisSelectContainer
      .appendDiv('yaxis-select')
      .data('groupId', 2);
    scrollbars.install(this.$yAxisSelect, {
      parent: this,
      session: this.session,
      axis: 'y'
    });

    // map for selection (column id, $element)
    this._chartGroup1Map = {};
    this._chartGroup2Map = {};

    // find best x- and y-axis: best is 9 different entries
    let matrix = new TableMatrix(this.table, this.session),
      columnCount = matrix.columnCount(false); // filterNumberColumns false: number columns will be filtered below
    columnCount.sort((a, b) => {
      return Math.abs(a[1] as number - 8) - Math.abs(b[1] as number - 8);
    });

    let axisCount, enabled;
    let columns = matrix.columns(false); // filterNumberColumns false: number columns will be filtered below

    // all x/y-axis for selection
    for (let c1 = 0; c1 < columns.length; c1++) {
      let column1 = columns[c1];

      // Check if data-spread is too large. This is a problem in large tables where a column has unique values.
      // We cannot create DOM elements for each unique value because this causes all browser to stop script
      // execution. May be in a later release we could implement some sort of data aggregation, but this is not
      // a simple task on the UI layer, because it requires some know-how about the entity represented by the table,
      // which we don't have in the UI. Another possible solution: make the charts scrollable, however this is
      // probably not a good idea, because with a lot of data, the chart fails to provide an oversight over the data
      // when the user must scroll and only sees a small part of the chart.
      if (column1 instanceof DateColumn) {
        // dates are always aggregated, and thus we must not check if the chart has "too much data".
        enabled = true;
      } else {
        axisCount = this._axisCount(columnCount, column1);
        enabled = (axisCount <= ChartTableControl.MAX_AXIS_COUNT);
      }

      let content = this._axisContentForColumn(column1);

      let $div = this.$contentContainer
        .makeDiv('select-axis prevent-initial-focus', this._plainAxisText(column1, content.text))
        .data('column', column1)
        .unfocusable()
        .setEnabled(enabled);

      if (!enabled) {
        if (this.chartGroup1 && this.chartGroup1.id === column1.id) {
          this.chartGroup1 = null;
          this.chartGroup2 = null;
        }
        if (this.chartGroup2 && this.chartGroup2.id === column1.id) {
          this.chartGroup2 = null;
        }
      }

      if (content.icon) {
        $div.addClass(content.icon.appendCssClass('font-icon'));
      }

      if (column1 instanceof DateColumn) {
        $div
          .data('modifier', TableMatrix.DateGroup.YEAR)
          .appendDiv('select-axis-group', String(this.dateGroup[0][1]));
      }

      // install click handler or tooltip
      if (enabled) {
        $div.on('click', this._onChartGroupClick.bind(this));
        $div.on('keydown', this._onChartGroupKeyDown.bind(this));
        tooltips.installForEllipsis($div, {
          parent: this
        });
      } else {
        tooltips.install($div, {
          parent: this,
          text: this.session.text('ui.TooMuchData')
        });
      }

      let $yDiv = $div.clone(true);
      this._chartGroup1Map[column1.id] = $div;
      this._chartGroup2Map[column1.id] = $yDiv;
      this.$xAxisSelect.append($div);
      this.$yAxisSelect.append($yDiv);
    }
    let chartGroup1KeyStrokeContext = new KeyStrokeContext({$bindTarget: this.$xAxisSelect, $scopeTarget: this.$xAxisSelect});
    this.session.keyStrokeManager.installKeyStrokeContext(chartGroup1KeyStrokeContext);
    this._chartGroup1TabbableCoordinator.registerKeyStrokes(this, chartGroup1KeyStrokeContext);
    this._chartGroup1TabbableCoordinator.setItems(this.$xAxisSelect.children('.select-axis').map((i, item) => new TabbableItem($(item))).get());

    let chartGroup2KeyStrokeContext = new KeyStrokeContext({$bindTarget: this.$yAxisSelect, $scopeTarget: this.$yAxisSelect});
    this.session.keyStrokeManager.installKeyStrokeContext(chartGroup2KeyStrokeContext);
    this._chartGroup2TabbableCoordinator.registerKeyStrokes(this, chartGroup2KeyStrokeContext);
    // Items will be set in _renderChartType

    // map for selection (column id, $element)
    this._aggregationMap = {};

    if (this._hasColumns()) {
      // create container for data
      this.$dataSelect = this.$axisSelectContainer.appendDiv('data-select');
      scrollbars.install(this.$dataSelect, {
        parent: this,
        session: this.session,
        axis: 'y'
      });

      // add data-count for no column restriction (all columns)
      let countDesc = this.session.text('ui.Count');
      this._aggregationMap.all = this.$dataSelect
        .appendDiv('select-data data-count prevent-initial-focus', countDesc)
        .unfocusable()
        .data('column', null)
        .data('modifier', TableMatrix.NumberGroup.COUNT);

      // all data for selection
      for (let c2 = 0; c2 < columns.length; c2++) {
        let column2 = columns[c2];
        let fakeNumberLabelCol2 = c2 + 1;

        if (column2 instanceof NumberColumn) {
          let columnText;
          if (strings.hasText(column2.text)) {
            columnText = this._plainAxisText(column2, column2.text);
          } else if (strings.hasText(column2.headerTooltipText)) {
            columnText = column2.headerTooltipText;
          } else {
            columnText = '[' + fakeNumberLabelCol2 + ']';
          }

          this._aggregationMap[column2.id] = this.$dataSelect
            .appendDiv('select-data data-sum prevent-initial-focus', columnText)
            .unfocusable()
            .data('column', column2)
            .data('modifier', TableMatrix.NumberGroup.SUM);
        }
      }

      // click handling for data
      $('.select-data', this.$contentContainer)
        .on('click', this._onAggregationClick.bind(this))
        .on('keydown', this._onAggregationKeyDown.bind(this));

      let keyStrokeContext = new KeyStrokeContext({$bindTarget: this.$dataSelect, $scopeTarget: this.$dataSelect});
      this.session.keyStrokeManager.installKeyStrokeContext(keyStrokeContext);
      this._aggregationTabbableCoordinator.registerKeyStrokes(this, keyStrokeContext);
      this._aggregationTabbableCoordinator.setItems(this.$dataSelect.children('.select-data').map((i, item) => new TabbableItem($(item))).get());
    }

    return columnCount;
  }

  protected _initializeSelection(columnCount: (number | Column<any>)[][]) {
    let $axisColumns;

    if (!this.chartType) {
      this.setChartType(Chart.Type.BAR);
    }

    // no id selected
    if (!this.chartAggregation || !this._aggregationMap[this.chartAggregation.id]) {
      this._setChartAggregation({
        id: null,
        modifier: TableMatrix.NumberGroup.COUNT
      });
    }

    // apply default selection
    if (!this.chartGroup1 || !this.chartGroup1.id || !this._chartGroup1Map[this.chartGroup1.id]) {
      $axisColumns = this.$xAxisSelect.children(':not(.disabled)');
      this._setDefaultSelectionForGroup(1, columnCount, $axisColumns, 0 /* only use the first column for the first group */);
    }
    if (!this.chartGroup2 || !this.chartGroup2.id || !this._chartGroup2Map[this.chartGroup2.id]) {
      $axisColumns = this.$yAxisSelect.children(':not(.disabled)');
      this._setDefaultSelectionForGroup(2, columnCount, $axisColumns, 1 /* try to use the second column for the second group (if available). Otherwise, the first column is used. */);
    }
  }

  /**
   * Applies the default column selection for the specified chartGroup.
   * The implementation only considers columns that are part of the specified columnCount matrix and $candidates array.
   * From all these columns the last match that is lower or equal to the specified maxIndex is set as default chart group.
   *
   * @param chartGroup The number of the chart group (1 or 2) for which the default column should be set.
   * @param columnCount Column-count matrix as returned by TableMatrix#columnCount(). Holds possible grouping columns.
   * @param $candidates jQuery array holding all axis columns that could be used as default.
   * @param maxIndex The maximum column index to use as default column for the specified chartGroup.
   */
  protected _setDefaultSelectionForGroup(chartGroup: 1 | 2, columnCount: (number | Column<any>)[][], $candidates: JQuery, maxIndex: number) {
    let col = this._getDefaultSelectedColumn(columnCount, $candidates, maxIndex);
    if (col) {
      this._setChartGroup(chartGroup, this._getDefaultChartGroup(col));
    }
  }

  protected _getDefaultSelectedColumn(columnCount: (number | Column<any>)[][], $candidates: JQuery, maxIndex: number): Column<any> {
    let matchCounter = 0,
      curColumn,
      result;
    for (let j = 0; j < columnCount.length && matchCounter <= maxIndex; j++) {
      curColumn = columnCount[j][0];
      if (this._existsInAxisColumns($candidates, curColumn)) {
        result = curColumn; // remember possible result
        matchCounter++;
      }
    }
    return result;
  }

  protected _existsInAxisColumns($candidates: JQuery, columnToSearch: Column<any>): boolean {
    for (let i = 0; i < $candidates.length; i++) {
      if ($candidates.eq(i).data('column') === columnToSearch) {
        return true;
      }
    }
    return false;
  }

  protected _getDefaultChartGroup(column: Column<any>): TableControlChartGroup {
    let modifier;
    if (column instanceof DateColumn) {
      modifier = 256;
    }
    return {
      id: column.id,
      modifier: modifier
    };
  }

  protected _renderChartParts() {
    this._renderChartType();
    this._renderChartAggregation();
    this._renderChartGroup1();
    this._renderChartGroup2();
  }

  protected _drawChart() {
    if (!this._hasColumns()) {
      this._hideChart();
      return;
    }

    let cube = this._calculateValues();

    if (cube && cube.length) {
      this.chart.setVisible(true);
    } else {
      this._hideChart();
      return;
    }

    let config: ChartConfig = {
      type: this.chartType,
      options: {
        handleResize: true,
        colorScheme: this.chartColorScheme,
        maxSegments: 5,
        plugins: {
          legend: {
            display: false
          }
        }
      }
    };

    let iconClasses = [];
    config.data = this._computeData(iconClasses, cube);
    this._adjustFont(config, iconClasses);

    this._adjustConfig(config);

    this.chart.setConfig(config);

    let checkedItems = this._computeCheckedItems(config.data.datasets[0].deterministicKeys);
    this.chart.setCheckedItems(checkedItems);
  }

  protected _hideChart() {
    this.chart.setConfig({
      type: this.chartType
    });
    this.chart.setVisible(false);
  }

  protected _getDatasetLabel(): string {
    let elem = this._aggregationMap[this.chartAggregation.id || 'all'];
    return (elem ? elem.text() : null) || this.session.text('ui.Value');
  }

  protected _calculateValues(): TableMatrixResult {
    // build matrix
    let matrix = new TableMatrix(this.table, this.session);

    // aggregation (data axis)
    let tableData = this.chartAggregation.id ? this._aggregationMap[this.chartAggregation.id].data('column') : -1;
    matrix.addData(tableData, this.chartAggregation.modifier);

    // find xAxis
    if (this.chartGroup1) {
      let axis = this._chartGroup1Map[this.chartGroup1.id].data('column');
      this.xAxis = matrix.addAxis(axis, this.chartGroup1.modifier);
    }

    // find yAxis
    // in case of bubble
    if (this.chartType === Chart.Type.BUBBLE && this.chartGroup2) {
      let axis2 = this._chartGroup2Map[this.chartGroup2.id].data('column');
      this.yAxis = matrix.addAxis(axis2, this.chartGroup2.modifier);
    } else {
      this.yAxis = null;
    }

    // return not possible to draw chart
    if (matrix.isEmpty() || !matrix.isMatrixValid()) {
      return;
    }

    // calculate matrix
    return matrix.calculate();
  }

  protected _getXAxis(): TableMatrixKeyAxis {
    return this.xAxis;
  }

  protected _getYAxis(): TableMatrixKeyAxis {
    return this.yAxis;
  }

  protected _computeData(iconClasses: string[], cube: TableMatrixResult): ChartData {
    let data = {
      datasets: [{
        label: this._getDatasetLabel()
      }]
    } as ChartData;
    if (!cube) {
      return data;
    }
    iconClasses = iconClasses || [];

    let segments = [];

    if (this.chartType === Chart.Type.BUBBLE) {
      segments = this._computeBubbleData(iconClasses, cube);
    } else {
      let xAxis = this._getXAxis();
      for (let x = 0; x < xAxis.length; x++) {
        let label,
          keyX = xAxis[x];
        if (xAxis.column instanceof NumberColumn) {
          // the axis will format numbers as two digit decimals and null/undefined as the text '-empty-' or something similar
          // only pass null/undefined to the axis as we want to leave the number format to the chart but need the '-empty-' string
          label = objects.isNullOrUndefined(keyX) ? xAxis.format(keyX) : keyX;
        } else {
          label = this._handleIconLabel(xAxis.format(keyX), xAxis, iconClasses);
        }
        segments.push({
          value: cube.getValue([keyX])[0],
          label: label,
          deterministicKey: xAxis.keyToDeterministicKey(keyX)
        });
      }
    }
    let dataset = data.datasets[0],
      labels = [];

    dataset.data = [];
    dataset.deterministicKeys = [];

    segments.forEach(elem => {
      dataset.data.push(elem.value);
      dataset.deterministicKeys.push(elem.deterministicKey);
      if (!objects.isNullOrUndefined(elem.label)) {
        labels.push(elem.label);
      }
    });

    if (labels.length) {
      data.labels = labels;
    }

    return data;
  }

  protected _computeBubbleData(iconClasses: string[], cube: TableMatrixResult): { value: BubbleDataPoint; deterministicKey: TableControlDeterministicKey }[] {
    if (!cube) {
      return [];
    }
    iconClasses = iconClasses || [];

    let xAxis = this._getXAxis(),
      yAxis = this._getYAxis(),
      segments = [];
    for (let x = 0; x < xAxis.length; x++) {
      let keyX = xAxis[x],
        xValue = keyX;
      this._handleIconLabel(xAxis.format(keyX), xAxis, iconClasses);
      if (!(xAxis.column instanceof NumberColumn) && xValue === null) {
        xValue = xAxis.max;
      }
      if (xAxis.column instanceof DateColumn) {
        xValue = xValue - xAxis.min;
      }
      for (let y = 0; y < yAxis.length; y++) {
        let keyY = yAxis[y],
          yValue = keyY,
          cubeValues = cube.getValue([keyX, keyY]);
        this._handleIconLabel(yAxis.format(keyY), yAxis, iconClasses);
        if (cubeValues && cubeValues.length) {
          if (!(yAxis.column instanceof NumberColumn) && yValue === null) {
            yValue = yAxis.max;
          }
          if (yAxis.column instanceof DateColumn) {
            yValue = yValue - yAxis.min;
          }
          segments.push({
            value: {
              x: xValue,
              y: yValue,
              z: cubeValues[0]
            },
            deterministicKey: [xAxis.keyToDeterministicKey(keyX), yAxis.keyToDeterministicKey(keyY)]
          });
        }
      }
    }
    return segments;
  }

  protected _handleIconLabel(label: string, axis: TableMatrixKeyAxis, iconClasses: string[]): string {
    if (axis && axis.isIcon) {
      let icon = icons.parseIconId(label);
      if (icon && icon.isFontIcon()) {
        iconClasses.push(...icon.appendCssClass('font-icon').split(' '));
        return icon.iconCharacter;
      }
    }
    return label;
  }

  protected _adjustFont(config: ChartConfig, iconClasses: string[]) {
    if (!config || !iconClasses) {
      return;
    }

    iconClasses = iconClasses.filter((value, index, self) => {
      return self.indexOf(value) === index;
    });
    if (iconClasses.length) {
      let fontFamily = styles.get(iconClasses, 'font-family').fontFamily;
      if (this.chartType !== Chart.Type.PIE) {
        config.options = $.extend(true, {}, config.options, {
          scales: {
            x: {
              ticks: {
                font: {
                  family: fontFamily
                }
              }
            },
            y: {
              ticks: {
                font: {
                  family: fontFamily
                }
              }
            }
          }
        });
      }
      config.options = $.extend(true, {}, config.options, {
        plugins: {
          tooltip: {
            titleFont: {
              family: fontFamily
            }
          }
        }
      });
      config.options = $.extend(true, {}, config.options, {
        plugins: {
          datalabels: {
            font: {
              family: fontFamily
            }
          }
        }
      });
    }
  }

  protected _adjustLabels(config: ChartConfig) {
    if (!config) {
      return;
    }

    let xAxis = this._getXAxis(),
      yAxis = this._getYAxis();
    if (this.chartType === Chart.Type.BUBBLE) {
      if (!(xAxis.column instanceof NumberColumn)) {
        config.options = $.extend(true, {}, config.options, {
          scales: {
            x: {
              ticks: {
                callback: label => this._formatLabel(label, xAxis)
              }
            }
          }
        });
      }
      if (!(yAxis.column instanceof NumberColumn)) {
        config.options = $.extend(true, {}, config.options, {
          scales: {
            y: {
              ticks: {
                callback: label => this._formatLabel(label, yAxis)
              }
            }
          }
        });
      }
    } else {
      if (xAxis.column instanceof NumberColumn) {
        config.options = $.extend(true, {}, config.options, {
          reformatLabels: true
        });
      }
    }
  }

  protected _formatLabel(label: number, axis: TableMatrixKeyAxis): string {
    if (!axis) {
      return '' + label;
    }

    if (axis.column instanceof DateColumn) {
      label = label + axis.min;
      if (label !== parseInt('' + label) || (axis.length < 2 && (label < axis.min || label > axis.max))) {
        return null;
      }
    }
    if (axis.indexOf(null) !== -1) {
      if (label === axis.max) {
        label = null;
      } else if (label > axis.max) {
        return null;
      }
    }
    let formatted = axis.format(label);
    if (axis.isIcon) {
      let icon = icons.parseIconId(formatted);
      if (icon && icon.isFontIcon()) {
        formatted = icon.iconCharacter;
      }
    }
    return formatted;
  }

  protected _adjustConfig(config: ChartConfig) {
    if (!config) {
      return;
    }

    this._adjustLabels(config);
    this._adjustClickable(config);

    if (this.chartType === Chart.Type.BUBBLE) {
      this._adjustBubble(config);
    } else if (this.chartType === Chart.Type.PIE) {
      this._adjustPie(config);
    } else {
      this._adjustScales(config);
    }
  }

  protected _adjustClickable(config: ChartConfig) {
    if (!config) {
      return;
    }

    if (this._isChartClickable()) {
      config.options = $.extend(true, {}, config.options, {
        clickable: true,
        checkable: true,
        otherSegmentClickable: true
      });
    }
  }

  protected _isChartClickable(): boolean {
    return true;
  }

  protected _adjustBubble(config: ChartConfig) {
    if (!config || this.chartType !== Chart.Type.BUBBLE) {
      return;
    }

    config.options.bubble = $.extend(true, {}, config.options.bubble, {
      sizeOfLargestBubble: 25,
      minBubbleSize: 5
    });
  }

  protected _adjustPie(config: ChartConfig) {
    if (!config || this.chartType !== Chart.Type.PIE) {
      return;
    }

    config.data.datasets[0].datalabels = {
      labels: {
        index: {
          display: 'auto',
          color: styles.get([this.chartColorScheme, this.chartType + '-chart', 'elements', 'label'], 'fill').fill,
          formatter: (value, context) => {
            return context.chart.data.labels[context.dataIndex];
          },
          anchor: 'end',
          align: 'end',
          clamp: true,
          offset: 10,
          padding: 4
        },
        labels: {}
      }
    };

    config.options = $.extend(true, {}, config.options, {
      plugins: {
        datalabels: {
          display: true
        }
      }
    });
    // Compensate the margin of the container so that the chart is always centered vertically
    let margin = this.chart.$container.cssMarginTop() - this.chart.$container.cssMarginBottom();
    config.options = $.extend(true, {}, config.options, {
      layout: {
        padding: {
          top: 30 + (Math.sign(margin) < 0 ? Math.abs(margin) : 0),
          bottom: 30 + (Math.sign(margin) > 0 ? margin : 0)
        }
      }
    });
  }

  protected _adjustScales(config: ChartConfig) {
    if (!config) {
      return;
    }

    config.options = $.extend(true, {}, config.options, {
      scales: {
        x: {
          beginAtZero: true
        },
        y: {
          beginAtZero: true
        }
      }
    });
  }

  protected _computeCheckedItems(deterministicKeys: TableControlDeterministicKey[]): ClickObject[] {
    if (!deterministicKeys) {
      return [];
    }

    let xAxis = this._getXAxis(),
      yAxis = this._getYAxis(),
      tableFilter = this.table.getFilter(ChartTableUserFilter.TYPE) as ChartTableUserFilter,
      filters = [],
      checkedIndices = [];

    if (tableFilter && (tableFilter.xAxis || {}).column === (xAxis || {}).column && (tableFilter.yAxis || {}).column === (yAxis || {}).column) {
      filters = tableFilter.filters;
    }

    deterministicKeys.forEach((deterministicKey, idx) => {
      if (filters.filter(filter => (Array.isArray(filter.deterministicKey) && Array.isArray(deterministicKey)) ? arrays.equals(filter.deterministicKey, deterministicKey) : filter.deterministicKey === deterministicKey).length) {
        checkedIndices.push(idx);
      }
    });
    let datasetIndex = 0;
    if (this.chartType === Chart.Type.PIE) {
      let maxSegments = this.chart.config.options.maxSegments,
        collapsedIndices = arrays.init(deterministicKeys.length - maxSegments, null).map((elem, idx) => idx + maxSegments);
      if (!arrays.containsAll(checkedIndices, collapsedIndices)) {
        arrays.remove(checkedIndices, maxSegments - 1);
      }
      arrays.removeAll(checkedIndices, collapsedIndices);

      // first dataset is hidden on pie charts
      datasetIndex = 1;
    }

    let checkedItems = [];
    if (checkedIndices.length) {
      checkedIndices.forEach(index => {
        checkedItems.push({
          datasetIndex: datasetIndex,
          dataIndex: index
        });
      });
    }

    return checkedItems;
  }

  protected _onChartValueClick() {
    //  prepare filter
    let filters = [];
    if (this.chart && this.chart.config.data) {
      let maxSegments = this.chart.config.options.maxSegments,
        dataset = this.chart.config.data.datasets[0],
        getFilters: (index: number) => { deterministicKey: TableControlDeterministicKey } | { deterministicKey: TableControlDeterministicKey }[] = index => ({deterministicKey: dataset.deterministicKeys[index]});
      if (this.chartType === Chart.Type.PIE) {
        getFilters = index => {
          index = parseInt('' + index);
          if (maxSegments && maxSegments === index + 1) {
            return arrays.init(dataset.deterministicKeys.length - index, null).map((elem, idx) => ({deterministicKey: dataset.deterministicKeys[idx + index]}));
          }
          return {deterministicKey: dataset.deterministicKeys[index]};
        };
      }

      let checkedIndices = this.chart.checkedItems.filter(item => item.datasetIndex === 0)
        .map(item => item.dataIndex);
      checkedIndices.forEach(index => {
        arrays.pushAll(filters, getFilters(index));
      });
    }

    //  filter function
    if (filters.length) {
      let filter = scout.create(ChartTableUserFilter, {
        session: this.session,
        table: this.table,
        text: this.tooltipText,
        xAxis: this._getXAxis(),
        yAxis: this._getYAxis(),
        filters: filters
      });

      this.table.addFilter(filter);
    } else {
      this.table.removeFilterByKey(ChartTableUserFilter.TYPE);
    }
  }

  protected _axisContentForColumn(column: Column<any>): { text: string; icon?: IconDesc } {
    let text = column.text;
    if (strings.hasText(text)) {
      return {
        text: text
      };
    }

    if (column.headerIconId) {
      let icon = icons.parseIconId(column.headerIconId);
      if (icon.isFontIcon()) {
        return {
          text: icon.iconCharacter,
          icon: icon
        };
      }
    }

    if (column.headerTooltipText) {
      return {
        text: column.headerTooltipText
      };
    }

    return {
      text: '[' + (this._columns().indexOf(column) + 1) + ']'
    };
  }

  protected override _removeContent() {
    this._removeScrollbars();
    this.$contentContainer.remove();
    this.chart.remove();
    this.table.events.removeListener(this._filterRemovedListener);
    this._removeListeners();
    this.oldChartType = null;
    this.recomputeEnabled();
  }

  protected _removeScrollbars() {
    this.$xAxisSelect.each((index, element) => {
      tooltips.uninstall($(element));
    });
    scrollbars.uninstall(this.$xAxisSelect, this.session);
    this.$yAxisSelect.each((index, element) => {
      tooltips.uninstall($(element));
    });
    scrollbars.uninstall(this.$yAxisSelect, this.session);
    scrollbars.uninstall(this.$dataSelect, this.session);
    this._uninstallScrollbars();
  }

  protected _removeListeners() {
    this.table.off('rowsInserted', this._tableUpdatedHandler);
    this.table.off('rowsDeleted', this._tableUpdatedHandler);
    this.table.off('allRowsDeleted', this._tableUpdatedHandler);
    this.chart.off('valueClick', this._chartValueClickedHandler);
  }

  protected _onTableUpdated(event?: Event<Table>) {
    if (this._tableUpdatedTimeOutId) {
      return;
    }

    this._tableUpdatedTimeOutId = setTimeout(() => {
      this._tableUpdatedTimeOutId = null;

      if (!this.rendered) {
        return;
      }

      this._setChartGroup1(null);
      this._setChartGroup2(null);
      this.removeContent();
      this.renderContent();
    });
  }

  protected _onTableColumnStructureChanged() {
    this.recomputeEnabled();
    if (this.contentRendered && this.selected) {
      this._onTableUpdated();
    }
  }
}

export type TableControlChartType = typeof Chart.Type['BAR' | 'BAR_HORIZONTAL' | 'LINE' | 'PIE' | 'BUBBLE'];

export type TableControlChartAggregation = {
  id?: string;
  modifier?: TableMatrixNumberGroup;
};

export type TableControlChartGroup = {
  id?: string;
  modifier?: TableMatrixNumberGroup | TableMatrixDateGroup;
};

export type TableControlDeterministicKey = (number | string) | (number | string)[];

// extend chart.js
declare module 'chart.js' {
  interface ChartDatasetProperties<TType extends ChartJsType, TData> {
    deterministicKeys?: TableControlDeterministicKey[];
  }
}
