/** Calculates the Food Made+Used data sets. */
  private void calculateFoodMadeUsedDataSets() {
    // Food Made+Used is showing 2 player stats at once: Food Made and Food Used

    // First calculate FOOD_USED data set...
    calculateDataSets(Stat.FOOD_USED);
    // ...and clone it because we'll need it in a moment
    final List<Chart<LineChartDataSet>> clonedChartList = new ArrayList<>(chartList.size());
    for (final Chart<LineChartDataSet> chart : chartList) {
      final Chart<LineChartDataSet> clonedChart = createChart();

      for (final DataModel<LineChartDataSet> model : chart.getDataModelList()) {
        final DataModel<LineChartDataSet> clonedModel =
            new DataModel<>(model.getTitle(), model.getColor());

        for (final LineChartDataSet dataSet : model.getDataSetList()) {
          // Use brighter color as this will be the 2nd dataset
          final Color color = ((User) model.getUserobObject()).getPlayerColor().brighterColor;
          clonedModel.addDataSet(
              new LineChartDataSet(
                  color, dataSet.getStroke(), dataSet.getLoops(), dataSet.getValues()));
        }

        clonedChart.addModel(clonedModel);
      }

      clonedChartList.add(clonedChart);
    }

    // Cloning done, let's roll on to calculating the FOOD_MADE data set...
    calculateDataSets(Stat.FOOD_MADE);

    // And the final step: "merge" the 2 data sets into one, simply add the datasets...
    for (int chartIdx = 0; chartIdx < chartList.size(); chartIdx++) {
      final Chart<LineChartDataSet> chart = chartList.get(chartIdx);
      final Chart<LineChartDataSet> clonedChart = clonedChartList.get(chartIdx);

      for (int modelIdx = 0; modelIdx < chart.getDataModelList().size(); modelIdx++) {
        final DataModel<LineChartDataSet> model = chart.getDataModelList().get(modelIdx);
        final DataModel<LineChartDataSet> clonedModel =
            clonedChart.getDataModelList().get(modelIdx);

        model.getDataSetList().addAll(clonedModel.getDataSetList());
      }
    }
  }
  @Override
  public void reconfigureChartCanvas() {
    final boolean gasOnly =
        statComboBox.getSelectedItem().hasMinGas
            && separateMinsGasCheckBox.isSelected()
            && !minsCheckBox.isSelected();

    final Presentation p = presentationComboBox.getSelectedItem();

    for (final Chart<?> chart : chartsComp.getChartsCanvas().getChartList()) {
      if (chart instanceof LineTimeChart) {
        final LineTimeChart lc = (LineTimeChart) chart;
        lc.setPresentation(p);

        // Stroke depends on the presentation:
        for (final DataModel<LineChartDataSet> model : lc.getDataModelList()) {
          int i = 0;
          for (final LineChartDataSet dataSet : model.getDataSetList())
            dataSet.setStroke(i++ > 0 || gasOnly ? p.storke : p.storkeDouble);
        }
      }
    }
  }
  /**
   * Calculates the data sets for the specified {@link Stat}.
   *
   * @param stat stat to calculate the data sets for
   */
  private void calculateDataSets(final Stat stat) {
    // Data points for each data set, inside being mapped from loop to value.
    final Map<LineChartDataSet, Map<Integer, Int>> dataSetPointsMap = new HashMap<>();

    final boolean mergeMinGas = stat.hasMinGas && !separateMinsGasCheckBox.isSelected();

    final String[] eventFields; // Event fields to query eventually
    if (stat.hasMinGas) {
      final boolean includeMins = minsCheckBox.isSelected();
      final boolean includeGas = gasCheckBox.isSelected();
      if (includeMins && includeGas) eventFields = stat.eventFields;
      else if (includeMins) eventFields = new String[] {stat.eventFields[0]};
      else if (includeGas) eventFields = new String[] {stat.eventFields[1]};
      else eventFields = new String[0];
    } else eventFields = stat.eventFields;

    // Calculate data points maps
    for (final Event event : repProc.replay.trackerEvents.events) {
      if (event.id != ITrackerEvents.ID_PLAYER_STATS) continue;

      final DataModel<LineChartDataSet> model = modelByPlayerIds[event.getPlayerId()];
      if (model == null) continue;

      // One extra player stat event is recorded when a user leaves (at the same loop) even if at
      // the same loop there was a
      // scheduled player stat event. So we have to exclude the last player stat event (to avoid the
      // chart getting
      // "screwed"). (This for example contains mineralsCurrent = 0.)
      // Note: if there's a scheduled stat event when the player left, this will be excluded
      // wrongly,
      // but I don't care, it won't make any noticable difference...
      if (event.loop == repProc.usersByPlayerId[event.getPlayerId()].leaveLoop) continue;

      // Data sets per event fields
      for (int i = 0; i < eventFields.length; i++) {
        final String field = eventFields[i];
        final LineChartDataSet dataSet = model.getDataSetList().get(mergeMinGas ? 0 : i);

        Map<Integer, Int> pointsMap = dataSetPointsMap.get(dataSet);
        if (pointsMap == null) dataSetPointsMap.put(dataSet, pointsMap = new HashMap<>());
        final Integer loop = Integer.valueOf(event.loop);
        Int point = pointsMap.get(loop);
        if (point == null) {
          pointsMap.put(loop, point = new Int());
        }
        int value = ((PlayerStatsEvent) event).stats.<Integer>get(field);
        if (field == PlayerStatsEvent.F_FOOD_MADE || field == PlayerStatsEvent.F_FOOD_USED)
          value >>= 12;
        point.value += value;
      }
    }

    // Convert data points maps to arrays
    for (final Chart<LineChartDataSet> chart : chartList) {
      for (final DataModel<LineChartDataSet> model : chart.getDataModelList()) {
        for (final LineChartDataSet dataSet : model.getDataSetList()) {
          final Map<Integer, Int> pointsMap = dataSetPointsMap.get(dataSet);
          if (pointsMap == null) continue;

          // Data points are needed sorted by loop
          final List<Entry<Integer, Int>> dataPointList = new ArrayList<>(pointsMap.entrySet());
          Collections.sort(
              dataPointList,
              new Comparator<Entry<Integer, Int>>() {
                @Override
                public int compare(Entry<Integer, Int> o1, Entry<Integer, Int> o2) {
                  return o1.getKey().compareTo(o2.getKey());
                }
              });

          int i = 0;
          final int[] loops = new int[pointsMap.size()];
          final int[] values = new int[pointsMap.size()];
          for (final Entry<Integer, Int> dataPoint : dataPointList) {
            loops[i] = dataPoint.getKey();
            values[i] = dataPoint.getValue().value;
            i++;
          }

          dataSet.setLoops(loops);
          dataSet.setValues(values);
          dataSet.calculateValueMax();
        }
      }
    }
  }
  /** Calculates the Spending Quotient data sets. */
  private void calculateSQDataSets() {
    // Spending Quotient is a function of 2 other Stats: SQ = f( RES_CURRENT, RES_COLL_RATE )

    // First calculate RES_CURRENT data set...
    calculateDataSets(Stat.RES_CURRENT);
    // ...and clone it because we'll need it in a moment
    final List<Chart<LineChartDataSet>> clonedChartList = new ArrayList<>(chartList.size());
    for (final Chart<LineChartDataSet> chart : chartList) {
      final Chart<LineChartDataSet> clonedChart = createChart();

      for (final DataModel<LineChartDataSet> model : chart.getDataModelList()) {
        final DataModel<LineChartDataSet> clonedModel = new DataModel<>(null, null);

        for (final LineChartDataSet dataSet : model.getDataSetList())
          clonedModel.addDataSet(
              new LineChartDataSet(null, null, dataSet.getLoops(), dataSet.getValues()));

        clonedChart.addModel(clonedModel);
      }

      clonedChartList.add(clonedChart);
    }

    // Cloning done, let's roll on to calculating the RES_COLL_RATE data set...
    calculateDataSets(Stat.RES_COLL_RATE);

    // We'll need the last command game event loops indexed by data model (last cmd loop is the same
    // for all data sets in a
    // model)
    final Map<DataModel<LineChartDataSet>, Int> modelLastCmdLoopsMap = new HashMap<>();
    for (final Event event : repProc.replay.gameEvents.events) {
      if (event.id != IGameEvents.ID_CMD) continue;

      // Player stats go by tracker events which go by player id. First check if there is player for
      // the user!
      final int playerId = repProc.usersByUserId[event.userId].playerId;
      if (playerId <= 0) continue;

      final DataModel<LineChartDataSet> model = modelByPlayerIds[playerId];
      if (model == null) continue;

      // Update last command loop per model
      Int lastCmdLoop = modelLastCmdLoopsMap.get(model);
      if (lastCmdLoop == null) modelLastCmdLoopsMap.put(model, lastCmdLoop = new Int());
      lastCmdLoop.value = event.loop;
    }

    // And the final step: "merge" the 2 data sets, the result being the SQ
    for (int chartIdx = 0; chartIdx < chartList.size(); chartIdx++) {
      final Chart<LineChartDataSet> chart = chartList.get(chartIdx);
      final Chart<LineChartDataSet> clonedChart = clonedChartList.get(chartIdx);

      for (int modelIdx = 0; modelIdx < chart.getDataModelList().size(); modelIdx++) {
        final DataModel<LineChartDataSet> model = chart.getDataModelList().get(modelIdx);
        final DataModel<LineChartDataSet> clonedModel =
            clonedChart.getDataModelList().get(modelIdx);

        final int lastCmdLoop =
            modelLastCmdLoopsMap.get(model) == null ? 0 : modelLastCmdLoopsMap.get(model).value;

        for (int dataSetIdx = 0; dataSetIdx < model.getDataSetList().size(); dataSetIdx++) {
          final LineChartDataSet dataSet = model.getDataSetList().get(dataSetIdx);
          final LineChartDataSet clonedDataSet = clonedModel.getDataSetList().get(dataSetIdx);

          // Only include values in total before the last command loop! And of course only count
          // samples up to that.

          final int[] loops = dataSet.getLoops();
          final int[] values = dataSet.getValues();
          final int[] clonedValues = clonedDataSet.getValues();

          long totalValues = 0, totalClonedValues = 0;
          int totalCount = 0;

          for (int valueIdx = 0; valueIdx < values.length; valueIdx++) {
            if (loops[valueIdx] <= lastCmdLoop) {
              totalClonedValues += clonedValues[valueIdx];
              totalValues += values[valueIdx];
              totalCount++;
            }
            values[valueIdx] =
                RepProcessor.calculateSQImpl(clonedValues[valueIdx], values[valueIdx]);
          }

          // Calculate average SQ for the data set, put in data set title!
          final int avgSQ =
              totalCount == 0
                  ? 0
                  : RepProcessor.calculateSQImpl(
                      (int) (totalClonedValues / totalCount), (int) (totalValues / totalCount));
          dataSet.setTitle("SQ: " + avgSQ);
          dataSet.calculateValueMax();
        }
      }
    }
  }