content: package ru.dimension.ui.component.chart.realtime;
  
  import static ru.dimension.ui.component.chart.RangeUtils.getSubRangeSize;
  
  import java.awt.BorderLayout;
  import java.text.SimpleDateFormat;
  import java.util.ArrayList;
  import java.util.Date;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.LinkedHashSet;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  import java.util.concurrent.ExecutorService;
  import java.util.concurrent.Executors;
  import java.util.function.Consumer;
  import java.util.regex.PatternSyntaxException;
  import javax.swing.JPanel;
  import javax.swing.JTextField;
  import javax.swing.RowFilter;
  import javax.swing.event.DocumentEvent;
  import javax.swing.event.DocumentListener;
  import javax.swing.table.TableColumn;
  import javax.swing.table.TableRowSorter;
  import lombok.extern.log4j.Log4j2;
  import ru.dimension.db.core.DStore;
  import ru.dimension.db.exception.BeginEndWrongOrderException;
  import ru.dimension.db.exception.SqlColMetadataException;
  import ru.dimension.db.model.OrderBy;
  import ru.dimension.db.model.filter.CompositeFilter;
  import ru.dimension.db.model.output.StackedColumn;
  import ru.dimension.db.model.profile.CProfile;
  import ru.dimension.ui.component.chart.ChartConfig;
  import ru.dimension.ui.component.chart.RangeUtils;
  import ru.dimension.ui.component.chart.RangeUtils.TimeRange;
  import ru.dimension.ui.component.module.analyze.handler.TableSelectionHandler;
  import ru.dimension.ui.helper.FilterHelper;
  import ru.dimension.ui.helper.GUIHelper;
  import ru.dimension.ui.helper.ProgressBarHelper;
  import ru.dimension.ui.model.ProfileTaskQueryKey;
  import ru.dimension.ui.model.chart.ChartRange;
  import ru.dimension.ui.model.column.ColumnNames;
  import ru.dimension.ui.model.config.Metric;
  import ru.dimension.ui.model.chart.ChartType;
  import ru.dimension.ui.model.function.GroupFunction;
  import ru.dimension.ui.model.info.QueryInfo;
  import ru.dimension.ui.model.info.gui.ChartInfo;
  import ru.dimension.ui.model.view.SeriesType;
  import ru.dimension.ui.state.SqlQueryState;
  
  @Log4j2
  public class ServerRealtimeSCP extends RealtimeSCP {
  
    private final QueryInfo queryInfo;
    private final ChartInfo chartInfo;
    private final Metric metric;
  
    protected ExecutorService executorService = Executors.newSingleThreadExecutor();
  
    protected Map<CProfile, LinkedHashSet<String>> topMapSelected;
  
    private boolean noDataFound = false;
    private TimeRange currentTimeRange = new TimeRange(0, 0);
  
    private JTextField seriesSearch;
    private TableRowSorter<?> seriesSorter;
  
    public ServerRealtimeSCP(SqlQueryState sqlQueryState,
                             DStore dStore,
                             ChartConfig config,
                             ProfileTaskQueryKey profileTaskQueryKey) {
      this(sqlQueryState, dStore, config, profileTaskQueryKey, null);
    }
  
    public ServerRealtimeSCP(SqlQueryState sqlQueryState,
                             DStore dStore,
                             ChartConfig config,
                             ProfileTaskQueryKey profileTaskQueryKey,
                             Map<CProfile, LinkedHashSet<String>> topMapSelected) {
      super(config, profileTaskQueryKey);
  
      super.setSqlQueryState(sqlQueryState);
      super.setDStore(dStore);
  
      this.chartInfo = config.getChartInfo();
      this.queryInfo = config.getQueryInfo();
      this.metric = config.getMetric();
  
      this.dataHandler = initFunctionDataHandler(profileTaskQueryKey, metric, queryInfo, dStore);
  
      if (topMapSelected != null) {
        if (topMapSelected.values().stream().allMatch(LinkedHashSet::isEmpty)) {
          this.topMapSelected = null;
          this.dataHandler.setFilter(null);
        } else {
          this.topMapSelected = topMapSelected;
          this.dataHandler.setFilter(topMapSelected);
        }
      }
    }
  
    @Override
    public void initialize() {
      log.info(getHourMinSec(queryInfo.getDeltaLocalServerTime()));
  
      disablePlotUpdates();
  
      long serverTimeMillis = System.currentTimeMillis() - queryInfo.getDeltaLocalServerTime();
      long rangeStart = serverTimeMillis - (chartInfo.getRangeRealtime().getMinutes() * 60 * 1000L);
  
      ChartRange chartRange = new ChartRange(rangeStart, serverTimeMillis);
  
      try {
        if (GroupFunction.COUNT.equals(metric.getGroupFunction())) {
          List<String> distinct = getDistinct(dStore,
                                              queryInfo.getName(),
                                              metric.getYAxis(),
                                              chartRange,
                                              MAX_SERIES);
  
          boolean useCustom = distinct.size() > THRESHOLD_SERIES;
  
          if (useCustom) {
            seriesType = SeriesType.CUSTOM;
  
            int showSeries = SHOW_SERIES;
  
            initializeSeriesSelectableTable(distinct, showSeries);
  
            List<String> checkBoxSelected = getCheckBoxSelected();
            this.topMapSelected = new HashMap<>() {{
              put(config.getMetric().getYAxis(), new LinkedHashSet<>(checkBoxSelected));
            }};
            this.dataHandler.setFilter(topMapSelected);
  
            series.clear();
            series.addAll(checkBoxSelected);
          } else {
            seriesType = SeriesType.COMMON;
            series.addAll(distinct);
          }
        } else {
          seriesType = SeriesType.COMMON;
          series.add(metric.getYAxis().getColName());
        }
      } catch (BeginEndWrongOrderException e) {
        log.error("Error fetching distinct series", e);
      }
  
      if (seriesType == SeriesType.CUSTOM) {
        initializeWithSeriesTable();
      } else {
        initializeGUI();
      }
  
      try {
        initializeChartData(rangeStart, serverTimeMillis);
      } catch (SqlColMetadataException | BeginEndWrongOrderException e) {
        log.error(e);
        throw new RuntimeException(e);
      }
  
      enablePlotUpdates();
    }
  
  
    protected void initializeSeriesSelectableTable(List<String> distinct, int showSeries) {
      Consumer<String> runAction = this::runAction;
      String[] columnNames = {ColumnNames.NAME.getColName(), ColumnNames.PICK.getColName()};
      int nameId = 0;
      int checkBoxId = 1;
  
      this.seriesSelectable = GUIHelper.getJXTableCaseCheckBoxAdHoc(10, columnNames, checkBoxId);
      TableColumn colMetric = seriesSelectable.getJxTable().getColumnModel().getColumn(checkBoxId);
      colMetric.setMinWidth(40);
      colMetric.setMaxWidth(45);
  
      TableSelectionHandler handler = new TableSelectionHandler(seriesSelectable,
                                                                ColumnNames.NAME.getColName(), runAction);
      handler.initializeTableModelListener(nameId, checkBoxId);
  
      seriesSelectable.setBlockRunAction(true);
      distinct.forEach(seriesName ->
                           seriesSelectable.getDefaultTableModel().addRow(new Object[]{seriesName, false})
      );
      for (int i = 0; i < Math.min(showSeries, distinct.size()); i++) {
        seriesSelectable.getDefaultTableModel().setValueAt(true, i, 1);
      }
      seriesSelectable.setBlockRunAction(false);
  
      // Initialize row sorter for search
      seriesSorter = new TableRowSorter<>(seriesSelectable.getJxTable().getModel());
      seriesSelectable.getJxTable().setRowSorter(seriesSorter);
    }
  
    protected void setCustomFilter() {
      if (seriesType == SeriesType.CUSTOM) {
        Set<String> newSelection = new HashSet<>(getCheckBoxSelected());
  
        if (topMapSelected != null) {
          topMapSelected.clear();
          topMapSelected.putIfAbsent(metric.getYAxis(), new LinkedHashSet<>(newSelection));
        } else {
          topMapSelected = new HashMap<>() {{
            put(metric.getYAxis(), new LinkedHashSet<>(newSelection));
          }};
        }
  
        dataHandler.setFilter(topMapSelected);
      }
    }
  
    protected void runAction(String seriesName) {
      if (seriesSelectable.isBlockRunAction()) {
        return;
      }
  
      executorService.submit(() -> {
        try {
          seriesSelectable.setBlockRunAction(true);
          seriesSelectable.getJxTable().setEnabled(false);
  
          disablePlotUpdates();
  
          showProgressBar();
  
          Set<String> newSelection = new HashSet<>(getCheckBoxSelected());
  
          if (!newSelection.equals(series)) {
            clearSeriesColor();
  
            series.clear();
            series.addAll(newSelection);
  
            setCustomFilter();
  
            if (detailAndAnalyzeHolder != null) {
              detailAndAnalyzeHolder.detailAction().cleanMainPanel();
            }
  
            reloadDataForCurrentRange();
  
            if (detailAndAnalyzeHolder != null) {
              detailAndAnalyzeHolder.detailAction().cleanMainPanel();
              detailAndAnalyzeHolder.customAction().setCustomSeriesFilter(topMapSelected, getSeriesColorMap());
            }
          }
          showChart();
        } finally {
          seriesSelectable.setBlockRunAction(false);
          seriesSelectable.getJxTable().setEnabled(true);
  
          enablePlotUpdates();
        }
      });
    }
  
    protected List<String> getCheckBoxSelected() {
      List<String> selected = new ArrayList<>();
      int modelRowCount = seriesSelectable.getDefaultTableModel().getRowCount();
      for (int modelRow = 0; modelRow < modelRowCount; modelRow++) {
        if ((Boolean) seriesSelectable.getDefaultTableModel().getValueAt(modelRow, 1)) {
          selected.add((String) seriesSelectable.getDefaultTableModel().getValueAt(modelRow, 0));
        }
      }
      return selected;
    }
  
  
    protected void initializeWithSeriesTable() {
      this.setLayout(new BorderLayout());
      this.add(stackedChart.getChartPanel(), BorderLayout.CENTER);
  
      JPanel eastPanel = new JPanel(new BorderLayout());
  
      // Initialize search field
      seriesSearch = new JTextField();
      seriesSearch.getDocument().addDocumentListener(new DocumentListener() {
        @Override
        public void insertUpdate(DocumentEvent e) {
          updateSeriesFilter();
        }
  
        @Override
        public void removeUpdate(DocumentEvent e) {
          updateSeriesFilter();
        }
  
        @Override
        public void changedUpdate(DocumentEvent e) {
          updateSeriesFilter();
        }
      });
      seriesSearch.setToolTipText("Search...");
  
      eastPanel.add(seriesSearch, BorderLayout.NORTH);
      eastPanel.add(seriesSelectable.getJScrollPane(), BorderLayout.CENTER);
  
      this.add(eastPanel, BorderLayout.EAST);
  
      this.revalidate();
      this.repaint();
    }
  
    private void updateSeriesFilter() {
      String searchText = seriesSearch.getText();
  
      if (seriesSelectable == null) return;
  
      if (seriesSorter == null) {
        seriesSorter = new TableRowSorter<>(seriesSelectable.getJxTable().getModel());
        seriesSelectable.getJxTable().setRowSorter(seriesSorter);
      }
  
      if (searchText == null || searchText.isEmpty()) {
        seriesSorter.setRowFilter(null);
      } else {
        try {
          seriesSorter.setRowFilter(RowFilter.regexFilter("(?iu)" + searchText, 0));
        } catch (PatternSyntaxException e) {
          seriesSorter.setRowFilter(RowFilter.regexFilter("$^", 0));
        }
      }
    }
  
    @Override
    protected void reloadDataForCurrentRange() {
      try {
        long serverTimeMillis = System.currentTimeMillis() - queryInfo.getDeltaLocalServerTime();
        long rangeStart = serverTimeMillis - (chartInfo.getRangeRealtime().getMinutes() * 60 * 1000L);
  
        stackedChart.deleteAllSeriesData(chartInfo.getRangeRealtime().getMinutes());
  
        List<StackedColumn> sColumnList = dataHandler.handleFunctionComplex(rangeStart, serverTimeMillis);
        fillSeriesAnalyze(sColumnList);
  
        if (!series.isEmpty()) {
          fillChart(serverTimeMillis, chartInfo.getRangeRealtime().getMinutes());
        }
      } catch (Exception e) {
        log.error("Error reloading data", e);
      }
    }
  
    @Override
    public Map<CProfile, LinkedHashSet<String>> getFilter() {
      return topMapSelected;
    }
  
    public void reinitializeChartInCustomMode() {
      this.removeAll();
  
      series.clear();
      clearSeriesColor();
  
      seriesType = SeriesType.CUSTOM;
  
      disablePlotUpdates();
  
      try {
        long serverTimeMillis = System.currentTimeMillis() - queryInfo.getDeltaLocalServerTime();
        long rangeStart = serverTimeMillis - (chartInfo.getRangeRealtime().getMinutes() * 60 * 1000L);
        ChartRange chartRange = new ChartRange(rangeStart, serverTimeMillis);
  
        List<String> distinct = fetchDistinctSeries(chartRange);
  
        int showSeries = SHOW_SERIES;
        if (topMapSelected != null && topMapSelected.containsKey(metric.getYAxis())) {
          List<String> combined = new ArrayList<>(topMapSelected.get(metric.getYAxis()));
          for (String value : distinct) {
            if (!combined.contains(value)) {
              combined.add(value);
            }
          }
          distinct = combined;
          showSeries = topMapSelected.get(metric.getYAxis()).size();
        }
  
        initializeSeriesSelectableTable(distinct, showSeries);
  
        series.clear();
        series.addAll(getCheckBoxSelected());
  
        setCustomFilter();
  
        initializeWithSeriesTable();
  
        if (detailAndAnalyzeHolder != null) {
          detailAndAnalyzeHolder.detailAction().cleanMainPanel();
        }
  
        reloadDataForCurrentRange();
  
        if (detailAndAnalyzeHolder != null) {
          detailAndAnalyzeHolder.customAction().setCustomSeriesFilter(topMapSelected, getSeriesColorMap());
        }
      } catch (BeginEndWrongOrderException e) {
        log.error("Error reinitializing chart", e);
      } finally {
        enablePlotUpdates();
      }
    }
  
    private List<String> fetchDistinctSeries(ChartRange chartRange) throws BeginEndWrongOrderException {
      if (topMapSelected != null) {
        CompositeFilter compositeFilter = FilterHelper.toCompositeFilter(topMapSelected);
  
        return dStore.getDistinct(
            queryInfo.getName(),
            metric.getYAxis(),
            OrderBy.DESC,
            compositeFilter,
            MAX_SERIES,
            chartRange.getBegin(),
            chartRange.getEnd());
      } else {
        return dStore.getDistinct(
            queryInfo.getName(),
            metric.getYAxis(),
            OrderBy.DESC,
            null,
            MAX_SERIES,
            chartRange.getBegin(),
            chartRange.getEnd()
        );
      }
    }
  
    public SeriesType getSeriesType() {
      return seriesType;
    }
  
    public JTextField getSeriesSearch() {
      return seriesSearch;
    }
  
    private void initializeChartData(long rangeStart, long serverTimeMillis)
        throws SqlColMetadataException, BeginEndWrongOrderException {
      List<StackedColumn> sColumnList = dataHandler.handleFunctionComplex(rangeStart, serverTimeMillis);
      fillSeriesAnalyze(sColumnList);
  
      if (series.isEmpty()) {
        handleEmptySeriesCase(rangeStart, serverTimeMillis);
      } else {
        sColumnList = dataHandler.handleFunctionComplex(rangeStart, serverTimeMillis);
        fillSeriesAnalyze(sColumnList);
        fillChart(serverTimeMillis, chartInfo.getRangeRealtime().getMinutes());
      }
    }
  
    private void handleEmptySeriesCase(long rangeStart, long serverTimeMillis)
        throws SqlColMetadataException, BeginEndWrongOrderException {
      long last = dStore.getLast(queryInfo.getName(), Long.MIN_VALUE, Long.MAX_VALUE);
  
      if (last != 0) {
        log.info("Get data from history");
        if (last < rangeStart) {
          List<StackedColumn> sColumnList = dataHandler.handleFunctionComplex(last - getRangeRealTime(chartInfo), last);
          fillSeriesAnalyze(sColumnList);
          fillChartEmpty(serverTimeMillis, chartInfo.getRangeRealtime().getMinutes());
        }
      } else {
        noDataFound = true;
  
        currentTimeRange = new TimeRange(serverTimeMillis - chartInfo.getPullTimeoutClient() * 1000L, serverTimeMillis);
        log.info("No data found in history");
      }
    }
  
    protected void fillChart(long serverTimeMillis, int rangeMinutes) {
      processTimeRanges(serverTimeMillis, rangeMinutes, false);
    }
  
    protected void fillChartEmpty(long serverTimeMillis,
                                  int rangeMinutes) {
      List<TimeRange> ranges = RangeUtils.calculateRanges(serverTimeMillis, rangeMinutes);
  
      for (TimeRange range : ranges) {
        if (!updateCurrentTimeRange(range)) {
          break;
        }
  
        if (ChartType.LINEAR.equals(metric.getChartType())) {
          stackedChart.addSeriesValue(range.getStart(), 0.0, metric.getYAxis().getColName());
        } else {
          addZeroValuesForAllSeries(range.getStart());
        }
  
        if (sqlQueryState.getLastTimestamp(profileTaskQueryKey) == range.getEnd()) {
          break;
        }
      }
    }
  
    @Override
    public void loadData() {
      disablePlotUpdates();
  
      try {
        int subRangeSize = getSubRangeSize(chartInfo.getRangeRealtime().getMinutes());
        long serverTimeMillis = System.currentTimeMillis() - queryInfo.getDeltaLocalServerTime();
        long startRange = currentTimeRange.getEnd() + 1;
  
        if (noDataFound) {
          long last = dStore.getLast(queryInfo.getName(), startRange, serverTimeMillis);
  
          if (last != 0) {
            List<StackedColumn> sColumnList;
            try {
              sColumnList = dataHandler.handleFunctionComplex(serverTimeMillis - getRangeRealTime(chartInfo), serverTimeMillis);
            } catch (BeginEndWrongOrderException | SqlColMetadataException e) {
              throw new RuntimeException(e);
            }
            fillSeriesAnalyze(sColumnList);
            fillChartEmpty(currentTimeRange.getStart(), chartInfo.getRangeRealtime().getMinutes());
  
            noDataFound = false;
          } else {
            noDataFound = true;
  
            currentTimeRange = new TimeRange(serverTimeMillis - chartInfo.getPullTimeoutClient() * 1000L, serverTimeMillis);
            log.info("No data found");
          }
        }
  
        while (startRange < sqlQueryState.getLastTimestamp(profileTaskQueryKey)) {
          long endRange = startRange + subRangeSize;
          TimeRange range = new TimeRange(startRange, endRange);
  
          if (!updateCurrentTimeRange(range)) break;
  
          double k = chartInfo.getRangeRealtime().getMinutes();
  
          boolean applyFilter = topMapSelected != null
              && !topMapSelected.isEmpty()
              && topMapSelected.values().stream().anyMatch(set -> !set.isEmpty());
  
          if (applyFilter) {
            dataHandler.handleFunction(range.getStart(), range.getEnd(), k, series, topMapSelected, stackedChart);
          } else {
            dataHandler.handleFunction(range.getStart(), range.getEnd(), false, range.getStart(), k, series, stackedChart);
          }
  
          startRange = endRange + 1;
  
          if (sqlQueryState.getLastTimestamp(profileTaskQueryKey) == range.getEnd()) {
            break;
          }
        }
  
        stackedChart.deleteAllSeriesData(chartInfo.getRangeRealtime().getMinutes());
      } finally {
        enablePlotUpdates();
      }
    }
  
    private void processTimeRanges(long serverTimeMillis, int rangeMinutes, boolean fillEmpty) {
      List<TimeRange> ranges = RangeUtils.calculateRanges(serverTimeMillis, rangeMinutes);
      double k = rangeMinutes;
  
      for (TimeRange range : ranges) {
        if (!updateCurrentTimeRange(range)) break;
  
        if (fillEmpty) {
          if (ChartType.LINEAR.equals(metric.getChartType())) {
            stackedChart.addSeriesValue(range.getStart(), 0.0, metric.getYAxis().getColName());
          } else {
            addZeroValuesForAllSeries(range.getStart());
          }
        } else {
          boolean applyFilter = topMapSelected != null
              && !topMapSelected.isEmpty()
              && topMapSelected.values().stream().anyMatch(set -> !set.isEmpty());
  
          if (applyFilter) {
            dataHandler.handleFunction(range.getStart(), range.getEnd(), k, series, topMapSelected, stackedChart);
          } else {
            dataHandler.handleFunction(range.getStart(), range.getEnd(), false, range.getStart(), k, series, stackedChart);
          }
        }
  
        if (sqlQueryState.getLastTimestamp(profileTaskQueryKey) == range.getEnd()) {
          break;
        }
      }
    }
  
    private boolean updateCurrentTimeRange(TimeRange range) {
      if (sqlQueryState.getLastTimestamp(profileTaskQueryKey) >= range.getEnd()) {
        this.currentTimeRange = range;
        return true;
      }
      return false;
    }
  
    private void addZeroValuesForAllSeries(long xValue) {
      series.forEach(seriesName -> {
        try {
          stackedChart.addSeriesValue(xValue, 0.0, seriesName);
        } catch (Exception exception) {
          log.info(exception);
        }
      });
    }
  
    private void showProgressBar() {
      BorderLayout layout = (BorderLayout) getLayout();
      if (layout.getLayoutComponent(BorderLayout.CENTER) != null) {
        remove(layout.getLayoutComponent(BorderLayout.CENTER));
      }
      add(ProgressBarHelper.createProgressBar("Loading, please wait..."), BorderLayout.CENTER);
      revalidate();
      repaint();
    }
  
    private void showChart() {
      BorderLayout layout = (BorderLayout) getLayout();
      if (layout.getLayoutComponent(BorderLayout.CENTER) != null) {
        remove(layout.getLayoutComponent(BorderLayout.CENTER));
      }
      add(stackedChart.getChartPanel(), BorderLayout.CENTER);
      revalidate();
      repaint();
    }
  }