/*
 * Copyright 2011 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.optaplanner.benchmark.impl.report;

import java.awt.BasicStroke;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.text.NumberFormat;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.NavigableSet;
import java.util.TreeSet;
import javax.imageio.ImageIO;

import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.CategoryAxis;
import org.jfree.chart.axis.LogarithmicAxis;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.labels.ItemLabelAnchor;
import org.jfree.chart.labels.ItemLabelPosition;
import org.jfree.chart.labels.StandardCategoryItemLabelGenerator;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.category.BarRenderer;
import org.jfree.chart.renderer.category.BoxAndWhiskerRenderer;
import org.jfree.chart.renderer.xy.StandardXYItemRenderer;
import org.jfree.chart.renderer.xy.XYItemRenderer;
import org.jfree.data.category.DefaultCategoryDataset;
import org.jfree.data.statistics.DefaultBoxAndWhiskerCategoryDataset;
import org.jfree.data.xy.XYDataItem;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
import org.jfree.ui.TextAnchor;
import org.optaplanner.benchmark.impl.ranking.SolverRankingWeightFactory;
import org.optaplanner.benchmark.impl.result.PlannerBenchmarkResult;
import org.optaplanner.benchmark.impl.result.ProblemBenchmarkResult;
import org.optaplanner.benchmark.impl.result.SingleBenchmarkResult;
import org.optaplanner.benchmark.impl.result.SolverBenchmarkResult;
import org.optaplanner.benchmark.impl.result.SubSingleBenchmarkResult;
import org.optaplanner.benchmark.impl.statistic.ProblemStatistic;
import org.optaplanner.benchmark.impl.statistic.PureSubSingleStatistic;
import org.optaplanner.benchmark.impl.statistic.SubSingleStatistic;
import org.optaplanner.benchmark.impl.statistic.common.MillisecondsSpentNumberFormat;
import org.optaplanner.core.config.solver.EnvironmentMode;
import org.optaplanner.core.impl.score.ScoreUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class BenchmarkReport {

    protected final transient Logger logger = LoggerFactory.getLogger(getClass());

    public static final int CHARTED_SCORE_LEVEL_SIZE = 15;
    public static final int LOG_SCALE_MIN_DATASETS_COUNT = 5;

    private final PlannerBenchmarkResult plannerBenchmarkResult;

    private Locale locale = null;
    private ZoneId timezoneId = null;
    private Comparator<SolverBenchmarkResult> solverRankingComparator = null;
    private SolverRankingWeightFactory solverRankingWeightFactory = null;
    private File summaryDirectory = null;
    private List<File> bestScoreSummaryChartFileList = null;
    private List<File> bestScoreScalabilitySummaryChartFileList = null;
    private List<File> bestScoreDistributionSummaryChartFileList = null;
    private List<File> winningScoreDifferenceSummaryChartFileList = null;
    private List<File> worstScoreDifferencePercentageSummaryChartFileList = null;
    private File scoreCalculationSpeedSummaryChartFile = null;
    private File worstScoreCalculationSpeedDifferencePercentageSummaryChartFile = null;
    private File timeSpentSummaryChartFile = null;
    private File timeSpentScalabilitySummaryChartFile = null;
    private List<File> bestScorePerTimeSpentSummaryChartFileList = null;

    private Integer defaultShownScoreLevelIndex = null;
    private List<String> warningList = null;

    private File htmlOverviewFile = null;

    public BenchmarkReport(PlannerBenchmarkResult plannerBenchmarkResult) {
        this.plannerBenchmarkResult = plannerBenchmarkResult;
    }

    public PlannerBenchmarkResult getPlannerBenchmarkResult() {
        return plannerBenchmarkResult;
    }

    public Locale getLocale() {
        return locale;
    }

    public void setLocale(Locale locale) {
        this.locale = locale;
    }

    public ZoneId getTimezoneId() {
        return timezoneId;
    }

    public void setTimezoneId(ZoneId timezoneId) {
        this.timezoneId = timezoneId;
    }

    public Comparator<SolverBenchmarkResult> getSolverRankingComparator() {
        return solverRankingComparator;
    }

    public void setSolverRankingComparator(Comparator<SolverBenchmarkResult> solverRankingComparator) {
        this.solverRankingComparator = solverRankingComparator;
    }

    public SolverRankingWeightFactory getSolverRankingWeightFactory() {
        return solverRankingWeightFactory;
    }

    public void setSolverRankingWeightFactory(SolverRankingWeightFactory solverRankingWeightFactory) {
        this.solverRankingWeightFactory = solverRankingWeightFactory;
    }

    public File getSummaryDirectory() {
        return summaryDirectory;
    }

    public List<File> getBestScoreSummaryChartFileList() {
        return bestScoreSummaryChartFileList;
    }

    public List<File> getBestScoreScalabilitySummaryChartFileList() {
        return bestScoreScalabilitySummaryChartFileList;
    }

    public List<File> getBestScoreDistributionSummaryChartFileList() {
        return bestScoreDistributionSummaryChartFileList;
    }

    public List<File> getWinningScoreDifferenceSummaryChartFileList() {
        return winningScoreDifferenceSummaryChartFileList;
    }

    public List<File> getWorstScoreDifferencePercentageSummaryChartFileList() {
        return worstScoreDifferencePercentageSummaryChartFileList;
    }

    public File getScoreCalculationSpeedSummaryChartFile() {
        return scoreCalculationSpeedSummaryChartFile;
    }

    public File getWorstScoreCalculationSpeedDifferencePercentageSummaryChartFile() {
        return worstScoreCalculationSpeedDifferencePercentageSummaryChartFile;
    }

    public File getTimeSpentSummaryChartFile() {
        return timeSpentSummaryChartFile;
    }

    public File getTimeSpentScalabilitySummaryChartFile() {
        return timeSpentScalabilitySummaryChartFile;
    }

    public List<File> getBestScorePerTimeSpentSummaryChartFileList() {
        return bestScorePerTimeSpentSummaryChartFileList;
    }

    public Integer getDefaultShownScoreLevelIndex() {
        return defaultShownScoreLevelIndex;
    }

    public List<String> getWarningList() {
        return warningList;
    }

    public File getHtmlOverviewFile() {
        return htmlOverviewFile;
    }

    // ************************************************************************
    // Smart getters
    // ************************************************************************

    public String getRelativePathToBenchmarkReportDirectory(File file) {
        String benchmarkReportDirectoryPath = plannerBenchmarkResult.getBenchmarkReportDirectory().getAbsoluteFile().toURI().getPath();
        String filePath = file.getAbsoluteFile().toURI().getPath();
        if (!filePath.startsWith(benchmarkReportDirectoryPath)) {
            throw new IllegalArgumentException("The filePath (" + filePath
                    + ") does not start with the benchmarkReportDirectoryPath (" + benchmarkReportDirectoryPath + ").");
        }
        String relativePath = filePath.substring(benchmarkReportDirectoryPath.length());
        if (relativePath.startsWith("/")) {
            relativePath = relativePath.substring(1);
        }
        return relativePath;
    }

    public String getSolverRankingClassSimpleName() {
        Class solverRankingClass = getSolverRankingClass();
        return solverRankingClass == null ? null : solverRankingClass.getSimpleName();
    }

    public String getSolverRankingClassFullName() {
        Class solverRankingClass = getSolverRankingClass();
        return solverRankingClass == null ? null : solverRankingClass.getName();
    }

    // ************************************************************************
    // Write methods
    // ************************************************************************

    public void writeReport() {
        logger.info("Generating benchmark report...");
        summaryDirectory = new File(plannerBenchmarkResult.getBenchmarkReportDirectory(), "summary");
        summaryDirectory.mkdir();
        plannerBenchmarkResult.accumulateResults(this);
        fillWarningList();
        writeBestScoreSummaryChart();
        writeBestScoreScalabilitySummaryChart();
        writeWinningScoreDifferenceSummaryChart();
        writeWorstScoreDifferencePercentageSummaryChart();
        writeBestScoreDistributionSummaryChart();
        writeScoreCalculationSpeedSummaryChart();
        writeWorstScoreCalculationSpeedDifferencePercentageSummaryChart();
        writeTimeSpentSummaryChart();
        writeTimeSpentScalabilitySummaryChart();
        writeBestScorePerTimeSpentSummaryChart();
        for (ProblemBenchmarkResult<Object> problemBenchmarkResult : plannerBenchmarkResult.getUnifiedProblemBenchmarkResultList()) {
            for (SingleBenchmarkResult singleBenchmarkResult : problemBenchmarkResult.getSingleBenchmarkResultList()) {
                for (SubSingleBenchmarkResult subSingleBenchmarkResult : singleBenchmarkResult.getSubSingleBenchmarkResultList()) {
                    if (!subSingleBenchmarkResult.hasAllSuccess()) {
                        continue;
                    }
                    for (SubSingleStatistic subSingleStatistic : subSingleBenchmarkResult.getEffectiveSubSingleStatisticMap().values()) {
                        try {
                            subSingleStatistic.unhibernatePointList();
                        } catch (IllegalStateException e) {
                            if (!plannerBenchmarkResult.getAggregation()) {
                                throw new IllegalStateException("Failed to unhibernate point list of SubSingleStatistic ("
                                        + subSingleStatistic + ") of SubSingleBenchmark (" + subSingleBenchmarkResult + ").", e);
                            }
                            logger.trace("This is expected, aggregator doesn't copy CSV files. Could not read CSV file "
                                    + "({}) of sub single statistic ({}).", subSingleStatistic.getCsvFile().getAbsolutePath(), subSingleStatistic);
                        }
                    }
                }
            }
        }
        for (ProblemBenchmarkResult<Object> problemBenchmarkResult : plannerBenchmarkResult.getUnifiedProblemBenchmarkResultList()) {
            if (problemBenchmarkResult.hasAnySuccess()) {
                for (ProblemStatistic problemStatistic : problemBenchmarkResult.getProblemStatisticList()) {
                    problemStatistic.writeGraphFiles(this);
                }
                for (SingleBenchmarkResult singleBenchmarkResult : problemBenchmarkResult.getSingleBenchmarkResultList()) {
                    if (singleBenchmarkResult.hasAllSuccess()) {
                        for (PureSubSingleStatistic pureSubSingleStatistic : singleBenchmarkResult.getMedian().getPureSubSingleStatisticList()) {
                            pureSubSingleStatistic.writeGraphFiles(this);
                        }
                    }
                }
            }
        }
        for (ProblemBenchmarkResult<Object> problemBenchmarkResult : plannerBenchmarkResult.getUnifiedProblemBenchmarkResultList()) {
            for (SingleBenchmarkResult singleBenchmarkResult : problemBenchmarkResult.getSingleBenchmarkResultList()) {
                for (SubSingleBenchmarkResult subSingleBenchmarkResult : singleBenchmarkResult.getSubSingleBenchmarkResultList()) {
                    if (!subSingleBenchmarkResult.hasAllSuccess()) {
                        continue;
                    }
                    for (SubSingleStatistic subSingleStatistic : subSingleBenchmarkResult.getEffectiveSubSingleStatisticMap().values()) {
                        if (plannerBenchmarkResult.getAggregation()) {
                            subSingleStatistic.setPointList(null);
                        } else {
                            subSingleStatistic.hibernatePointList();
                        }
                    }
                }
            }
        }
        determineDefaultShownScoreLevelIndex();
        writeHtmlOverviewFile();
    }

    protected void fillWarningList() {
        warningList = new ArrayList<>();
        String javaVmName = System.getProperty("java.vm.name");
        if (javaVmName != null && javaVmName.contains("Client VM")) {
            warningList.add("The Java VM (" + javaVmName + ") is the Client VM."
                    + " This decreases performance."
                    + " Maybe start the java process with the argument \"-server\" to get better results.");
        }
        Integer parallelBenchmarkCount = plannerBenchmarkResult.getParallelBenchmarkCount();
        Integer availableProcessors = plannerBenchmarkResult.getAvailableProcessors();
        if (parallelBenchmarkCount != null && availableProcessors != null
                && parallelBenchmarkCount > availableProcessors) {
            warningList.add("The parallelBenchmarkCount (" + parallelBenchmarkCount
                    + ") is higher than the number of availableProcessors (" + availableProcessors + ")."
                    + " This decreases performance."
                    + " Maybe reduce the parallelBenchmarkCount.");
        }
        EnvironmentMode environmentMode = plannerBenchmarkResult.getEnvironmentMode();
        if (environmentMode != null && environmentMode.isAsserted()) {
            warningList.add("The environmentMode (" + environmentMode + ") is asserting."
                    + " This decreases performance."
                    + " Maybe set the environmentMode to " + EnvironmentMode.REPRODUCIBLE + ".");
        }
        String loggingLevelOptaPlannerCore = plannerBenchmarkResult.getLoggingLevelOptaPlannerCore();
        if (loggingLevelOptaPlannerCore != null && loggingLevelOptaPlannerCore.equals("trace")) {
            warningList.add("The loggingLevel (" + loggingLevelOptaPlannerCore + ") of org.optaplanner.core is high."
                    + " This decreases performance."
                    + " Maybe set the loggingLevel to debug or lower.");
        }
        String loggingLevelDroolsCore = plannerBenchmarkResult.getLoggingLevelDroolsCore();
        if (loggingLevelDroolsCore != null
                && (loggingLevelDroolsCore.equals("trace") || loggingLevelDroolsCore.equals("debug"))) {
            warningList.add("The loggingLevel (" + loggingLevelDroolsCore + ") of org.drools.core is high."
                    + " This decreases performance."
                    + " Maybe set the loggingLevel to info or lower.");
        }
    }

    private void writeBestScoreSummaryChart() {
        // Each scoreLevel has its own dataset and chartFile
        List<DefaultCategoryDataset> datasetList = new ArrayList<>(CHARTED_SCORE_LEVEL_SIZE);
        for (SolverBenchmarkResult solverBenchmarkResult : plannerBenchmarkResult.getSolverBenchmarkResultList()) {
            String solverLabel = solverBenchmarkResult.getNameWithFavoriteSuffix();
            for (SingleBenchmarkResult singleBenchmarkResult : solverBenchmarkResult.getSingleBenchmarkResultList()) {
                String problemLabel = singleBenchmarkResult.getProblemBenchmarkResult().getName();
                if (singleBenchmarkResult.hasAllSuccess()) {
                    double[] levelValues = ScoreUtils.extractLevelDoubles(singleBenchmarkResult.getAverageScore());
                    for (int i = 0; i < levelValues.length && i < CHARTED_SCORE_LEVEL_SIZE; i++) {
                        if (i >= datasetList.size()) {
                            datasetList.add(new DefaultCategoryDataset());
                        }
                        datasetList.get(i).addValue(levelValues[i], solverLabel, problemLabel);
                    }
                }
            }
        }
        bestScoreSummaryChartFileList = new ArrayList<>(datasetList.size());
        int scoreLevelIndex = 0;
        for (DefaultCategoryDataset dataset : datasetList) {
            String scoreLevelLabel = plannerBenchmarkResult.findScoreLevelLabel(scoreLevelIndex);
            CategoryPlot plot = createBarChartPlot(dataset,
                    "Best " + scoreLevelLabel, NumberFormat.getInstance(locale));
            JFreeChart chart = new JFreeChart("Best " + scoreLevelLabel + " summary (higher is better)",
                    JFreeChart.DEFAULT_TITLE_FONT, plot, true);
            bestScoreSummaryChartFileList.add(writeChartToImageFile(chart, "bestScoreSummaryLevel" + scoreLevelIndex));
            scoreLevelIndex++;
        }
    }

    private void writeBestScoreScalabilitySummaryChart() {
        // Each scoreLevel has its own dataset and chartFile
        List<List<XYSeries>> seriesListList = new ArrayList<>(
                CHARTED_SCORE_LEVEL_SIZE);
        int solverBenchmarkIndex = 0;
        for (SolverBenchmarkResult solverBenchmarkResult : plannerBenchmarkResult.getSolverBenchmarkResultList()) {
            String solverLabel = solverBenchmarkResult.getNameWithFavoriteSuffix();
            for (SingleBenchmarkResult singleBenchmarkResult : solverBenchmarkResult.getSingleBenchmarkResultList()) {
                if (singleBenchmarkResult.hasAllSuccess()) {
                    long problemScale = singleBenchmarkResult.getProblemBenchmarkResult().getProblemScale();
                    double[] levelValues = ScoreUtils.extractLevelDoubles(singleBenchmarkResult.getAverageScore());
                    for (int i = 0; i < levelValues.length && i < CHARTED_SCORE_LEVEL_SIZE; i++) {
                        if (i >= seriesListList.size()) {
                            seriesListList.add(new ArrayList<>(
                                    plannerBenchmarkResult.getSolverBenchmarkResultList().size()));
                        }
                        List<XYSeries> seriesList = seriesListList.get(i);
                        while (solverBenchmarkIndex >= seriesList.size()) {
                            seriesList.add(new XYSeries(solverLabel));
                        }
                        seriesList.get(solverBenchmarkIndex).add((double) problemScale, levelValues[i]);
                    }
                }
            }
            solverBenchmarkIndex++;
        }
        bestScoreScalabilitySummaryChartFileList = new ArrayList<>(seriesListList.size());
        int scoreLevelIndex = 0;
        for (List<XYSeries> seriesList : seriesListList) {
            String scoreLevelLabel = plannerBenchmarkResult.findScoreLevelLabel(scoreLevelIndex);
            XYPlot plot = createScalabilityPlot(seriesList,
                    "Problem scale", NumberFormat.getInstance(locale),
                    "Best " + scoreLevelLabel, NumberFormat.getInstance(locale));
            JFreeChart chart = new JFreeChart(
                    "Best " + scoreLevelLabel + " scalability summary (higher is better)",
                    JFreeChart.DEFAULT_TITLE_FONT, plot, true);
            bestScoreScalabilitySummaryChartFileList.add(
                    writeChartToImageFile(chart, "bestScoreScalabilitySummaryLevel" + scoreLevelIndex));
            scoreLevelIndex++;
        }
    }

    private void writeBestScoreDistributionSummaryChart() {
        // Each scoreLevel has its own dataset and chartFile
        List<DefaultBoxAndWhiskerCategoryDataset> datasetList = new ArrayList<>(CHARTED_SCORE_LEVEL_SIZE);
        for (SolverBenchmarkResult solverBenchmarkResult : plannerBenchmarkResult.getSolverBenchmarkResultList()) {
            String solverLabel = solverBenchmarkResult.getNameWithFavoriteSuffix();
            for (SingleBenchmarkResult singleBenchmarkResult : solverBenchmarkResult.getSingleBenchmarkResultList()) {
                String problemLabel = singleBenchmarkResult.getProblemBenchmarkResult().getName();
                if (singleBenchmarkResult.hasAllSuccess()) {
                    List<List<Double>> distributionLevelList = new ArrayList<>(CHARTED_SCORE_LEVEL_SIZE);
                    for (SubSingleBenchmarkResult subSingleBenchmarkResult : singleBenchmarkResult.getSubSingleBenchmarkResultList()) {
                        double[] levelValues = ScoreUtils.extractLevelDoubles(subSingleBenchmarkResult.getAverageScore());
                        for (int i = 0; i < levelValues.length && i < CHARTED_SCORE_LEVEL_SIZE; i++) {
                            if (i >= distributionLevelList.size()) {
                                distributionLevelList.add(new ArrayList<>(singleBenchmarkResult.getSubSingleCount()));
                            }
                            distributionLevelList.get(i).add(levelValues[i]);
                        }
                    }
                    for (int i = 0; i < distributionLevelList.size() && i < CHARTED_SCORE_LEVEL_SIZE; i++) {
                        if (i >= datasetList.size()) {
                            datasetList.add(new DefaultBoxAndWhiskerCategoryDataset());
                        }
                        datasetList.get(i).add(
                                distributionLevelList.get(i),
                                solverLabel,
                                problemLabel);
                    }
                }
            }
        }
        bestScoreDistributionSummaryChartFileList = new ArrayList<>(datasetList.size());
        int scoreLevelIndex = 0;
        for (DefaultBoxAndWhiskerCategoryDataset dataset : datasetList) {
            String scoreLevelLabel = plannerBenchmarkResult.findScoreLevelLabel(scoreLevelIndex);
            CategoryPlot plot = createBoxAndWhiskerChartPlot(dataset,
                    "Best " + scoreLevelLabel, NumberFormat.getInstance(locale));
            JFreeChart chart = new JFreeChart("Best " + scoreLevelLabel + " distribution summary (higher is better)",
                    JFreeChart.DEFAULT_TITLE_FONT, plot, true);
            bestScoreDistributionSummaryChartFileList.add(writeChartToImageFile(chart, "bestScoreDistributionSummaryLevel" + scoreLevelIndex));
            scoreLevelIndex++;
        }
    }

    private void writeWinningScoreDifferenceSummaryChart() {
        // Each scoreLevel has its own dataset and chartFile
        List<DefaultCategoryDataset> datasetList = new ArrayList<>(CHARTED_SCORE_LEVEL_SIZE);
        for (SolverBenchmarkResult solverBenchmarkResult : plannerBenchmarkResult.getSolverBenchmarkResultList()) {
            String solverLabel = solverBenchmarkResult.getNameWithFavoriteSuffix();
            for (SingleBenchmarkResult singleBenchmarkResult : solverBenchmarkResult.getSingleBenchmarkResultList()) {
                String problemLabel = singleBenchmarkResult.getProblemBenchmarkResult().getName();
                if (singleBenchmarkResult.hasAllSuccess()) {
                    double[] levelValues = ScoreUtils.extractLevelDoubles(singleBenchmarkResult.getWinningScoreDifference());
                    for (int i = 0; i < levelValues.length && i < CHARTED_SCORE_LEVEL_SIZE; i++) {
                        if (i >= datasetList.size()) {
                            datasetList.add(new DefaultCategoryDataset());
                        }
                        datasetList.get(i).addValue(levelValues[i], solverLabel, problemLabel);
                    }
                }
            }
        }
        winningScoreDifferenceSummaryChartFileList = new ArrayList<>(datasetList.size());
        int scoreLevelIndex = 0;
        for (DefaultCategoryDataset dataset : datasetList) {
            String scoreLevelLabel = plannerBenchmarkResult.findScoreLevelLabel(scoreLevelIndex);
            CategoryPlot plot = createBarChartPlot(dataset,
                    "Winning " + scoreLevelLabel + " difference", NumberFormat.getInstance(locale));
            JFreeChart chart = new JFreeChart("Winning " + scoreLevelLabel + " difference summary (higher is better)",
                    JFreeChart.DEFAULT_TITLE_FONT, plot, true);
            winningScoreDifferenceSummaryChartFileList.add(
                    writeChartToImageFile(chart, "winningScoreDifferenceSummaryLevel" + scoreLevelIndex));
            scoreLevelIndex++;
        }
    }

    private void writeWorstScoreDifferencePercentageSummaryChart() {
        // Each scoreLevel has its own dataset and chartFile
        List<DefaultCategoryDataset> datasetList = new ArrayList<>(CHARTED_SCORE_LEVEL_SIZE);
        for (SolverBenchmarkResult solverBenchmarkResult : plannerBenchmarkResult.getSolverBenchmarkResultList()) {
            String solverLabel = solverBenchmarkResult.getNameWithFavoriteSuffix();
            for (SingleBenchmarkResult singleBenchmarkResult : solverBenchmarkResult.getSingleBenchmarkResultList()) {
                String problemLabel = singleBenchmarkResult.getProblemBenchmarkResult().getName();
                if (singleBenchmarkResult.hasAllSuccess()) {
                    double[] levelValues = singleBenchmarkResult.getWorstScoreDifferencePercentage().getPercentageLevels();
                    for (int i = 0; i < levelValues.length && i < CHARTED_SCORE_LEVEL_SIZE; i++) {
                        if (i >= datasetList.size()) {
                            datasetList.add(new DefaultCategoryDataset());
                        }
                        datasetList.get(i).addValue(levelValues[i], solverLabel, problemLabel);
                    }
                }
            }
        }
        worstScoreDifferencePercentageSummaryChartFileList = new ArrayList<>(datasetList.size());
        int scoreLevelIndex = 0;
        for (DefaultCategoryDataset dataset : datasetList) {
            String scoreLevelLabel = plannerBenchmarkResult.findScoreLevelLabel(scoreLevelIndex);
            CategoryPlot plot = createBarChartPlot(dataset,
                    "Worst " + scoreLevelLabel + " difference percentage",
                    NumberFormat.getPercentInstance(locale));
            JFreeChart chart = new JFreeChart("Worst " + scoreLevelLabel + " difference percentage"
                    + " summary (higher is better)",
                    JFreeChart.DEFAULT_TITLE_FONT, plot, true);
            worstScoreDifferencePercentageSummaryChartFileList.add(
                    writeChartToImageFile(chart, "worstScoreDifferencePercentageSummaryLevel" + scoreLevelIndex));
            scoreLevelIndex++;
        }
    }

    private void writeScoreCalculationSpeedSummaryChart() {
        List<XYSeries> seriesList = new ArrayList<>(plannerBenchmarkResult.getSolverBenchmarkResultList().size());
        for (SolverBenchmarkResult solverBenchmarkResult : plannerBenchmarkResult.getSolverBenchmarkResultList()) {
            String solverLabel = solverBenchmarkResult.getNameWithFavoriteSuffix();
            XYSeries series = new XYSeries(solverLabel);
            for (SingleBenchmarkResult singleBenchmarkResult : solverBenchmarkResult.getSingleBenchmarkResultList()) {
                if (singleBenchmarkResult.hasAllSuccess()) {
                    long problemScale = singleBenchmarkResult.getProblemBenchmarkResult().getProblemScale();
                    long scoreCalculationSpeed = singleBenchmarkResult.getScoreCalculationSpeed();
                    series.add((Long) problemScale, (Long) scoreCalculationSpeed);
                }
            }
            seriesList.add(series);
        }
        XYPlot plot = createScalabilityPlot(seriesList,
                "Problem scale", NumberFormat.getInstance(locale),
                "Score calculation speed per second", NumberFormat.getInstance(locale));
        JFreeChart chart = new JFreeChart("Score calculation speed summary (higher is better)",
                JFreeChart.DEFAULT_TITLE_FONT, plot, true);
        scoreCalculationSpeedSummaryChartFile = writeChartToImageFile(chart, "scoreCalculationSpeedSummary");
    }

    private void writeWorstScoreCalculationSpeedDifferencePercentageSummaryChart() {
        DefaultCategoryDataset dataset = new DefaultCategoryDataset();
        for (SolverBenchmarkResult solverBenchmarkResult : plannerBenchmarkResult.getSolverBenchmarkResultList()) {
            String solverLabel = solverBenchmarkResult.getNameWithFavoriteSuffix();
            for (SingleBenchmarkResult singleBenchmarkResult : solverBenchmarkResult.getSingleBenchmarkResultList()) {
                String problemLabel = singleBenchmarkResult.getProblemBenchmarkResult().getName();
                if (singleBenchmarkResult.hasAllSuccess()) {
                    double worstScoreCalculationSpeedDifferencePercentage
                            = singleBenchmarkResult.getWorstScoreCalculationSpeedDifferencePercentage();
                    dataset.addValue(worstScoreCalculationSpeedDifferencePercentage, solverLabel, problemLabel);
                }
            }
        }
        CategoryPlot plot = createBarChartPlot(dataset,
                "Worst score calculation speed difference percentage",
                NumberFormat.getPercentInstance(locale));
        JFreeChart chart = new JFreeChart("Worst score calculation speed difference percentage"
                + " summary (higher is better)",
                JFreeChart.DEFAULT_TITLE_FONT, plot, true);
        worstScoreCalculationSpeedDifferencePercentageSummaryChartFile = writeChartToImageFile(chart, "worstScoreCalculationSpeedDifferencePercentageSummary");
    }

    private void writeTimeSpentSummaryChart() {
        DefaultCategoryDataset dataset = new DefaultCategoryDataset();
        for (SolverBenchmarkResult solverBenchmarkResult : plannerBenchmarkResult.getSolverBenchmarkResultList()) {
            String solverLabel = solverBenchmarkResult.getNameWithFavoriteSuffix();
            for (SingleBenchmarkResult singleBenchmarkResult : solverBenchmarkResult.getSingleBenchmarkResultList()) {
                String problemLabel = singleBenchmarkResult.getProblemBenchmarkResult().getName();
                if (singleBenchmarkResult.hasAllSuccess()) {
                    long timeMillisSpent = singleBenchmarkResult.getTimeMillisSpent();
                    dataset.addValue(timeMillisSpent, solverLabel, problemLabel);
                }
            }
        }
        CategoryPlot plot = createBarChartPlot(dataset, "Time spent", new MillisecondsSpentNumberFormat(locale));
        JFreeChart chart = new JFreeChart("Time spent summary (lower time is better)",
                JFreeChart.DEFAULT_TITLE_FONT, plot, true);
        timeSpentSummaryChartFile = writeChartToImageFile(chart, "timeSpentSummary");
    }

    private void writeTimeSpentScalabilitySummaryChart() {
        List<XYSeries> seriesList = new ArrayList<>(plannerBenchmarkResult.getSolverBenchmarkResultList().size());
        for (SolverBenchmarkResult solverBenchmarkResult : plannerBenchmarkResult.getSolverBenchmarkResultList()) {
            String solverLabel = solverBenchmarkResult.getNameWithFavoriteSuffix();
            XYSeries series = new XYSeries(solverLabel);
            for (SingleBenchmarkResult singleBenchmarkResult : solverBenchmarkResult.getSingleBenchmarkResultList()) {
                if (singleBenchmarkResult.hasAllSuccess()) {
                    long problemScale = singleBenchmarkResult.getProblemBenchmarkResult().getProblemScale();
                    long timeMillisSpent = singleBenchmarkResult.getTimeMillisSpent();
                    series.add((Long) problemScale, (Long) timeMillisSpent);
                }
            }
            seriesList.add(series);
        }
        XYPlot plot = createScalabilityPlot(seriesList,
                "Problem scale", NumberFormat.getInstance(locale),
                "Time spent", new MillisecondsSpentNumberFormat(locale));
        JFreeChart chart = new JFreeChart("Time spent scalability summary (lower is better)",
                JFreeChart.DEFAULT_TITLE_FONT, plot, true);
        timeSpentScalabilitySummaryChartFile = writeChartToImageFile(chart, "timeSpentScalabilitySummary");
    }

    private void writeBestScorePerTimeSpentSummaryChart() {
        // Each scoreLevel has its own dataset and chartFile
        List<List<XYSeries>> seriesListList = new ArrayList<>(
                CHARTED_SCORE_LEVEL_SIZE);
        int solverBenchmarkIndex = 0;
        for (SolverBenchmarkResult solverBenchmarkResult : plannerBenchmarkResult.getSolverBenchmarkResultList()) {
            String solverLabel = solverBenchmarkResult.getNameWithFavoriteSuffix();
            for (SingleBenchmarkResult singleBenchmarkResult : solverBenchmarkResult.getSingleBenchmarkResultList()) {
                if (singleBenchmarkResult.hasAllSuccess()) {
                    long timeMillisSpent = singleBenchmarkResult.getTimeMillisSpent();
                    double[] levelValues = ScoreUtils.extractLevelDoubles(singleBenchmarkResult.getAverageScore());
                    for (int i = 0; i < levelValues.length && i < CHARTED_SCORE_LEVEL_SIZE; i++) {
                        if (i >= seriesListList.size()) {
                            seriesListList.add(new ArrayList<>(
                                    plannerBenchmarkResult.getSolverBenchmarkResultList().size()));
                        }
                        List<XYSeries> seriesList = seriesListList.get(i);
                        while (solverBenchmarkIndex >= seriesList.size()) {
                            seriesList.add(new XYSeries(solverLabel));
                        }
                        seriesList.get(solverBenchmarkIndex).add((Long) timeMillisSpent, (Double) levelValues[i]);
                    }
                }
            }
            solverBenchmarkIndex++;
        }
        bestScorePerTimeSpentSummaryChartFileList = new ArrayList<>(seriesListList.size());
        int scoreLevelIndex = 0;
        for (List<XYSeries> seriesList : seriesListList) {
            String scoreLevelLabel = plannerBenchmarkResult.findScoreLevelLabel(scoreLevelIndex);
            XYPlot plot = createScalabilityPlot(seriesList,
                    "Time spent", new MillisecondsSpentNumberFormat(locale),
                    "Best " + scoreLevelLabel, NumberFormat.getInstance(locale));
            JFreeChart chart = new JFreeChart(
                    "Best " + scoreLevelLabel + " per time spent summary (higher left is better)",
                    JFreeChart.DEFAULT_TITLE_FONT, plot, true);
            bestScorePerTimeSpentSummaryChartFileList.add(
                    writeChartToImageFile(chart, "bestScorePerTimeSpentSummaryLevel" + scoreLevelIndex));
            scoreLevelIndex++;
        }
    }

    // ************************************************************************
    // Chart helper methods
    // ************************************************************************

    private CategoryPlot createBarChartPlot(DefaultCategoryDataset dataset,
            String yAxisLabel, NumberFormat yAxisNumberFormat) {
        CategoryAxis xAxis = new CategoryAxis("Data");
        xAxis.setCategoryMargin(0.40);
        NumberAxis yAxis = new NumberAxis(yAxisLabel);
        yAxis.setNumberFormatOverride(yAxisNumberFormat);
        BarRenderer renderer = createBarChartRenderer(yAxisNumberFormat);
        CategoryPlot plot = new CategoryPlot(dataset, xAxis, yAxis, renderer);
        plot.setOrientation(PlotOrientation.VERTICAL);
        return plot;
    }

    private BarRenderer createBarChartRenderer(NumberFormat numberFormat) {
        BarRenderer renderer = new BarRenderer();
        ItemLabelPosition positiveItemLabelPosition = new ItemLabelPosition(
                ItemLabelAnchor.OUTSIDE12, TextAnchor.BOTTOM_CENTER);
        renderer.setBasePositiveItemLabelPosition(positiveItemLabelPosition);
        ItemLabelPosition negativeItemLabelPosition = new ItemLabelPosition(
                ItemLabelAnchor.OUTSIDE6, TextAnchor.TOP_CENTER);
        renderer.setBaseNegativeItemLabelPosition(negativeItemLabelPosition);
        renderer.setBaseItemLabelGenerator(new StandardCategoryItemLabelGenerator(
                StandardCategoryItemLabelGenerator.DEFAULT_LABEL_FORMAT_STRING, numberFormat));
        renderer.setBaseItemLabelsVisible(true);
        return renderer;
    }

    private XYPlot createScalabilityPlot(List<XYSeries> seriesList,
            String xAxisLabel, NumberFormat xAxisNumberFormat,
            String yAxisLabel, NumberFormat yAxisNumberFormat) {
        NumberAxis xAxis;
        if (useLogarithmicProblemScale(seriesList)) {
            LogarithmicAxis logarithmicAxis = new LogarithmicAxis(xAxisLabel + " (logarithmic)");
            logarithmicAxis.setAllowNegativesFlag(true);
            xAxis = logarithmicAxis;
        } else {
            xAxis = new NumberAxis(xAxisLabel);
        }
        xAxis.setNumberFormatOverride(xAxisNumberFormat);
        NumberAxis yAxis = new NumberAxis(yAxisLabel);
        yAxis.setNumberFormatOverride(yAxisNumberFormat);
        XYPlot plot = new XYPlot(null, xAxis, yAxis, null);
        int seriesIndex = 0;
        for (XYSeries series : seriesList) {
            XYSeriesCollection seriesCollection = new XYSeriesCollection();
            seriesCollection.addSeries(series);
            plot.setDataset(seriesIndex, seriesCollection);
            XYItemRenderer renderer = createScalabilityPlotRenderer(yAxisNumberFormat);
            plot.setRenderer(seriesIndex, renderer);
            seriesIndex++;
        }
        plot.setOrientation(PlotOrientation.VERTICAL);
        return plot;
    }

    protected boolean useLogarithmicProblemScale(List<XYSeries> seriesList) {
        NavigableSet<Double> xValueSet = new TreeSet<>();
        int xValueListSize = 0;
        for (XYSeries series : seriesList) {
            for (XYDataItem dataItem : (List<XYDataItem>) series.getItems()) {
                xValueSet.add(dataItem.getXValue());
                xValueListSize++;
            }
        }
        if (xValueListSize < LOG_SCALE_MIN_DATASETS_COUNT) {
            return false;
        }
        // If 60% of the points are in 20% of the value space, use a logarithmic scale
        double threshold = 0.2 * (xValueSet.last() - xValueSet.first());
        int belowThresholdCount = xValueSet.headSet(threshold).size();
        return belowThresholdCount >= (0.6 * xValueSet.size());
    }

    private XYItemRenderer createScalabilityPlotRenderer(NumberFormat numberFormat) {
        XYItemRenderer renderer = new StandardXYItemRenderer(StandardXYItemRenderer.SHAPES_AND_LINES);
        // Use dashed line
        renderer.setSeriesStroke(0, new BasicStroke(
                1.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 1.0f, new float[] {2.0f, 6.0f}, 0.0f
        ));
        return renderer;
    }

    private CategoryPlot createBoxAndWhiskerChartPlot(DefaultBoxAndWhiskerCategoryDataset dataset,
            String yAxisLabel, NumberFormat yAxisNumberFormat) {
        CategoryAxis xAxis = new CategoryAxis("Data");
        NumberAxis yAxis = new NumberAxis(yAxisLabel);
        yAxis.setNumberFormatOverride(yAxisNumberFormat);
        BoxAndWhiskerRenderer renderer = new BoxAndWhiskerRenderer();
        renderer.setItemMargin(0.10);
        renderer.setMeanVisible(false);
        // Improve readability by avoiding low contrast with light colors
        renderer.setUseOutlinePaintForWhiskers(true);
        CategoryPlot plot = new CategoryPlot(dataset, xAxis, yAxis, renderer);
        plot.setOrientation(PlotOrientation.VERTICAL);
        return plot;
    }

    private File writeChartToImageFile(JFreeChart chart, String fileNameBase) {
        BufferedImage chartImage = chart.createBufferedImage(1024, 768);
        File summaryChartFile = new File(summaryDirectory, fileNameBase + ".png");
        try (OutputStream out = new FileOutputStream(summaryChartFile)) {
            ImageIO.write(chartImage, "png", out);
        } catch (IOException e) {
            throw new IllegalArgumentException("Failed writing summaryChartFile (" + summaryChartFile + ").", e);
        }
        return summaryChartFile;
    }

    private void determineDefaultShownScoreLevelIndex() {
        defaultShownScoreLevelIndex = Integer.MAX_VALUE;
        for (ProblemBenchmarkResult<Object> problemBenchmarkResult : plannerBenchmarkResult.getUnifiedProblemBenchmarkResultList()) {
            if (problemBenchmarkResult.hasAnySuccess()) {
                double[] winningScoreLevels = ScoreUtils.extractLevelDoubles(
                        problemBenchmarkResult.getWinningSingleBenchmarkResult().getAverageScore());
                int[] differenceCount = new int[winningScoreLevels.length];
                for (int i = 0; i < differenceCount.length; i++) {
                    differenceCount[i] = 0;
                }
                for (SingleBenchmarkResult singleBenchmarkResult : problemBenchmarkResult.getSingleBenchmarkResultList()) {
                    if (singleBenchmarkResult.hasAllSuccess()) {
                        double[] scoreLevels = ScoreUtils.extractLevelDoubles(singleBenchmarkResult.getAverageScore());
                        for (int i = 0; i < scoreLevels.length; i++) {
                            if (scoreLevels[i] != winningScoreLevels[i]) {
                                differenceCount[i] = differenceCount[i] + 1;
                            }
                        }
                    }
                }
                int firstInterestingLevel = differenceCount.length - 1;
                for (int i = 0; i < differenceCount.length; i++) {
                    if (differenceCount[i] > 0) {
                        firstInterestingLevel = i;
                        break;
                    }
                }
                if (defaultShownScoreLevelIndex > firstInterestingLevel) {
                    defaultShownScoreLevelIndex = firstInterestingLevel;
                }
            }
        }
    }

    private void writeHtmlOverviewFile() {
        File benchmarkReportDirectory = plannerBenchmarkResult.getBenchmarkReportDirectory();
        WebsiteResourceUtils.copyResourcesTo(benchmarkReportDirectory);

        htmlOverviewFile = new File(benchmarkReportDirectory, "index.html");
        Configuration freemarkerCfg = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
        freemarkerCfg.setDefaultEncoding("UTF-8");
        freemarkerCfg.setLocale(locale);
        freemarkerCfg.setClassForTemplateLoading(BenchmarkReport.class, "");

        String templateFilename = "benchmarkReport.html.ftl";
        Map<String, Object> model = new HashMap<>();
        model.put("benchmarkReport", this);
        model.put("reportHelper", new ReportHelper());

        try (Writer writer = new OutputStreamWriter(new FileOutputStream(htmlOverviewFile), "UTF-8")) {
            Template template = freemarkerCfg.getTemplate(templateFilename);
            template.process(model, writer);
        } catch (IOException e) {
            throw new IllegalArgumentException("Can not read templateFilename (" + templateFilename
                    + ") or write htmlOverviewFile (" + htmlOverviewFile + ").", e);
        } catch (TemplateException e) {
            throw new IllegalArgumentException("Can not process Freemarker templateFilename (" + templateFilename
                    + ") to htmlOverviewFile (" + htmlOverviewFile + ").", e);
        }
    }

    private Class getSolverRankingClass() {
        if (solverRankingComparator != null) {
            return solverRankingComparator.getClass();
        } else if (solverRankingWeightFactory != null) {
            return solverRankingWeightFactory.getClass();
        } else {
            return null;
        }
    }

}


/*
 * Copyright 2012 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.optaplanner.core.impl.heuristic.selector.move.generic.chained;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import org.optaplanner.core.api.domain.solution.PlanningSolution;
import org.optaplanner.core.impl.domain.variable.descriptor.GenuineVariableDescriptor;
import org.optaplanner.core.impl.domain.variable.inverserelation.SingletonInverseVariableSupply;
import org.optaplanner.core.impl.heuristic.selector.move.generic.SwapMove;
import org.optaplanner.core.impl.score.director.ScoreDirector;

/**
 * @param <Solution_> the solution type, the class with the {@link PlanningSolution} annotation
 */
public class ChainedSwapMove<Solution_> extends SwapMove<Solution_> {

    protected final List<Object> oldLeftTrailingEntityList;
    protected final List<Object> oldRightTrailingEntityList;

    public ChainedSwapMove(List<GenuineVariableDescriptor<Solution_>> variableDescriptorList,
            List<SingletonInverseVariableSupply> inverseVariableSupplyList, Object leftEntity, Object rightEntity) {
        super(variableDescriptorList, leftEntity, rightEntity);
        oldLeftTrailingEntityList = new ArrayList<>(inverseVariableSupplyList.size());
        oldRightTrailingEntityList = new ArrayList<>(inverseVariableSupplyList.size());
        for (SingletonInverseVariableSupply inverseVariableSupply : inverseVariableSupplyList) {
            oldLeftTrailingEntityList.add(inverseVariableSupply.getInverseSingleton(leftEntity));
            oldRightTrailingEntityList.add(inverseVariableSupply.getInverseSingleton(rightEntity));
        }
    }

    public ChainedSwapMove(List<GenuineVariableDescriptor<Solution_>> genuineVariableDescriptors,
            Object leftEntity, Object rightEntity,
            List<Object> oldLeftTrailingEntityList, List<Object> oldRightTrailingEntityList) {
        super(genuineVariableDescriptors, leftEntity, rightEntity);
        this.oldLeftTrailingEntityList = oldLeftTrailingEntityList;
        this.oldRightTrailingEntityList = oldRightTrailingEntityList;
    }

    // ************************************************************************
    // Worker methods
    // ************************************************************************

    @Override
    public ChainedSwapMove<Solution_> createUndoMove(ScoreDirector<Solution_> scoreDirector) {
        return new ChainedSwapMove<>(variableDescriptorList, rightEntity, leftEntity, oldLeftTrailingEntityList, oldRightTrailingEntityList);
    }

    @Override
    protected void doMoveOnGenuineVariables(ScoreDirector<Solution_> scoreDirector) {
        for (int i = 0; i < variableDescriptorList.size(); i++) {
            GenuineVariableDescriptor<Solution_> variableDescriptor = variableDescriptorList.get(i);
            Object oldLeftValue = variableDescriptor.getValue(leftEntity);
            Object oldRightValue = variableDescriptor.getValue(rightEntity);
            if (!Objects.equals(oldLeftValue, oldRightValue)) {
                if (!variableDescriptor.isChained()) {
                    scoreDirector.changeVariableFacade(variableDescriptor, leftEntity, oldRightValue);
                    scoreDirector.changeVariableFacade(variableDescriptor, rightEntity, oldLeftValue);
                } else {
                    Object oldLeftTrailingEntity = oldLeftTrailingEntityList.get(i);
                    Object oldRightTrailingEntity = oldRightTrailingEntityList.get(i);
                    if (oldRightValue == leftEntity) {
                        // Change the right entity
                        scoreDirector.changeVariableFacade(variableDescriptor, rightEntity, oldLeftValue);
                        // Change the left entity
                        scoreDirector.changeVariableFacade(variableDescriptor, leftEntity, rightEntity);
                        // Reroute the new left chain
                        if (oldRightTrailingEntity != null) {
                            scoreDirector.changeVariableFacade(variableDescriptor, oldRightTrailingEntity, leftEntity);
                        }
                    } else if (oldLeftValue == rightEntity) {
                        // Change the right entity
                        scoreDirector.changeVariableFacade(variableDescriptor, leftEntity, oldRightValue);
                        // Change the left entity
                        scoreDirector.changeVariableFacade(variableDescriptor, rightEntity, leftEntity);
                        // Reroute the new left chain
                        if (oldLeftTrailingEntity != null) {
                            scoreDirector.changeVariableFacade(variableDescriptor, oldLeftTrailingEntity, rightEntity);
                        }
                    } else {
                        // Change the left entity
                        scoreDirector.changeVariableFacade(variableDescriptor, leftEntity, oldRightValue);
                        // Change the right entity
                        scoreDirector.changeVariableFacade(variableDescriptor, rightEntity, oldLeftValue);
                        // Reroute the new left chain
                        if (oldRightTrailingEntity != null) {
                            scoreDirector.changeVariableFacade(variableDescriptor, oldRightTrailingEntity, leftEntity);
                        }
                        // Reroute the new right chain
                        if (oldLeftTrailingEntity != null) {
                            scoreDirector.changeVariableFacade(variableDescriptor, oldLeftTrailingEntity, rightEntity);
                        }
                    }
                }
            }
        }
    }

    @Override
    public ChainedSwapMove<Solution_> rebase(ScoreDirector<Solution_> destinationScoreDirector) {
        return new ChainedSwapMove<>(variableDescriptorList,
                destinationScoreDirector.lookUpWorkingObject(leftEntity),
                destinationScoreDirector.lookUpWorkingObject(rightEntity),
                rebaseList(oldLeftTrailingEntityList, destinationScoreDirector),
                rebaseList(oldRightTrailingEntityList, destinationScoreDirector));
    }

}


/*
 * Copyright 2010 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.optaplanner.core.impl.solver.termination;

import java.util.List;

import org.optaplanner.core.impl.phase.scope.AbstractPhaseScope;
import org.optaplanner.core.impl.solver.ChildThreadType;
import org.optaplanner.core.impl.solver.scope.DefaultSolverScope;

public class OrCompositeTermination extends AbstractCompositeTermination {

    public OrCompositeTermination(List<Termination> terminationList) {
        super(terminationList);
    }

    public OrCompositeTermination(Termination... terminations) {
        super(terminations);
    }

    // ************************************************************************
    // Terminated methods
    // ************************************************************************

    /**
     * @param solverScope never null
     * @return true if any of the Termination is terminated.
     */
    @Override
    public boolean isSolverTerminated(DefaultSolverScope solverScope) {
        for (Termination termination : terminationList) {
            if (termination.isSolverTerminated(solverScope)) {
                return true;
            }
        }
        return false;
    }

    /**
     * @param phaseScope never null
     * @return true if any of the Termination is terminated.
     */
    @Override
    public boolean isPhaseTerminated(AbstractPhaseScope phaseScope) {
        for (Termination termination : terminationList) {
            if (termination.isPhaseTerminated(phaseScope)) {
                return true;
            }
        }
        return false;
    }

    // ************************************************************************
    // Time gradient methods
    // ************************************************************************

    /**
     * Calculates the maximum timeGradient of all Terminations.
     * Not supported timeGradients (-1.0) are ignored.
     * @param solverScope never null
     * @return the maximum timeGradient of the Terminations.
     */
    @Override
    public double calculateSolverTimeGradient(DefaultSolverScope solverScope) {
        double timeGradient = 0.0;
        for (Termination termination : terminationList) {
            double nextTimeGradient = termination.calculateSolverTimeGradient(solverScope);
            if (nextTimeGradient >= 0.0) {
                timeGradient = Math.max(timeGradient, nextTimeGradient);
            }
        }
        return timeGradient;
    }

    /**
     * Calculates the maximum timeGradient of all Terminations.
     * Not supported timeGradients (-1.0) are ignored.
     * @param phaseScope never null
     * @return the maximum timeGradient of the Terminations.
     */
    @Override
    public double calculatePhaseTimeGradient(AbstractPhaseScope phaseScope) {
        double timeGradient = 0.0;
        for (Termination termination : terminationList) {
            double nextTimeGradient = termination.calculatePhaseTimeGradient(phaseScope);
            if (nextTimeGradient >= 0.0) {
                timeGradient = Math.max(timeGradient, nextTimeGradient);
            }
        }
        return timeGradient;
    }

    // ************************************************************************
    // Other methods
    // ************************************************************************

    @Override
    public OrCompositeTermination createChildThreadTermination(
            DefaultSolverScope solverScope, ChildThreadType childThreadType) {
        return new OrCompositeTermination(createChildThreadTerminationList(solverScope, childThreadType));
    }

    @Override
    public String toString() {
        return "Or(" + terminationList + ")";
    }

}


/*
 * Copyright 2012 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.optaplanner.core.api.score.buildin.hardmediumsoft;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.BiConsumer;

import org.kie.api.definition.rule.Rule;
import org.kie.api.runtime.rule.RuleContext;
import org.optaplanner.core.api.domain.constraintweight.ConstraintConfiguration;
import org.optaplanner.core.api.domain.constraintweight.ConstraintWeight;
import org.optaplanner.core.api.score.holder.AbstractScoreHolder;

/**
 * @see HardMediumSoftScore
 */
public class HardMediumSoftScoreHolder extends AbstractScoreHolder<HardMediumSoftScore> {

    protected final Map<Rule, BiConsumer<RuleContext, Integer>> matchExecutorByNumberMap = new LinkedHashMap<>();
    /** Slower than {@link #matchExecutorByNumberMap} */
    protected final Map<Rule, BiConsumer<RuleContext, HardMediumSoftScore>> matchExecutorByScoreMap = new LinkedHashMap<>();

    protected int hardScore;
    protected int mediumScore;
    protected int softScore;

    public HardMediumSoftScoreHolder(boolean constraintMatchEnabled) {
        super(constraintMatchEnabled, HardMediumSoftScore.ZERO);
    }

    public int getHardScore() {
        return hardScore;
    }

    public int getMediumScore() {
        return mediumScore;
    }

    public int getSoftScore() {
        return softScore;
    }

    // ************************************************************************
    // Setup methods
    // ************************************************************************

    @Override
    public void configureConstraintWeight(Rule rule, HardMediumSoftScore constraintWeight) {
        super.configureConstraintWeight(rule, constraintWeight);
        BiConsumer<RuleContext, Integer> matchExecutor;
        if (constraintWeight.equals(HardMediumSoftScore.ZERO)) {
            matchExecutor = (RuleContext kcontext, Integer weightMultiplier) -> {};
        } else if (constraintWeight.getMediumScore() == 0 && constraintWeight.getSoftScore() == 0) {
            matchExecutor = (RuleContext kcontext, Integer weightMultiplier)
                    -> addHardConstraintMatch(kcontext, constraintWeight.getHardScore() * weightMultiplier);
        } else if (constraintWeight.getHardScore() == 0 && constraintWeight.getSoftScore() == 0) {
            matchExecutor = (RuleContext kcontext, Integer weightMultiplier)
                    -> addMediumConstraintMatch(kcontext, constraintWeight.getMediumScore() * weightMultiplier);
        } else if (constraintWeight.getHardScore() == 0 && constraintWeight.getMediumScore() == 0) {
            matchExecutor = (RuleContext kcontext, Integer weightMultiplier)
                    -> addSoftConstraintMatch(kcontext, constraintWeight.getSoftScore() * weightMultiplier);
        } else {
            matchExecutor = (RuleContext kcontext, Integer weightMultiplier)
                    -> addMultiConstraintMatch(kcontext,
                    constraintWeight.getHardScore() * weightMultiplier,
                    constraintWeight.getMediumScore() * weightMultiplier,
                    constraintWeight.getSoftScore() * weightMultiplier);
        }
        matchExecutorByNumberMap.put(rule, matchExecutor);
        matchExecutorByScoreMap.put(rule, (RuleContext kcontext,
                HardMediumSoftScore weightMultiplier) -> addMultiConstraintMatch(kcontext,
                constraintWeight.getHardScore() * weightMultiplier.getHardScore(),
                constraintWeight.getMediumScore() * weightMultiplier.getMediumScore(),
                constraintWeight.getSoftScore() * weightMultiplier.getSoftScore()));
    }

    // ************************************************************************
    // Penalize and reward methods
    // ************************************************************************

    /**
     * Penalize a match by the {@link ConstraintWeight} negated.
     * @param kcontext never null, the magic variable in DRL
     */
    public void penalize(RuleContext kcontext) {
        reward(kcontext, -1);
    }

    /**
     * Penalize a match by the {@link ConstraintWeight} negated and multiplied with the weightMultiplier for all score levels.
     * @param kcontext never null, the magic variable in DRL
     * @param weightMultiplier at least 0
     */
    public void penalize(RuleContext kcontext, int weightMultiplier) {
        reward(kcontext, -weightMultiplier);
    }

    /**
     * Penalize a match by the {@link ConstraintWeight} negated and multiplied with the specific weightMultiplier per score level.
     * Slower than {@link #penalize(RuleContext, int)}.
     * @param kcontext never null, the magic variable in DRL
     * @param hardWeightMultiplier at least 0
     * @param mediumWeightMultiplier at least 0
     * @param softWeightMultiplier at least 0
     */
    public void penalize(RuleContext kcontext, int hardWeightMultiplier, int mediumWeightMultiplier, int softWeightMultiplier) {
        reward(kcontext, -hardWeightMultiplier, -mediumWeightMultiplier, -softWeightMultiplier);
    }

    /**
     * Reward a match by the {@link ConstraintWeight}.
     * @param kcontext never null, the magic variable in DRL
     */
    public void reward(RuleContext kcontext) {
        reward(kcontext, 1);
    }

    /**
     * Reward a match by the {@link ConstraintWeight} multiplied with the weightMultiplier for all score levels.
     * @param kcontext never null, the magic variable in DRL
     * @param weightMultiplier at least 0
     */
    public void reward(RuleContext kcontext, int weightMultiplier) {
        Rule rule = kcontext.getRule();
        BiConsumer<RuleContext, Integer> matchExecutor = matchExecutorByNumberMap.get(rule);
        if (matchExecutor == null) {
            throw new IllegalStateException("The DRL rule (" + rule.getPackageName() + ":" + rule.getName()
                    + ") does not match a @" + ConstraintWeight.class.getSimpleName() + " on the @"
                    + ConstraintConfiguration.class.getSimpleName() + " annotated class.");
        }
        matchExecutor.accept(kcontext, weightMultiplier);
    }

    /**
     * Reward a match by the {@link ConstraintWeight} multiplied with the specific weightMultiplier per score level.
     * Slower than {@link #reward(RuleContext, int)}.
     * @param kcontext never null, the magic variable in DRL
     * @param hardWeightMultiplier at least 0
     * @param mediumWeightMultiplier at least 0
     * @param softWeightMultiplier at least 0
     */
    public void reward(RuleContext kcontext, int hardWeightMultiplier, int mediumWeightMultiplier, int softWeightMultiplier) {
        Rule rule = kcontext.getRule();
        BiConsumer<RuleContext, HardMediumSoftScore> matchExecutor = matchExecutorByScoreMap.get(rule);
        if (matchExecutor == null) {
            throw new IllegalStateException("The DRL rule (" + rule.getPackageName() + ":" + rule.getName()
                    + ") does not match a @" + ConstraintWeight.class.getSimpleName() + " on the @"
                    + ConstraintConfiguration.class.getSimpleName() + " annotated class.");
        }
        matchExecutor.accept(kcontext, HardMediumSoftScore.of(hardWeightMultiplier, mediumWeightMultiplier, softWeightMultiplier));
    }

    // ************************************************************************
    // Other match methods
    // ************************************************************************

    /**
     * @param kcontext never null, the magic variable in DRL
     * @param hardWeight higher is better, negative for a penalty, positive for a reward
     */
    public void addHardConstraintMatch(RuleContext kcontext, int hardWeight) {
        hardScore += hardWeight;
        registerConstraintMatch(kcontext,
                () -> hardScore -= hardWeight,
                () -> HardMediumSoftScore.of(hardWeight, 0, 0));
    }

    /**
     * @param kcontext never null, the magic variable in DRL
     * @param mediumWeight higher is better, negative for a penalty, positive for a reward
     */
    public void addMediumConstraintMatch(RuleContext kcontext, int mediumWeight) {
        mediumScore += mediumWeight;
        registerConstraintMatch(kcontext,
                () -> mediumScore -= mediumWeight,
                () -> HardMediumSoftScore.of(0, mediumWeight, 0));
    }

    /**
     * @param kcontext never null, the magic variable in DRL
     * @param softWeight higher is better, negative for a penalty, positive for a reward
     */
    public void addSoftConstraintMatch(RuleContext kcontext, int softWeight) {
        softScore += softWeight;
        registerConstraintMatch(kcontext,
                () -> softScore -= softWeight,
                () -> HardMediumSoftScore.of(0, 0, softWeight));
    }

    /**
     * @param kcontext never null, the magic variable in DRL
     * @param hardWeight higher is better, negative for a penalty, positive for a reward
     * @param mediumWeight higher is better, negative for a penalty, positive for a reward
     * @param softWeight higher is better, negative for a penalty, positive for a reward
     */
    public void addMultiConstraintMatch(RuleContext kcontext, int hardWeight, int mediumWeight, int softWeight) {
        hardScore += hardWeight;
        mediumScore += mediumWeight;
        softScore += softWeight;
        registerConstraintMatch(kcontext,
                () -> {
                    hardScore -= hardWeight;
                    mediumScore -= mediumWeight;
                    softScore -= softWeight;
                },
                () -> HardMediumSoftScore.of(hardWeight, mediumWeight, softWeight));
    }

    @Override
    public HardMediumSoftScore extractScore(int initScore) {
        return HardMediumSoftScore.ofUninitialized(initScore, hardScore, mediumScore, softScore);
    }

}


/*
 * Copyright 2014 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.optaplanner.examples.cheaptime.domain;

import com.thoughtworks.xstream.annotations.XStreamAlias;
import org.optaplanner.core.api.domain.entity.PlanningEntity;
import org.optaplanner.core.api.domain.valuerange.CountableValueRange;
import org.optaplanner.core.api.domain.valuerange.ValueRangeFactory;
import org.optaplanner.core.api.domain.valuerange.ValueRangeProvider;
import org.optaplanner.core.api.domain.variable.PlanningVariable;
import org.optaplanner.examples.cheaptime.domain.solver.TaskAssignmentDifficultyComparator;
import org.optaplanner.examples.common.domain.AbstractPersistable;

@PlanningEntity(difficultyComparatorClass = TaskAssignmentDifficultyComparator.class)
@XStreamAlias("CtTaskAssignment")
public class TaskAssignment extends AbstractPersistable {

    private Task task;

    // Planning variables: changes during planning, between score calculations.
    private Machine machine;
    private Integer startPeriod;

    public Task getTask() {
        return task;
    }

    public void setTask(Task task) {
        this.task = task;
    }

    @PlanningVariable(valueRangeProviderRefs = {"machineRange"})
    public Machine getMachine() {
        return machine;
    }

    public void setMachine(Machine machine) {
        this.machine = machine;
    }

    @PlanningVariable(valueRangeProviderRefs = {"startPeriodRange"})
    public Integer getStartPeriod() {
        return startPeriod;
    }

    public void setStartPeriod(Integer startPeriod) {
        this.startPeriod = startPeriod;
    }

    // ************************************************************************
    // Complex methods
    // ************************************************************************

    /**
     * The startPeriod is included and the endPeriod is excluded.
     * @return null if {@link #getStartPeriod()} is null
     */
    public Integer getEndPeriod() {
        if (startPeriod == null) {
            return null;
        }
        return startPeriod + task.getDuration();
    }

    public String getLabel() {
        return task.getLabel();
    }

    // ************************************************************************
    // Ranges
    // ************************************************************************

    @ValueRangeProvider(id = "startPeriodRange")
    public CountableValueRange<Integer> getStartPeriodRange() {
        return ValueRangeFactory.createIntValueRange(task.getStartPeriodRangeFrom(), task.getStartPeriodRangeTo());
    }

}


/*
 * Copyright 2014 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.optaplanner.examples.cheaptime.solver.score;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import org.optaplanner.core.api.score.buildin.hardmediumsoftlong.HardMediumSoftLongScore;
import org.optaplanner.core.api.score.constraint.ConstraintMatchTotal;
import org.optaplanner.core.api.score.constraint.Indictment;
import org.optaplanner.core.impl.score.director.incremental.AbstractIncrementalScoreCalculator;
import org.optaplanner.core.impl.score.director.incremental.ConstraintMatchAwareIncrementalScoreCalculator;
import org.optaplanner.examples.cheaptime.domain.CheapTimeSolution;
import org.optaplanner.examples.cheaptime.domain.Machine;
import org.optaplanner.examples.cheaptime.domain.PeriodPowerPrice;
import org.optaplanner.examples.cheaptime.domain.Resource;
import org.optaplanner.examples.cheaptime.domain.Task;
import org.optaplanner.examples.cheaptime.domain.TaskAssignment;
import org.optaplanner.examples.cheaptime.domain.TaskRequirement;
import org.optaplanner.examples.cheaptime.solver.CheapTimeCostCalculator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class CheapTimeIncrementalScoreCalculator extends AbstractIncrementalScoreCalculator<CheapTimeSolution>
        implements ConstraintMatchAwareIncrementalScoreCalculator<CheapTimeSolution> {

    protected static final String CONSTRAINT_PACKAGE = "org.optaplanner.examples.cheaptime.solver";

    protected final transient Logger logger = LoggerFactory.getLogger(getClass());

    private CheapTimeSolution cheapTimeSolution;

    private int resourceListSize;
    private int globalPeriodRangeTo;
    private MachinePeriodPart[][] machineToMachinePeriodListMap; // Map<List<>> replaced by array[][] for performance
    private MachinePeriodPart[] unassignedMachinePeriodList; // List<> replaced by array[] for performance

    private long hardScore;
    private long mediumScore;
    private long softScore;

    private Machine oldMachine = null;
    private Integer oldStartPeriod = null;

    // ************************************************************************
    // Lifecycle methods
    // ************************************************************************

    @Override
    public void resetWorkingSolution(CheapTimeSolution solution) {
        this.cheapTimeSolution = solution;
        hardScore = 0L;
        mediumScore = 0L;
        softScore = 0L;
        if (solution.getGlobalPeriodRangeFrom() != 0) {
            throw new IllegalStateException("The globalPeriodRangeFrom (" + solution.getGlobalPeriodRangeFrom()
                    + ") should be 0.");
        }
        resourceListSize = solution.getResourceList().size();
        globalPeriodRangeTo = solution.getGlobalPeriodRangeTo();
        List<Machine> machineList = solution.getMachineList();
        List<PeriodPowerPrice> periodPowerPriceList = solution.getPeriodPowerPriceList();
        machineToMachinePeriodListMap = new MachinePeriodPart[machineList.size()][];
        for (Machine machine : machineList) {
            MachinePeriodPart[] machinePeriodList = new MachinePeriodPart[globalPeriodRangeTo];
            for (int period = 0; period < globalPeriodRangeTo; period++) {
                machinePeriodList[period] = new MachinePeriodPart(machine, periodPowerPriceList.get(period));
            }
            machineToMachinePeriodListMap[machine.getIndex()] = machinePeriodList;
        }
        unassignedMachinePeriodList = new MachinePeriodPart[globalPeriodRangeTo];
        for (int period = 0; period < globalPeriodRangeTo; period++) {
            unassignedMachinePeriodList[period] = new MachinePeriodPart(null, periodPowerPriceList.get(period));
        }
        for (TaskAssignment taskAssignment : solution.getTaskAssignmentList()) {
            // Do not do modifyMachine(taskAssignment, null, taskAssignment.getMachine());
            // because modifyStartPeriod does all its effects too
            modifyStartPeriod(taskAssignment, null, taskAssignment.getStartPeriod());
        }
    }

    @Override
    public void beforeEntityAdded(Object entity) {
        // Do nothing
    }

    @Override
    public void afterEntityAdded(Object entity) {
        TaskAssignment taskAssignment = (TaskAssignment) entity;
        // Do not do modifyMachine(taskAssignment, null, taskAssignment.getMachine());
        // because modifyStartPeriod does all its effects too
        modifyStartPeriod(taskAssignment, null, taskAssignment.getStartPeriod());
    }

    @Override
    public void beforeVariableChanged(Object entity, String variableName) {
        TaskAssignment taskAssignment = (TaskAssignment) entity;
        switch (variableName) {
            case "machine":
                oldMachine = taskAssignment.getMachine();
                break;
            case "startPeriod":
                oldStartPeriod = taskAssignment.getStartPeriod();
                break;
            default:
                throw new IllegalArgumentException("The variableName (" + variableName + ") is not supported.");
        }
    }

    @Override
    public void afterVariableChanged(Object entity, String variableName) {
        TaskAssignment taskAssignment = (TaskAssignment) entity;
        switch (variableName) {
            case "machine":
                modifyMachine(taskAssignment, oldMachine, taskAssignment.getMachine());
                break;
            case "startPeriod":
                modifyStartPeriod(taskAssignment, oldStartPeriod, taskAssignment.getStartPeriod());
                break;
            default:
                throw new IllegalArgumentException("The variableName (" + variableName + ") is not supported.");
        }
    }

    @Override
    public void beforeEntityRemoved(Object entity) {
        TaskAssignment taskAssignment = (TaskAssignment) entity;
        oldMachine = taskAssignment.getMachine();
        oldStartPeriod = taskAssignment.getStartPeriod();
    }

    @Override
    public void afterEntityRemoved(Object entity) {
        TaskAssignment taskAssignment = (TaskAssignment) entity;
        // Do not do modifyMachine(taskAssignment, oldMachine, null);
        // because modifyStartPeriod does all its effects too
        modifyStartPeriod(taskAssignment, oldStartPeriod, null);
    }

    // ************************************************************************
    // Modify methods
    // ************************************************************************

    private void modifyMachine(TaskAssignment taskAssignment, Machine oldMachine, Machine newMachine) {
        if (Objects.equals(oldMachine, newMachine)) {
            return;
        }
        Integer startPeriod = taskAssignment.getStartPeriod();
        if (startPeriod == null) {
            return;
        }
        Integer endPeriod = taskAssignment.getEndPeriod();
        if (oldMachine != null) {
            MachinePeriodPart[] machinePeriodList = machineToMachinePeriodListMap[oldMachine.getIndex()];
            retractRange(taskAssignment, machinePeriodList, startPeriod, endPeriod, false);
        }
        if (newMachine != null) {
            MachinePeriodPart[] machinePeriodList = machineToMachinePeriodListMap[newMachine.getIndex()];
            insertRange(taskAssignment, machinePeriodList, startPeriod, endPeriod, false);
        }
    }

    private void modifyStartPeriod(TaskAssignment taskAssignment, Integer oldStartPeriod, Integer newStartPeriod) {
        if (Objects.equals(oldStartPeriod, newStartPeriod)) {
            return;
        }
        Task task = taskAssignment.getTask();
        int duration = task.getDuration();
        int retractStart;
        int retractEnd;
        int insertStart;
        int insertEnd;
        if (oldStartPeriod == null) {
            retractStart = -1;
            retractEnd = -1;
            insertStart = newStartPeriod;
            insertEnd = insertStart + duration;
        } else if (newStartPeriod == null) {
            retractStart = oldStartPeriod;
            retractEnd = retractStart + duration;
            insertStart = -1;
            insertEnd = -1;
        } else {
            retractStart = oldStartPeriod;
            retractEnd = retractStart + duration;
            insertStart = newStartPeriod;
            insertEnd = insertStart + duration;
            if (oldStartPeriod < newStartPeriod) {
                if (insertStart < retractEnd) {
                    int overlap = retractEnd - insertStart;
                    retractEnd -= overlap;
                    insertStart += overlap;
                }
            } else {
                if (retractStart < insertEnd) {
                    int overlap = insertEnd - retractStart;
                    insertEnd -= overlap;
                    retractStart += overlap;
                }
            }
        }
        if (oldStartPeriod != null) {
            softScore += oldStartPeriod;
        }
        if (newStartPeriod != null) {
            softScore -= newStartPeriod;
        }
        Machine machine = taskAssignment.getMachine();
        MachinePeriodPart[] machinePeriodList;
        if (machine != null) {
            machinePeriodList = machineToMachinePeriodListMap[machine.getIndex()];
        } else {
            machinePeriodList = unassignedMachinePeriodList;
        }
        if (retractStart != retractEnd) {
            retractRange(taskAssignment, machinePeriodList, retractStart, retractEnd, true);
        }
        if (insertStart != insertEnd) {
            insertRange(taskAssignment, machinePeriodList, insertStart, insertEnd, true);
        }
    }

    private void retractRange(TaskAssignment taskAssignment, MachinePeriodPart[] machinePeriodList,
            int startPeriod, int endPeriod, boolean retractTaskCost) {
        long powerConsumptionMicros = taskAssignment.getTask().getPowerConsumptionMicros();
        long spinUpDownCostMicros = taskAssignment.getMachine().getSpinUpDownCostMicros();

        MachinePeriodStatus previousStatus;
        int idlePeriodStart = Integer.MIN_VALUE;
        long idleAvailable;
        if (startPeriod == 0) {
            previousStatus = MachinePeriodStatus.OFF;
            idleAvailable = Long.MIN_VALUE;
        } else {
            previousStatus = machinePeriodList[startPeriod - 1].status;
            if (previousStatus == MachinePeriodStatus.IDLE) {
                idleAvailable = spinUpDownCostMicros;
                for (int i = startPeriod - 1; i >= 0; i--) {
                    MachinePeriodPart machinePeriod = machinePeriodList[i];
                    if (machinePeriod.status.isActive()) {
                        idlePeriodStart = i + 1;
                        break;
                    }
                    machinePeriod.undoMakeIdle();
                    idleAvailable -= machinePeriod.machineCostMicros;
                }
                if (idleAvailable < 0L) {
                    throw new IllegalStateException("The range of idlePeriodStart (" + idlePeriodStart
                            + ") to startPeriod (" + startPeriod
                            + ") should have been IDLE because the idleAvailable (" + idleAvailable
                            + ") is negative.");
                }
            } else {
                idleAvailable = Long.MIN_VALUE;
            }
        }
        for (int i = startPeriod; i < endPeriod; i++) {
            MachinePeriodPart machinePeriod = machinePeriodList[i];
            machinePeriod.retractTaskAssignment(taskAssignment);
            if (retractTaskCost) {
                mediumScore += CheapTimeCostCalculator.multiplyTwoMicros(powerConsumptionMicros,
                        machinePeriod.periodPowerPriceMicros);
            }
            // SpinUp vs idle
            if (machinePeriod.status.isActive()) {
                if (previousStatus == MachinePeriodStatus.OFF) {
                    // Only if (startPeriod == i), it could be SPIN_UP_AND_ACTIVE
                    if (machinePeriod.status != MachinePeriodStatus.SPIN_UP_AND_ACTIVE) {
                        machinePeriod.spinUp();
                    }
                } else if (previousStatus == MachinePeriodStatus.IDLE) {
                    // Create idle period
                    for (int j = idlePeriodStart; j < i; j++) {
                        machinePeriodList[j].makeIdle();
                    }
                    idlePeriodStart = Integer.MIN_VALUE;
                    idleAvailable = Long.MIN_VALUE;
                }
                previousStatus = MachinePeriodStatus.STILL_ACTIVE;
            } else if (machinePeriod.status == MachinePeriodStatus.OFF) {
                if (previousStatus != MachinePeriodStatus.OFF) {
                    if (previousStatus.isActive()) {
                        idlePeriodStart = i;
                        idleAvailable = spinUpDownCostMicros;
                    }
                    idleAvailable -= machinePeriod.machineCostMicros;
                    if (idleAvailable < 0) {
                        previousStatus = MachinePeriodStatus.OFF;
                        idlePeriodStart = Integer.MIN_VALUE;
                        idleAvailable = Long.MIN_VALUE;
                    } else {
                        previousStatus = MachinePeriodStatus.IDLE;
                    }
                }
            } else {
                throw new IllegalStateException("Impossible status (" + machinePeriod.status + ").");
            }
        }
        if (endPeriod < globalPeriodRangeTo && machinePeriodList[endPeriod].status != MachinePeriodStatus.OFF
                && !previousStatus.isActive()) {
            for (int i = endPeriod; i < globalPeriodRangeTo; i++) {
                MachinePeriodPart machinePeriod = machinePeriodList[i];
                if (machinePeriod.status.isActive()) {
                    if (previousStatus == MachinePeriodStatus.OFF) {
                        machinePeriod.spinUp();
                    } else if (previousStatus == MachinePeriodStatus.IDLE) {
                        // Create idle period
                        for (int j = idlePeriodStart; j < i; j++) {
                            machinePeriodList[j].makeIdle();
                        }
                    }
                    break;
                } else if (machinePeriod.status == MachinePeriodStatus.IDLE) {
                    machinePeriod.undoMakeIdle();
                    if (previousStatus == MachinePeriodStatus.IDLE) {
                        idleAvailable -= machinePeriod.machineCostMicros;
                        if (idleAvailable < 0) {
                            previousStatus = MachinePeriodStatus.OFF;
                            idlePeriodStart = Integer.MIN_VALUE;
                            idleAvailable = Long.MIN_VALUE;
                        }
                    }
                } else {
                    throw new IllegalStateException("Impossible status (" + machinePeriod.status + ").");
                }
            }
        }
    }

    private void insertRange(TaskAssignment taskAssignment, MachinePeriodPart[] machinePeriodList,
            int startPeriod, int endPeriod, boolean insertTaskCost) {
        long powerConsumptionMicros = taskAssignment.getTask().getPowerConsumptionMicros();
        MachinePeriodPart startMachinePeriod = machinePeriodList[startPeriod];
        boolean startIsOff = startMachinePeriod.status == MachinePeriodStatus.OFF;
        boolean lastIsOff = machinePeriodList[endPeriod - 1].status == MachinePeriodStatus.OFF;
        for (int i = startPeriod; i < endPeriod; i++) {
            MachinePeriodPart machinePeriod = machinePeriodList[i];
            machinePeriod.insertTaskAssignment(taskAssignment);
            if (insertTaskCost) {
                mediumScore -= CheapTimeCostCalculator.multiplyTwoMicros(powerConsumptionMicros,
                        machinePeriod.periodPowerPriceMicros);
            }
            // SpinUp vs idle
            if (machinePeriod.status == MachinePeriodStatus.SPIN_UP_AND_ACTIVE && i != startPeriod) {
                machinePeriod.undoSpinUp();
            }
        }
        // SpinUp vs idle
        if (startIsOff) {
            long idleAvailable = taskAssignment.getMachine().getSpinUpDownCostMicros();
            int idlePeriodStart = Integer.MIN_VALUE;
            for (int i = startPeriod - 1; i >= 0 && idleAvailable >= 0L; i--) {
                MachinePeriodPart machinePeriod = machinePeriodList[i];
                if (machinePeriod.status.isActive()) {
                    idlePeriodStart = i + 1;
                    break;
                }
                idleAvailable -= machinePeriod.machineCostMicros;
            }
            if (idlePeriodStart >= 0) {
                // Create idle period
                for (int i = idlePeriodStart; i < startPeriod; i++) {
                    machinePeriodList[i].makeIdle();
                }
            } else {
                startMachinePeriod.spinUp();
            }
        }
        if (lastIsOff) {
            long idleAvailable = taskAssignment.getMachine().getSpinUpDownCostMicros();
            int idlePeriodEnd = Integer.MIN_VALUE;
            for (int i = endPeriod; i < globalPeriodRangeTo && idleAvailable >= 0L; i++) {
                MachinePeriodPart machinePeriod = machinePeriodList[i];
                if (machinePeriod.status.isActive()) {
                    idlePeriodEnd = i;
                    machinePeriod.undoSpinUp();
                    break;
                }
                idleAvailable -= machinePeriod.machineCostMicros;
            }
            if (idlePeriodEnd >= 0) {
                // Create idle period
                for (int i = endPeriod; i < idlePeriodEnd; i++) {
                    machinePeriodList[i].makeIdle();
                }
            }
        }
    }

    @Override
    public HardMediumSoftLongScore calculateScore() {
        return HardMediumSoftLongScore.of(hardScore, mediumScore, softScore);
    }

    private class MachinePeriodPart {

        private final Machine machine;
        private final int period;
        private final long periodPowerPriceMicros;
        private final long machineCostMicros;

        private int taskCount;
        private MachinePeriodStatus status;
        private int[] resourceAvailableList; // List<> replaced by array[] for performance

        private MachinePeriodPart(Machine machine, PeriodPowerPrice periodPowerPrice) {
            this.machine = machine;
            this.period = periodPowerPrice.getPeriod();
            this.periodPowerPriceMicros = periodPowerPrice.getPowerPriceMicros();
            taskCount = 0;
            status = MachinePeriodStatus.OFF;
            if (machine != null) {
                resourceAvailableList = new int[resourceListSize];
                for (int i = 0; i < resourceListSize; i++) {
                    resourceAvailableList[i] = machine.getMachineCapacityList().get(i).getCapacity();
                }
                machineCostMicros = CheapTimeCostCalculator.multiplyTwoMicros(machine.getPowerConsumptionMicros(),
                        periodPowerPriceMicros);
            } else {
                machineCostMicros = Long.MIN_VALUE;
            }
        }

        public void spinUp() {
            if (status != MachinePeriodStatus.STILL_ACTIVE) {
                throw new IllegalStateException("Impossible status (" + status + ").");
            }
            mediumScore -= machine.getSpinUpDownCostMicros();
            status = MachinePeriodStatus.SPIN_UP_AND_ACTIVE;
        }

        public void undoSpinUp() {
            if (status != MachinePeriodStatus.SPIN_UP_AND_ACTIVE) {
                throw new IllegalStateException("Impossible status (" + status + ").");
            }
            mediumScore += machine.getSpinUpDownCostMicros();
            status = MachinePeriodStatus.STILL_ACTIVE;
        }

        public void makeIdle() {
            if (status != MachinePeriodStatus.OFF) {
                throw new IllegalStateException("Impossible status (" + status + ").");
            }
            mediumScore -= machineCostMicros;
            status = MachinePeriodStatus.IDLE;
        }

        public void undoMakeIdle() {
            if (status != MachinePeriodStatus.IDLE) {
                throw new IllegalStateException("Impossible status (" + status + ").");
            }
            mediumScore += machineCostMicros;
            status = MachinePeriodStatus.OFF;
        }

        public void insertTaskAssignment(TaskAssignment taskAssignment) {
            if (machine == null) {
                return;
            }
            Task task = taskAssignment.getTask();
            if (status == MachinePeriodStatus.OFF) {
                mediumScore -= machineCostMicros;
                status = MachinePeriodStatus.STILL_ACTIVE;
            } else if (status == MachinePeriodStatus.IDLE) {
                status = MachinePeriodStatus.STILL_ACTIVE;
            }
            taskCount++;
            for (int i = 0; i < resourceAvailableList.length; i++) {
                int resourceAvailable = resourceAvailableList[i];
                TaskRequirement taskRequirement = task.getTaskRequirementList().get(i);
                if (resourceAvailable < 0) {
                    hardScore -= resourceAvailable;
                }
                resourceAvailable -= taskRequirement.getResourceUsage();
                if (resourceAvailable < 0) {
                    hardScore += resourceAvailable;
                }
                resourceAvailableList[i] = resourceAvailable;
            }
        }

        public void retractTaskAssignment(TaskAssignment taskAssignment) {
            if (machine == null) {
                return;
            }
            Task task = taskAssignment.getTask();
            if (status == MachinePeriodStatus.OFF || status == MachinePeriodStatus.IDLE) {
                throw new IllegalStateException("Impossible status (" + status + ").");
            }
            taskCount--;
            if (taskCount == 0) {
                mediumScore += machineCostMicros;
                if (status == MachinePeriodStatus.SPIN_UP_AND_ACTIVE) {
                    mediumScore += machine.getSpinUpDownCostMicros();
                }
                status = MachinePeriodStatus.OFF;
            }
            for (int i = 0; i < resourceAvailableList.length; i++) {
                int resourceAvailable = resourceAvailableList[i];
                TaskRequirement taskRequirement = task.getTaskRequirementList().get(i);
                if (resourceAvailable < 0) {
                    hardScore -= resourceAvailable;
                }
                resourceAvailable += taskRequirement.getResourceUsage();
                if (resourceAvailable < 0) {
                    hardScore += resourceAvailable;
                }
                resourceAvailableList[i] = resourceAvailable;
            }
        }

        @Override
        public String toString() {
            return status.name() + " (" + taskCount + " tasks)";
        }

    }

    // ************************************************************************
    // ConstraintMatchAwareIncrementalScoreCalculator methods
    // ************************************************************************

    @Override
    public void resetWorkingSolution(CheapTimeSolution workingSolution, boolean constraintMatchEnabled) {
        resetWorkingSolution(workingSolution);
        // ignore constraintMatchEnabled, it is always presumed enabled
    }

    @Override
    public Collection<ConstraintMatchTotal> getConstraintMatchTotals() {
        List<Resource> resourceList = cheapTimeSolution.getResourceList();
        ConstraintMatchTotal resourceCapacityMatchTotal = new ConstraintMatchTotal(
                CONSTRAINT_PACKAGE, "resourceCapacity", HardMediumSoftLongScore.ZERO);
        ConstraintMatchTotal spinUpDownMatchTotal = new ConstraintMatchTotal(
                CONSTRAINT_PACKAGE, "spinUpDown", HardMediumSoftLongScore.ZERO);
        ConstraintMatchTotal machineConsumptionMatchTotal = new ConstraintMatchTotal(
                CONSTRAINT_PACKAGE, "machineConsumption", HardMediumSoftLongScore.ZERO);
        ConstraintMatchTotal taskConsumptionMatchTotal = new ConstraintMatchTotal(
                CONSTRAINT_PACKAGE, "taskConsumption", HardMediumSoftLongScore.ZERO);
        ConstraintMatchTotal minimizeTaskStartPeriodMatchTotal = new ConstraintMatchTotal(
                CONSTRAINT_PACKAGE, "minimizeTaskStartPeriod", HardMediumSoftLongScore.ZERO);
        long taskConsumptionWeight = mediumScore;
        for (Machine machine : cheapTimeSolution.getMachineList()) {
            for (int period = 0; period < globalPeriodRangeTo; period++) {
                MachinePeriodPart machinePeriod = machineToMachinePeriodListMap[machine.getIndex()][period];
                for (int i = 0; i < machinePeriod.resourceAvailableList.length; i++) {
                    int resourceAvailable = machinePeriod.resourceAvailableList[i];
                    if (resourceAvailable < 0) {
                        resourceCapacityMatchTotal.addConstraintMatch(
                                Arrays.asList(machine, period, resourceList.get(i)),
                                HardMediumSoftLongScore.of(resourceAvailable, 0, 0));
                    }
                }
                if (machinePeriod.status == MachinePeriodStatus.SPIN_UP_AND_ACTIVE) {
                    spinUpDownMatchTotal.addConstraintMatch(
                            Arrays.asList(machine, period),
                            HardMediumSoftLongScore.of(0, - machine.getSpinUpDownCostMicros(), 0));
                    taskConsumptionWeight += machine.getSpinUpDownCostMicros();
                }
                if (machinePeriod.status != MachinePeriodStatus.OFF) {
                    machineConsumptionMatchTotal.addConstraintMatch(
                            Arrays.asList(machine, period),
                            HardMediumSoftLongScore.of(0, - machinePeriod.machineCostMicros, 0));
                    taskConsumptionWeight += machinePeriod.machineCostMicros;
                }
            }
        }
        // Individual taskConsumption isn't tracked for performance
        taskConsumptionMatchTotal.addConstraintMatch(
                Arrays.asList(),
                HardMediumSoftLongScore.of(0, taskConsumptionWeight, 0));
        // Individual taskStartPeriod isn't tracked for performance
        // but we mimic it
        for (TaskAssignment taskAssignment : cheapTimeSolution.getTaskAssignmentList()) {
            Integer startPeriod = taskAssignment.getStartPeriod();
            if (startPeriod != null) {
                minimizeTaskStartPeriodMatchTotal.addConstraintMatch(
                        Arrays.asList(taskAssignment),
                        HardMediumSoftLongScore.of(0, 0, - startPeriod));
            }

        }

        List<ConstraintMatchTotal> constraintMatchTotalList = new ArrayList<>(4);
        constraintMatchTotalList.add(resourceCapacityMatchTotal);
        constraintMatchTotalList.add(spinUpDownMatchTotal);
        constraintMatchTotalList.add(machineConsumptionMatchTotal);
        constraintMatchTotalList.add(taskConsumptionMatchTotal);
        constraintMatchTotalList.add(minimizeTaskStartPeriodMatchTotal);
        return constraintMatchTotalList;
    }

    @Override
    public Map<Object, Indictment> getIndictmentMap() {
        return null; // Calculate it non-incrementally from getConstraintMatchTotals()
    }

    private enum MachinePeriodStatus {
        OFF,
        IDLE,
        SPIN_UP_AND_ACTIVE,
        STILL_ACTIVE;

        public boolean isActive() {
            return this == MachinePeriodStatus.STILL_ACTIVE || this == MachinePeriodStatus.SPIN_UP_AND_ACTIVE;
        }

    }

}