//   Copyright 2012 Georg-August-Universität Göttingen, Germany
//
//   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 de.ugoe.cs.autoquest.ui.swt;

import java.awt.BasicStroke;
import java.awt.Graphics2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Rectangle2D;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.text.DecimalFormat;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.AxisLocation;
import org.jfree.chart.axis.LogarithmicAxis;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.ValueMarker;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.chart.title.TextTitle;
import org.jfree.data.xy.DefaultTableXYDataset;
import org.jfree.data.xy.XYDataset;
import org.jfree.data.xy.XYSeries;
import org.jfree.ui.RectangleAnchor;
import org.jfree.ui.RectangleInsets;

import com.lowagie.text.Rectangle;
import com.lowagie.text.pdf.DefaultFontMapper;
import com.lowagie.text.pdf.PdfContentByte;
import com.lowagie.text.pdf.PdfTemplate;
import com.lowagie.text.pdf.PdfWriter;

import de.ugoe.cs.autoquest.tasktrees.treeifc.DefaultTaskInstanceTraversingVisitor;
import de.ugoe.cs.autoquest.tasktrees.treeifc.IEventTask;
import de.ugoe.cs.autoquest.tasktrees.treeifc.IEventTaskInstance;
import de.ugoe.cs.autoquest.tasktrees.treeifc.ISequence;
import de.ugoe.cs.autoquest.tasktrees.treeifc.ITask;
import de.ugoe.cs.autoquest.tasktrees.treeifc.ITaskInfo;
import de.ugoe.cs.autoquest.tasktrees.treeifc.ITaskInstance;
import de.ugoe.cs.autoquest.tasktrees.treeifc.ITaskModel;
import de.ugoe.cs.autoquest.tasktrees.treeifc.TaskMetric;
import de.ugoe.cs.autoquest.tasktrees.treeifc.TaskTreeUtils;

/**
 * <p>
 * TODO comment
 * </p>
 * 
 * @author Patrick Harms
 */
public class TaskTreePlotUtils {
    
    /** name for visualizing a task count to event coverage plot */
    public static final String TASK_COUNT__EVENT_COVERAGE_PLOT = "event coverage plot";
    
    /** name for visualizing the instance counts of tasks ordered by the event coverage */
    public static final String ORDERED_TASK_COUNT_PLOT = "task instance count - coverage plot";
    
    /** name for visualizing a cumulative coverage of events by the tasks orded by their event
     *  coverage */
    public static final String CUMULATIVE_TASK_COVERAGE_PLOT = "cumulative task coverage plot";
    
    /** colors */
    public static final java.awt.Color[] colors = new java.awt.Color[] {
        java.awt.Color.BLACK, java.awt.Color.GRAY, java.awt.Color.RED, java.awt.Color.CYAN,
        java.awt.Color.ORANGE, java.awt.Color.GREEN, java.awt.Color.MAGENTA, java.awt.Color.BLUE,
        java.awt.Color.PINK, java.awt.Color.DARK_GRAY, java.awt.Color.LIGHT_GRAY,
        java.awt.Color.YELLOW
    };
    
    /**
     *
     */
    public static JFreeChart createPlot(String           name,
                                        List<ITaskModel> models,
                                        List<String>     modelNames,
                                        List<Integer>    groups)
    {
        JFreeChart chart = null;
        
        if (TASK_COUNT__EVENT_COVERAGE_PLOT.equals(name)) {
            XYDataset dataset1 = createTaskCountEventCoveragePlotDataSet(models.get(0));
            
            chart = ChartFactory.createXYLineChart
                (TASK_COUNT__EVENT_COVERAGE_PLOT, "task counts", "event coverage",
                 dataset1, PlotOrientation.VERTICAL, models.size() > 1, false, false);
            
            for (int i = 1; i < models.size(); i++) {
                chart.getXYPlot().setDataset
                    (i, createTaskCountEventCoveragePlotDataSet(models.get(i)));
            }
            
            XYPlot plot = chart.getXYPlot();
            NumberAxis domainAxis = new LogarithmicAxis("task counts");
            NumberAxis rangeAxis = new LogarithmicAxis("event coverage");
            plot.setDomainAxis(domainAxis);
            plot.setRangeAxis(rangeAxis);
        }
        else if (CUMULATIVE_TASK_COVERAGE_PLOT.equals(name)) {
            Set<ISequence> mostProminent =
                TaskTreeUtils.getMostProminentTasks(models.get(0), models.get(0).getTasks());
            
            XYDataset dataset = createCumulativeTaskCoveragePlotDataSet(models.get(0),
                                                                        modelNames.get(0));
            chart = ChartFactory.createXYLineChart
                (CUMULATIVE_TASK_COVERAGE_PLOT, "tasks ordered by coverage",
                 "cumulative action instance coverage", dataset,
                 PlotOrientation.VERTICAL, models.size() > 1, false, false);
            
            final XYPlot plot = chart.getXYPlot();
            plot.setBackgroundPaint(java.awt.Color.WHITE);
            plot.setDomainAxisLocation(AxisLocation.BOTTOM_OR_RIGHT);
            
            double mpMarker = (double) 100 * mostProminent.size() / dataset.getItemCount(0);
            ValueMarker marker = new ValueMarker(mpMarker);
            marker.setLabel(new DecimalFormat("#0.0").format(mpMarker));
            marker.setLabelAnchor(RectangleAnchor.BOTTOM);
            marker.setLabelOffset(new RectangleInsets(0, -12, 10, 15));
            plot.addDomainMarker(marker);
            
            double xLow = 0;
            double yLow = 0;
            double xHigh = 100;
            double yHigh = 100;
            double maxY = 0;
            for (int i = 0; i < dataset.getItemCount(0); i++) {
                double xValue = dataset.getXValue(0, i);
                double yValue = dataset.getYValue(0, i);
                
                if ((xLow < xValue) && (xValue < mpMarker)) {
                    xLow = xValue;
                    yLow = yValue;
                }
                else if ((xHigh > xValue) && (xValue > mpMarker)) {
                    xHigh = xValue;
                    yHigh = yValue;
                }
                
                maxY = Math.max(maxY, yValue);
            }
            
            marker = new ValueMarker((yHigh + yLow) / 2);
            marker.setLabel(new DecimalFormat("#0.0").format((yHigh + yLow) / 2));
            marker.setLabelOffset(new RectangleInsets(5, 15, 0, 10));
            plot.addRangeMarker(marker);
            marker = new ValueMarker(maxY);
            marker.setLabel(new DecimalFormat("#0.0").format(maxY));
            marker.setLabelOffset(new RectangleInsets(5, 15, 0, 10));
            plot.addRangeMarker(marker);
            
            plot.getRenderer(0).setSeriesPaint(0, colors[0]);
            plot.getRenderer(0).setSeriesStroke(0, new BasicStroke(3.0f, BasicStroke.CAP_ROUND,
                                                                   BasicStroke.JOIN_ROUND));
            
            for (int i = 1; i < models.size(); i++) {
                plot.setDataset(i, createCumulativeTaskCoveragePlotDataSet(models.get(i),
                                                                           modelNames.get(i)));
                
                plot.setRenderer(i, new XYLineAndShapeRenderer(true, false));
                plot.getRenderer(i).setSeriesPaint(0, colors[groups.get(i)]);
                plot.getRenderer(i).setSeriesStroke(0, new BasicStroke(1.0f, BasicStroke.CAP_ROUND,
                                                                       BasicStroke.JOIN_ROUND));
            }

            NumberAxis domainAxis = new NumberAxis("sequences ordered by coverage");
            domainAxis.setUpperBound(100);
            plot.setDomainAxis(domainAxis);
            
            NumberAxis rangeAxis = new NumberAxis("cumulative action instance coverage");
            rangeAxis.setUpperBound(100);
            plot.setRangeAxis(rangeAxis);
        }
        else if (ORDERED_TASK_COUNT_PLOT.equals(name)) {
            XYDataset dataset = createOrderedTaskCountPlotDataSet(models.get(0), modelNames.get(0));
            
            chart = ChartFactory.createXYLineChart
                (ORDERED_TASK_COUNT_PLOT, "tasks ordered by coverage",
                 "number of instances", dataset, PlotOrientation.VERTICAL, models.size() > 1,
                 false, false);
            
            final XYPlot plot = chart.getXYPlot();
            plot.setBackgroundPaint(java.awt.Color.WHITE);
            plot.setDomainAxisLocation(AxisLocation.BOTTOM_OR_RIGHT);

            XYLineAndShapeRenderer scatterRenderer = new XYLineAndShapeRenderer(false, true);
            scatterRenderer.setSeriesShapesFilled(0, false);
            scatterRenderer.setSeriesShape(0, new Ellipse2D.Float(-3.0f, -3.0f, 6.0f, 6.0f));
            scatterRenderer.setSeriesPaint(0, colors[0]);
            plot.setRenderer(0, scatterRenderer);

            for (int i = 1; i < models.size(); i++) {
                chart.getXYPlot().setDataset
                    (i, createOrderedTaskCountPlotDataSet(models.get(i), modelNames.get(i)));
                
                scatterRenderer = new XYLineAndShapeRenderer(false, true);
                scatterRenderer.setSeriesShapesFilled(0, false);
                scatterRenderer.setSeriesShape(0, new Ellipse2D.Float(-3.0f, -3.0f, 6.0f, 6.0f));
                scatterRenderer.setSeriesPaint(0, colors[i]);
                plot.setRenderer(i, scatterRenderer);
            }

            NumberAxis domainAxis = new NumberAxis("sequences ordered by coverage");
            domainAxis.setUpperBound(100);
            plot.setDomainAxis(domainAxis);
            
            NumberAxis rangeAxis = new LogarithmicAxis("number of instances");
            plot.setRangeAxis(rangeAxis);
        }

        return chart;
    }
    
    /**
     * 
     */
    public static void saveChartToPDF(JFreeChart chart, String fileName, int width, int height)
        throws Exception
    {
        if (chart != null) {
            BufferedOutputStream out = null;
            try {
                out = new BufferedOutputStream(new FileOutputStream(fileName)); 
                    
                //convert chart to PDF with iText:
                Rectangle pagesize = new Rectangle(width, height); 
                com.lowagie.text.Document document =
                    new com.lowagie.text.Document(pagesize, 50, 50, 50, 50);
                
                try { 
                    PdfWriter writer = PdfWriter.getInstance(document, out); 
                    document.addAuthor("JFreeChart"); 
                    document.open(); 
            
                    PdfContentByte cb = writer.getDirectContent(); 
                    PdfTemplate tp = cb.createTemplate(width, height); 
                    Graphics2D g2 = tp.createGraphics(width, height, new DefaultFontMapper()); 
            
                    Rectangle2D r2D = new Rectangle2D.Double(0, 0, width, height);
                    TextTitle title = chart.getTitle();
                    chart.setTitle((TextTitle) null);
                    chart.draw(g2, r2D, null);
                    chart.setTitle(title);
                    g2.dispose(); 
                    cb.addTemplate(tp, 0, 0); 
                }
                finally {
                    document.close(); 
                }
            }
            finally {
                if (out != null) {
                    out.close();
                }
            }
        }
    }

    /**
     *
     */
    private static XYDataset createTaskCountEventCoveragePlotDataSet(ITaskModel taskModel) {
        // tree maps required to have sorted maps
        TreeMap<Integer, Integer> minValues = new TreeMap<>();
        TreeMap<Integer, List<Integer>> allValues = new TreeMap<>();
        TreeMap<Integer, Integer> maxValues = new TreeMap<>();
        
        for (ITask task : taskModel.getTasks()) {
            if (task instanceof ISequence) {
                ITaskInfo info = taskModel.getTaskInfo(task);
                int count = task.getInstances().size();
                int eventCoverage = info.getMeasureValue(TaskMetric.EVENT_COVERAGE);

                Integer minValue = minValues.get(count);

                if ((minValue == null) || (minValue > eventCoverage)) {
                    minValues.put(count, eventCoverage);
                }

                Integer maxValue = maxValues.get(count);

                if ((maxValue == null) || (maxValue < eventCoverage)) {
                    maxValues.put(count, eventCoverage);
                }

                List<Integer> values = allValues.get(count);

                if (values == null) {
                    values = new LinkedList<Integer>();
                    allValues.put(count, values);
                }

                values.add(eventCoverage);
            }
        }
        
        DefaultTableXYDataset dataset = new DefaultTableXYDataset();
        XYSeries minSeries = new XYSeries("min values", true, false);
        XYSeries avgSeries = new XYSeries("avg values", true, false);
        XYSeries maxSeries = new XYSeries("max values", true, false);
        
        // entries are correctly sorted due to the map
        for (Map.Entry<Integer, List<Integer>> entry : allValues.entrySet()) {
            int count = entry.getKey();

            minSeries.add(count, minValues.get(count));
            maxSeries.add(count, maxValues.get(count));
            
            avgSeries.add(count, getAverage(entry.getValue()));
        }
        
        dataset.addSeries(minSeries);
        dataset.addSeries(avgSeries);
        dataset.addSeries(maxSeries);
        return dataset;
    }

    /**
     *
     */
    private static XYDataset createCumulativeTaskCoveragePlotDataSet(ITaskModel     taskModel,
                                                                     String         taskModelName)
    {
        Map<Integer, List<ISequence>> coverageCounts = new HashMap<>();
        Map<ISequence, Set<IEventTaskInstance>> coverages = new HashMap<>();
        Set<IEventTaskInstance> allEvents = new HashSet<>();
        int maxCoverage = 0;
        
        for (ITask task : taskModel.getTasks()) {
            if (task instanceof ISequence) {
                final Set<IEventTaskInstance> coveredEvents = new HashSet<>();
                
                for (ITaskInstance instance : task.getInstances()) {
                    instance.accept(new DefaultTaskInstanceTraversingVisitor() {
                        @Override
                        public void visit(IEventTaskInstance eventTaskInstance) {
                            coveredEvents.add(eventTaskInstance);
                        }
                    });
                }
                
                coverages.put((ISequence) task, coveredEvents);
                
                List<ISequence> tasksWithSameCoverage = coverageCounts.get(coveredEvents.size());

                if (tasksWithSameCoverage == null) {
                    tasksWithSameCoverage = new LinkedList<>();
                    coverageCounts.put(coveredEvents.size(), tasksWithSameCoverage);
                }

                tasksWithSameCoverage.add((ISequence) task);
                
                maxCoverage = Math.max(maxCoverage, coveredEvents.size());
            }
            else if (task instanceof IEventTask) {
                for (ITaskInstance instance : task.getInstances()) {
                    allEvents.add((IEventTaskInstance) instance);
                }
            }
        }
        
        XYSeries cumulativeSeries = new XYSeries(taskModelName, true, false);
        Set<IEventTaskInstance> cumulativeCoverage = new HashSet<>();
        
        // entries are correctly sorted due to the map
        int sequenceIndex = 1;
        
        for (int i = maxCoverage; i > 0; i--) {
            List<ISequence> sequencesWithSameCoverage = coverageCounts.get(i);

            if (sequencesWithSameCoverage == null) {
                continue;
            }
            
            for (ISequence sequence : sequencesWithSameCoverage) {
                cumulativeCoverage.addAll(coverages.get(sequence));
                double xvalue = (double) 100 * sequenceIndex / coverages.size();
                double yvalue = (double) 100 * cumulativeCoverage.size() / allEvents.size();
                cumulativeSeries.add(xvalue, yvalue);
                sequenceIndex++;
            }
        }
        
        DefaultTableXYDataset dataset = new DefaultTableXYDataset();
        dataset.addSeries(cumulativeSeries);
        return dataset;
    }

    /**
     *
     */
    private static XYDataset createOrderedTaskCountPlotDataSet(ITaskModel taskModel,
                                                               String     taskModelName)
    {
        Map<Integer, List<ISequence>> coverageCounts = new HashMap<>();
        Map<ISequence, Set<IEventTaskInstance>> coverages = new HashMap<>();
        int maxCoverage = 0;
        
        for (ITask task : taskModel.getTasks()) {
            if (task instanceof ISequence) {
                final Set<IEventTaskInstance> coveredEvents = new HashSet<>();
                
                for (ITaskInstance instance : task.getInstances()) {
                    instance.accept(new DefaultTaskInstanceTraversingVisitor() {
                        @Override
                        public void visit(IEventTaskInstance eventTaskInstance) {
                            coveredEvents.add(eventTaskInstance);
                        }
                    });
                }
                
                coverages.put((ISequence) task, coveredEvents);
                
                List<ISequence> tasksWithSameCoverage = coverageCounts.get(coveredEvents.size());

                if (tasksWithSameCoverage == null) {
                    tasksWithSameCoverage = new LinkedList<>();
                    coverageCounts.put(coveredEvents.size(), tasksWithSameCoverage);
                }

                tasksWithSameCoverage.add((ISequence) task);
                
                maxCoverage = Math.max(maxCoverage, coveredEvents.size());
            }
        }
        
        XYSeries counts = new XYSeries(taskModelName, true, false);
        
        // entries are correctly sorted due to the map
        int sequenceIndex = 1;
        
        for (int i = maxCoverage; i > 0; i--) {
            List<ISequence> sequencesWithSameCoverage = coverageCounts.get(i);

            if (sequencesWithSameCoverage == null) {
                continue;
            }
            
            for (ISequence sequence : sequencesWithSameCoverage) {
                double xvalue = (double) 100 * sequenceIndex / coverages.size();
                counts.add(xvalue, sequence.getInstances().size());
                sequenceIndex++;
            }
        }
        
        DefaultTableXYDataset dataset = new DefaultTableXYDataset();
        dataset.addSeries(counts);
        return dataset;
    }


    /**
     *
     */
    private static double getAverage(List<Integer> values) {
        int all = 0;
        
        for (Integer value : values) {
            all += value;
        }

        return ((double) all) / values.size();
    }
}
