package ervu_dashboard.component.chart;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.stream.Collectors;

import component.chart.dto.SingleChartDataDto;
import component.chart.dto.SingleChartDataSetDto;
import component.chart.service.SingleChartDataSetService;
import component.chart.service.impl.AbstractChartDatasetService;
import ervu_dashboard.component.chart.label.RoundLabelConfiguration;
import ervu_dashboard.component.filter.FilterReferences;
import ervu_dashboard.model.chart.round.RoundAggregationData;
import ervu_dashboard.model.chart.round.RoundChartColumnSort;
import ervu_dashboard.model.chart.round.RoundChartDataDto;
import ervu_dashboard.model.chart.round.RoundChartDataSetConfiguration;
import ervu_dashboard.model.chart.round.RoundChartDataSetDto;
import ervu_dashboard.model.chart.round.RoundChartDataSetDtoWrapper;
import ervu_dashboard.model.chart.round.RoundColumnAggregationDataSet;
import ervu_dashboard.model.chart.round.RoundStaticData;
import ervu_dashboard.model.chart.round.label.ChartLabelModel;
import model.Filter;
import model.FilterModel;

import ru.cg.webbpm.modules.database.api.bean.TableRow;
import ru.cg.webbpm.modules.database.api.dao.option.LoadOptions;
import ru.cg.webbpm.modules.database.bean.entity_graph.AggregateFuncField;
import ru.cg.webbpm.modules.database.bean.entity_graph.EntityColumn;
import ru.cg.webbpm.modules.database.bean.filter.EntityFilter;
import ru.cg.webbpm.modules.standard_annotations.validation.NotNull;

import static java.lang.Runtime.getRuntime;
import static java.util.Arrays.stream;
import static java.util.concurrent.TimeUnit.MILLISECONDS;

/**
 * @author Vitaly Chekushkin
 */
public class RoundSingleChartDataSetService extends AbstractChartDatasetService
    implements SingleChartDataSetService {

  @NotNull
  public RoundChartDataSetConfiguration[] dataSetConfigurations;

  public RoundLabelConfiguration[] centerLabelConfigurations = new RoundLabelConfiguration[0];

  private FilterReferences filterReferences;

  @Override
  protected void start() {
    super.start();
    this.filterReferences = getScript(FilterReferences.class);
  }

  @Override
  public SingleChartDataDto loadData(Integer offset, Integer limit, Filter[] filters) {
    int availableProcessors = getRuntime().availableProcessors();
    ExecutorService executorService = new ThreadPoolExecutor(0, availableProcessors,
        0L, MILLISECONDS, new LinkedBlockingQueue<Runnable>()
    );

    List<Callable<RoundChartDataSetDtoWrapper>> dataSetTasks = new ArrayList<>();
    stream(dataSetConfigurations).forEach(dataSetConfiguration ->
        dataSetTasks.add(() ->
            loadData(dataSetConfiguration, loadOptions(offset, limit, filters))
        )
    );

    List<Callable<ChartLabelModel>> labelTasks = new ArrayList<>();
    stream(centerLabelConfigurations).forEach(labelConfiguration ->
        labelTasks.add(() ->
            labelConfiguration.getLabelModel(loadOptions(offset, limit, filters))
        )
    );

    try {
      List<Future<RoundChartDataSetDtoWrapper>> dataSetFutures = executorService.invokeAll(dataSetTasks);
      List<Future<ChartLabelModel>> labelFutures = executorService.invokeAll(labelTasks);

      List<SingleChartDataSetDto> datasets = new ArrayList<>();
      List<ChartLabelModel> centerLabelModels = new ArrayList<>();
      List<Object> labels = new ArrayList<>();

      for (Future<RoundChartDataSetDtoWrapper> future : dataSetFutures) {
        RoundChartDataSetDtoWrapper chartDataSetDto = future.get();

        if (chartDataSetDto.getRoundChartDataSetDto() != null) {
          datasets.add(chartDataSetDto.getRoundChartDataSetDto());
        }
        labels.addAll(chartDataSetDto.getLabels());
      }
      executorService.shutdown();

      for (Future<ChartLabelModel> future : labelFutures) {
        centerLabelModels.add(future.get());
      }

      return new RoundChartDataDto(datasets, labels, centerLabelModels);
    }
    catch (InterruptedException | ExecutionException e) {
      executorService.shutdownNow();
      throw new RuntimeException(e);
    }
  }

  protected RoundChartDataSetDtoWrapper loadData(
      RoundChartDataSetConfiguration dataSetConfiguration, LoadOptions loadOptions) {
    return switch (dataSetConfiguration.datasetType) {
      case AGGREGATION -> loadAggregationDataSet(dataSetConfiguration, loadOptions);
      case STATIC -> loadStaticDataSet(dataSetConfiguration, loadOptions);
      case COLUMN_AGGREGATION -> loadColumnAggregationDataSet(dataSetConfiguration, loadOptions);
    };
  }

  protected RoundChartDataSetDtoWrapper loadAggregationDataSet(
      RoundChartDataSetConfiguration dataSetConfiguration, LoadOptions loadOptions) {

    addSorting(dataSetConfiguration.aggregationDataSet.columnSorts, loadOptions);
    EntityColumn[] groupByColumnsArray = dataSetConfiguration.aggregationDataSet.groupByColumns;
    Set<EntityColumn> groupByColumns = stream(groupByColumnsArray).collect(Collectors.toSet());
    EntityColumn labelColumn = dataSetConfiguration.aggregationDataSet.labelColumn != null
                               ? dataSetConfiguration.aggregationDataSet.labelColumn
                               : groupByColumnsArray[groupByColumnsArray.length - 1];

    if (!groupByColumns.contains(labelColumn)) {
      throw new IllegalStateException(
          "Label column " + labelColumn + " must be in groupColumns array");
    }
    RoundAggregationData aggregationData = dataSetConfiguration.aggregationDataSet.aggregationData;
    AggregateFuncField funcField = new AggregateFuncField(
        aggregationData.aggregationColumn, aggregationData.aggregationFunction);
    Set<AggregateFuncField> aggregateFuncFields = Collections.singleton(funcField);
    List<Object> labels = new ArrayList<>();
    List<Object> data = new ArrayList<>();

    List<TableRow> rows = dataSetConfiguration.loadDao.loadAggregations(
        groupByColumns,
        aggregateFuncFields,
        loadOptions
    );

    rows.forEach(tableRow -> {
      labels.add(getAxesValue(tableRow, labelColumn,
          dataSetConfiguration.aggregationDataSet.labelColumFormatter
      ));
      data.add(getAxesValue(tableRow, aggregationData.aggregationColumn,
          aggregationData.aggregationColumnFormatter
      ));
    });

    String[] backgroundColors = generateBackgroundColors(dataSetConfiguration.backgroundColors,
        data
    );
    RoundChartDataSetDto singleChartDataSetDto = new RoundChartDataSetDto(
        aggregationData.dataLabel,
        data,
        backgroundColors,
        dataSetConfiguration.borderWidth,
        dataSetConfiguration.radius,
        dataSetConfiguration.cutout,
        dataSetConfiguration.hoverOffset
    );

    return new RoundChartDataSetDtoWrapper(singleChartDataSetDto, labels);
  }

  protected RoundChartDataSetDtoWrapper loadStaticDataSet(
      RoundChartDataSetConfiguration dataSetConfiguration, LoadOptions loadOptions) {

    addSorting(dataSetConfiguration.staticDataSet.columnSorts, loadOptions);
    RoundStaticData staticData = dataSetConfiguration.staticDataSet.staticData;
    Set<EntityColumn> columns = new HashSet<>(staticData.columns());
    List<Object> labels = new ArrayList<>();
    List<Object> data = new ArrayList<>();

    dataSetConfiguration.loadDao.load(columns, loadOptions)
        .forEach(tableRow -> {
          labels.add(getAxesValue(tableRow, staticData.labelColumn,
              staticData.labelColumnFormatter
          ));
          data.add(getAxesValue(tableRow, staticData.dataColumn,
              staticData.dataColumnFormatter
          ));
        });

    String[] backgroundColors = generateBackgroundColors(dataSetConfiguration.backgroundColors,
        data
    );

    RoundChartDataSetDto singleChartDataSetDto = new RoundChartDataSetDto(
        staticData.dataLabel,
        data,
        backgroundColors,
        dataSetConfiguration.borderWidth,
        dataSetConfiguration.radius,
        dataSetConfiguration.cutout,
        dataSetConfiguration.hoverOffset
    );

    return new RoundChartDataSetDtoWrapper(singleChartDataSetDto, labels);
  }

  protected RoundChartDataSetDtoWrapper loadColumnAggregationDataSet(
      RoundChartDataSetConfiguration dataSetConfiguration, LoadOptions loadOptions) {
    RoundColumnAggregationDataSet dataSet = dataSetConfiguration.columnAggregationDataSet;

    Set<AggregateFuncField> aggregateFuncFields = stream(dataSet.aggregationData)
        .map(aggData -> new AggregateFuncField(aggData.aggregationColumn,
            aggData.aggregationFunction
        ))
        .collect(LinkedHashSet::new, Set::add, Set::addAll);

    TableRow tableRow = Optional.ofNullable(dataSetConfiguration.loadDao.loadAggregations(
            aggregateFuncFields,
            loadOptions
    )).orElse(new TableRow());

    List<Object> labels = new ArrayList<>();
    List<Object> data = new ArrayList<>();
    List<String> backgroundColors = new ArrayList<>();

    stream(dataSet.aggregationData).forEach(aggData -> {
      Object formattedValue = getAxesValue(
          tableRow, aggData.aggregationColumn, aggData.aggregationColumnFormatter
      );

      if (formattedValue == null) return;

      labels.add(aggData.label);
      data.add(formattedValue);
      backgroundColors.add(aggData.backgroundColor);
    });

    RoundChartDataSetDto singleChartDataSetDto = new RoundChartDataSetDto(
        dataSet.dataLabel,
        data,
        backgroundColors.toArray(new String[0]),
        dataSetConfiguration.borderWidth,
        dataSetConfiguration.radius,
        dataSetConfiguration.cutout,
        dataSetConfiguration.hoverOffset
    );

    return new RoundChartDataSetDtoWrapper(singleChartDataSetDto, labels);
  }

  protected void addSorting(RoundChartColumnSort[] aggregationDataSet, LoadOptions loadOptions) {
    for (RoundChartColumnSort columnSort : aggregationDataSet) {
      loadOptions.addSortField(columnSort.field, columnSort.sortOrder);
    }
  }

  @Override
  protected List<EntityFilter> convertFilterModels(Filter filter) {
    return Arrays.stream(filter.filterModels)
        .map(filterModel -> getComponentFilter(filter.componentGuid, filterModel))
        .collect(Collectors.toList());
  }

  protected LoadOptions loadOptions(Integer offset, Integer limit, Filter[] filters) {
    LoadOptions loadOptions = new LoadOptions();
    loadOptions.setOffset(offset);
    loadOptions.setLimit(limit);
    loadOptions.setEntityFilterGroup(getEntityFilterGroup(filters));
    return loadOptions;
  }

  protected String[] generateBackgroundColors(String[] backgroundColors, List<Object> data) {
    String[] colors;

    if (backgroundColors.length > 0) {

      if (backgroundColors.length >= data.size()) {
        colors = backgroundColors;
      }
      else {
        Set<String> definedColors = stream(backgroundColors).collect(Collectors.toSet());
        String[] additionalColors = generateBackgroundColors(definedColors,
            data.size() - backgroundColors.length
        );
        colors = Arrays.copyOf(backgroundColors,
            backgroundColors.length + additionalColors.length
        );
        System.arraycopy(additionalColors, 0, colors, backgroundColors.length,
            additionalColors.length
        );
      }
    }
    else {
      colors = generateBackgroundColors(data.size());
    }
    return colors;
  }

  @Override
  protected EntityFilter getComponentFilter(String componentGuid, FilterModel filterModel) {
    if (filterReferences != null) {
      return filterReferences.toEntityFilter(componentGuid, filterModel);
    }
    else {
      return super.getComponentFilter(componentGuid, filterModel);
    }
  }
}
