/* Distributed under the Apache License, Version 2.0.
   See accompanying NOTICE file for details.*/
package com.kitware.physiology.utilities.csv.plots;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.io.File;
import java.io.IOException;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartUtilities;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.LogarithmicAxis;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.StandardXYItemRenderer;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;

import com.kitware.physiology.datamodel.substance.SESubstanceManager;
import com.kitware.physiology.utilities.DoubleUtils;
import com.kitware.physiology.utilities.FileUtils;
import com.kitware.physiology.utilities.Log;
import com.kitware.physiology.utilities.LogListener;
import com.kitware.physiology.utilities.csv.CSVContents;
import com.kitware.physiology.utilities.csv.ConvexHullMaker;
import com.kitware.physiology.utilities.csv.plots.PlotDriver.PlotJob;

public class ConvexHullPlotter implements Plotter
{

  protected Map<String,List<Double>> data = new HashMap<String,List<Double>>();
  
  public static void main(String[] args)
  {
    PlotDriver.main(args);
  }
  
  public List<List<Double>> splitHull(List<List<Double>> hullVals)
  {
    //The hull values should already be ordered such that the first point is the bottom-rightmost point (max X)
    //and subsequent points follow clockwise around the hull
    //We want two set of points, the ones that extend from the maximum X value to the minimum on bottom, and then the ones on the way back
    //So that our plot is continuous, we'll include the min and max X points in both sets
    
    List<Double> bottomXVals = new ArrayList<Double>();
    List<Double> bottomYVals = new ArrayList<Double>();
    List<Double> topXVals = new ArrayList<Double>();
    List<Double> topYVals = new ArrayList<Double>();
    List<Double> leftXVals = new ArrayList<Double>();
    List<Double> leftYVals = new ArrayList<Double>();
    List<Double> rightXVals = new ArrayList<Double>();
    List<Double> rightYVals = new ArrayList<Double>();
    List<List<Double>> splitVals = new ArrayList<List<Double>>();
    
    int minXindex = 0;
    
    for(int i = hullVals.get(0).size()-1; i >=0; i--)
    {
      if(hullVals.get(0).get(i) < hullVals.get(0).get(minXindex))
      {
        minXindex = i;
      }
    }
    
    bottomXVals.addAll(hullVals.get(0).subList(0, minXindex));
    bottomYVals.addAll(hullVals.get(1).subList(0, minXindex));
    
    //topXVals.add(hullVals.get(0).get(0));
    //topYVals.add(hullVals.get(1).get(0));
    topXVals.addAll(hullVals.get(0).subList(minXindex, hullVals.get(0).size()));
    topYVals.addAll(hullVals.get(1).subList(minXindex, hullVals.get(1).size()));
    
    leftXVals.add(bottomXVals.get(bottomXVals.size()-1));
    leftYVals.add(bottomYVals.get(bottomYVals.size()-1));
    leftXVals.add(topXVals.get(0));
    leftYVals.add(topYVals.get(0));
    
    rightXVals.add(bottomXVals.get(0));
    rightYVals.add(bottomYVals.get(0));
    rightXVals.add(topXVals.get(topXVals.size()-1));
    rightYVals.add(topYVals.get(topYVals.size()-1));
    
    splitVals.add(topXVals);
    splitVals.add(topYVals);
    splitVals.add(bottomXVals);
    splitVals.add(bottomYVals);
    splitVals.add(leftXVals);
    splitVals.add(leftYVals);
    splitVals.add(rightXVals);
    splitVals.add(rightYVals);
    
    return splitVals;
  }

  public void plot(LogListener listener, SESubstanceManager subMgr)
  {
    //fill PlotJob with needed data if it doesn't exist
    PlotJob job = (PlotJob)listener;
    if(job.dataPath == null || job.dataPath.isEmpty())
    {job.dataPath = job.verificationDirectory+"/";}
    if(job.dataFile == null || job.dataFile.isEmpty())
    {job.dataFile = job.name + "Results.csv";}
    
    //Get data contents for all headers
    if (data.isEmpty() || data == null)
    {
      try
      {
        CSVContents csv = new CSVContents(job.dataPath + job.dataFile);
        csv.abreviateContents = job.resultsSkipNum;
        for (int i = 0; i < job.headers.size(); i++)
        {
          List<Double> headerData = new ArrayList<Double>();
          csv.readHeader(csv.unitUnderscoreToSpace(job.headers.get(i)), headerData);
          data.put(job.headers.get(i), headerData);
        }        
      } catch (IOException e)
      {
        Log.error("Could not analyze file " + job.dataPath + job.dataFile);
      }
    }
    
    //Catch some errors
    if(job.Y2headers.size() > 0 && job.X2header == null)
    {
      Log.error("No X2 header specified for job "+job.name+". Each Y axis must have a corresponding X axis.");
      return;
    }
    
    //Make a dataSeries for desired headers and add to collection(s)
    CSVPlotTool plotTool = new CSVPlotTool();  //to leverage existing functions
    String title = job.name + "_";
    XYSeriesCollection dataSet1 = new XYSeriesCollection();
    XYSeriesCollection dataSet2 = new XYSeriesCollection();
    double maxY1 = 0;
    double minY1 = Double.MAX_VALUE;
    double maxY2 = 0;
    double minY2 = Double.MAX_VALUE;
    for(int i = 0; i < job.Y1headers.size(); i++)
    {
      XYSeries dataSeriesTop;
      XYSeries dataSeriesBottom;
      XYSeries dataSeriesLeft;
      XYSeries dataSeriesRight;
      
      //For convex hulls, we have to reorder points before inserting into the dataset
      ConvexHullMaker maker = new ConvexHullMaker();
      List<List<Double>> newVals = new ArrayList<List<Double>>();
      List<List<Double>> splitVals = new ArrayList<List<Double>>();
      newVals = maker.make(data.get(job.X1header), data.get(job.Y1headers.get(i)));
      splitVals = splitHull(newVals);
      dataSeriesTop = plotTool.createXYSeries(job.Y1headers.get(i),splitVals.get(0),splitVals.get(1));
      dataSeriesBottom = plotTool.createXYSeries("",splitVals.get(2),splitVals.get(3));
      dataSeriesLeft = plotTool.createXYSeries("",splitVals.get(4),splitVals.get(5));
      dataSeriesRight = plotTool.createXYSeries("",splitVals.get(6),splitVals.get(7));
      
      dataSeriesBottom.setKey("REMOVE");
      dataSeriesLeft.setKey("REMOVE");
      dataSeriesRight.setKey("REMOVE");
      
      dataSet1.addSeries(dataSeriesTop);
      dataSet1.addSeries(dataSeriesBottom);
      dataSet1.addSeries(dataSeriesLeft);
      dataSet1.addSeries(dataSeriesRight);
      
      title = title + job.Y1headers.get(i) + "_";
      maxY1 = maxY1 < dataSeriesTop.getMaxY() ? dataSeriesTop.getMaxY() : maxY1;
      minY1 = minY1 > dataSeriesBottom.getMinY() ? dataSeriesBottom.getMinY() : minY1;
    }
    for(int i = 0; i < job.Y2headers.size(); i++)
    {
      XYSeries dataSeriesTop;
      XYSeries dataSeriesBottom;
      XYSeries dataSeriesLeft;
      XYSeries dataSeriesRight;
      
      ConvexHullMaker maker = new ConvexHullMaker();
      List<List<Double>> newVals = new ArrayList<List<Double>>();
      List<List<Double>> splitVals = new ArrayList<List<Double>>();
      newVals = maker.make(data.get(job.X2header), data.get(job.Y2headers.get(i)));
      splitVals = splitHull(newVals);
      dataSeriesTop = plotTool.createXYSeries(job.Y2headers.get(i),splitVals.get(0),splitVals.get(1));
      dataSeriesBottom = plotTool.createXYSeries("",splitVals.get(2),splitVals.get(3));
      dataSeriesLeft = plotTool.createXYSeries("",splitVals.get(4),splitVals.get(5));
      dataSeriesRight = plotTool.createXYSeries("",splitVals.get(6),splitVals.get(7));
      
      dataSeriesBottom.setKey("REMOVE");
      dataSeriesLeft.setKey("REMOVE");
      dataSeriesRight.setKey("REMOVE");
      
      dataSet2.addSeries(dataSeriesTop);
      dataSet2.addSeries(dataSeriesBottom);
      dataSet2.addSeries(dataSeriesLeft);
      dataSet2.addSeries(dataSeriesRight);
      
      title = title + job.Y2headers.get(i) + "_";
      maxY2 = maxY2 < dataSeriesTop.getMaxY() ? dataSeriesTop.getMaxY() : maxY2;
      minY2 = minY2 > dataSeriesBottom.getMinY() ? dataSeriesBottom.getMinY() : minY2;
    }
    title = title + "vs_" + job.X1header;
    if(job.X2header != null && !job.X1header.equalsIgnoreCase(job.X2header))
      title = title + "_" + job.X2header;
    
    //Override the constructed title if desired
    if(job.titleOverride != null && !job.titleOverride.isEmpty() && !job.titleOverride.equalsIgnoreCase("None"))
      title = job.titleOverride;
    
    //set labels
    String XAxisLabel = job.X1header;
    String YAxisLabel = job.Y1headers.get(0);

    JFreeChart chart = ChartFactory.createXYLineChart(
        job.titleOverride!=null&&job.titleOverride.equalsIgnoreCase("None")?"":title,           // chart title
        XAxisLabel,         // x axis label
        YAxisLabel,         // y axis label
        dataSet1,                   // data
        PlotOrientation.VERTICAL,  // orientation
        true,                      // include legend
        true,                      // tooltips
        false                      // urls
        );
    
    Log.info("Creating Graph "+title);   
    XYPlot plot = (XYPlot) chart.getPlot();
    
    if (!job.logAxis)
    {
      // Determine Y1 range
      double resMax0 = maxY1;
      double resMin0 = minY1;
      if (Double.isNaN(resMax0) || Double.isNaN(resMin0))
        plot.getDomainAxis(0).setLabel("Range is NaN");
      if (DoubleUtils.isZero(resMin0))
        resMin0 = -0.001;
      if (DoubleUtils.isZero(resMax0))
        resMax0 = 0.001;
      double rangeLength = resMax0 - resMin0;
      ValueAxis yAxis = plot.getRangeAxis(0);
      yAxis.setRange(resMin0 - 0.15 * rangeLength, resMax0 + 0.15 * rangeLength);//15% buffer so we can see top and bottom clearly  
      
      //Override the bounds if desired
      try
      {
        if(job.Y1LowerBound != null)
          yAxis.setLowerBound(job.Y1LowerBound);
        if(job.Y1UpperBound != null)
          yAxis.setUpperBound(job.Y1UpperBound);
      } catch (Exception e)
      {
        Log.error("Couldn't set Y bounds. You probably tried to set a bound on an axis that doesn't exist.");
      }
      
      plot.setRangeAxis(0,yAxis);
      
      //Add the second Y axis to the right side
      if(!job.Y2headers.isEmpty())
      {
        ValueAxis rightYAxis = new NumberAxis();
        // Determine Y2 range
        double resMax1 = maxY2;
        double resMin1 = minY2;
        if (Double.isNaN(resMax1) || Double.isNaN(resMin1))
          plot.getDomainAxis(1).setLabel("Range is NaN");
        if (DoubleUtils.isZero(resMin1))
          resMin1 = -0.001;
        if (DoubleUtils.isZero(resMax1))
          resMax1 = 0.001;
        rangeLength = resMax1 - resMin1;
        rightYAxis.setRange(resMin1 - 0.15 * rangeLength, resMax1 + 0.15* rangeLength);
        rightYAxis.setLabel(job.Y2headers.get(0));
        
        //Override the bounds if desired
        try
        {
          if(job.Y2LowerBound != null)
            rightYAxis.setLowerBound(job.Y2LowerBound);
          if(job.Y2UpperBound != null)
            rightYAxis.setUpperBound(job.Y2UpperBound);
        } catch (Exception e)
        {
          Log.error("Couldn't set Y bounds. You probably tried to set a bound on an axis that doesn't exist.");
        }
        
        plot.setRangeAxis(1, rightYAxis);
      }
    }
    else
    {
      double resMin = minY1 < minY2 ? minY1 : minY2;
      if(resMin <= 0.0)
        resMin = .00001;
      LogarithmicAxis yAxis = new LogarithmicAxis("Log("+YAxisLabel+")");
      yAxis.setLowerBound(resMin);
      
      //Override the bounds if desired
      try
      {
        if(job.Y1LowerBound != null)
          yAxis.setLowerBound(job.Y1LowerBound);
        if(job.Y1UpperBound != null)
          yAxis.setUpperBound(job.Y1UpperBound);
      } catch (Exception e)
      {
        Log.error("Couldn't set Y bounds. You probably tried to set a bound on an axis that doesn't exist.");
      }
      
      plot.setRangeAxis(0, yAxis);
      
      if (!job.Y2headers.isEmpty())
      {
        LogarithmicAxis rightYAxis = new LogarithmicAxis("Log("
            + job.Y2headers.get(0)+")");
        rightYAxis.setLowerBound(resMin);
        
        //Override the bounds if desired
        try
        {
          if(job.Y2LowerBound != null)
            rightYAxis.setLowerBound(job.Y2LowerBound);
          if(job.Y2UpperBound != null)
            rightYAxis.setUpperBound(job.Y2UpperBound);
        } catch (Exception e)
        {
          Log.error("Couldn't set Y bounds. You probably tried to set a bound on an axis that doesn't exist.");
        }
        
        plot.setRangeAxis(1, rightYAxis);
      }
    }
    
    //Override X bounds if desired
    try
    {
      if(job.X1LowerBound != null)
        plot.getDomainAxis(0).setLowerBound(job.X1LowerBound);
      if(job.X1UpperBound != null)
        plot.getDomainAxis(0).setUpperBound(job.X1UpperBound);
      if(job.X2LowerBound != null)
        plot.getDomainAxis(1).setLowerBound(job.X2LowerBound);
      if(job.X2UpperBound != null)
        plot.getDomainAxis(1).setUpperBound(job.X2UpperBound);
    }
    catch(Exception e)
    {
      Log.error("Couldn't set X bounds. You probably tried to set a bound on an axis that doesn't exist.");
    }
    
    //Add the second dataset if necessary
    if(!job.Y2headers.isEmpty())
    {
      plot.setDataset(1, dataSet2);
      plot.mapDatasetToRangeAxis(1, 1);
    }
    
    //Override labels if desired
    if(job.X1Label != null && !plot.getDomainAxis(0).getLabel().contains("NaN"))
      plot.getDomainAxis(0).setLabel(job.X1Label.equalsIgnoreCase("None")?"":job.X1Label);
    if(job.X2Label != null && plot.getDomainAxis(1) != null)
      plot.getDomainAxis(1).setLabel(job.X2Label.equalsIgnoreCase("None")?"":job.X2Label);
    if(job.Y1Label != null)
      plot.getRangeAxis(0).setLabel(job.Y1Label.equalsIgnoreCase("None")?"":job.Y1Label);
    if(job.Y2Label != null && plot.getRangeAxis(1) != null)
      plot.getRangeAxis(1).setLabel(job.Y2Label.equalsIgnoreCase("None")?"":job.Y2Label);
    
    //Format lines and colors
    plotTool.formatXYPlot(chart, job.bgColor);
    plot.setDomainGridlinesVisible(job.showGridLines);
    plot.setRangeGridlinesVisible(job.showGridLines);
    formatConvexHullPlot(job, chart, dataSet1, dataSet2);
    
    //Handle legends
    if(job.removeAllLegends)
      chart.removeLegend();
    
    //Make the file
    try
    {
      FileUtils.createDirectory(job.outputDir);
      String filename = job.outputFilename==null ? job.outputDir+"/"+plotTool.MakeFileName(title)+".jpg" : job.outputDir+"/"+job.outputFilename;
      if(!filename.endsWith(".jpg"))
        filename = filename + ".jpg";
      File JPGFile = new File(filename);
      if(job.imageHeight != null && job.imageWidth != null)
        ChartUtilities.saveChartAsJPEG(JPGFile, chart, job.imageWidth, job.imageHeight);
      else
        ChartUtilities.saveChartAsJPEG(JPGFile, chart, 1600, 800);
    }
    catch (IOException e)
    {
      Log.error(e.getMessage());
    }

  }
  
  protected void formatConvexHullPlot(PlotJob job, JFreeChart chart, XYSeriesCollection dataSet1, XYSeriesCollection dataSet2)
  {
    XYPlot plot = (XYPlot) chart.getPlot();
    XYLineAndShapeRenderer renderer1 = (XYLineAndShapeRenderer) plot.getRenderer();
    BasicStroke wideLine = new BasicStroke( 2.0f ); 
    
    //For Scientific notation
    NumberFormat formatter = new DecimalFormat("0.######E0");
    
    for(int i = 0; i < plot.getDomainAxisCount(); i++)
    {
      plot.getDomainAxis(i).setLabelFont(new Font("SansSerif", Font.PLAIN, job.fontSize));
      plot.getDomainAxis(i).setTickLabelFont(new Font("SansSerif", Font.PLAIN, 15));
      plot.getDomainAxis(i).setLabelPaint(job.bgColor==Color.red?Color.white:Color.black);
      plot.getDomainAxis(i).setTickLabelPaint(job.bgColor==Color.red?Color.white:Color.black);
    }
    for(int i = 0; i < plot.getRangeAxisCount(); i++)
    {
      plot.getRangeAxis(i).setLabelFont(new Font("SansSerif", Font.PLAIN, job.fontSize));
      plot.getRangeAxis(i).setTickLabelFont(new Font("SansSerif", Font.PLAIN, 15));
      plot.getRangeAxis(i).setLabelPaint(job.bgColor==Color.red?Color.white:Color.black);
      plot.getRangeAxis(i).setTickLabelPaint(job.bgColor==Color.red?Color.white:Color.black);
      NumberAxis rangeAxis = (NumberAxis)plot.getRangeAxis(i);
      rangeAxis.setNumberFormatOverride(formatter);
    }

    //White background outside of plottable area
    chart.setBackgroundPaint(job.bgColor);

    plot.setBackgroundPaint(Color.white);
    plot.setDomainGridlinePaint(Color.black);
    plot.setRangeGridlinePaint(Color.black);

    plot.setDomainCrosshairVisible(true);
    plot.setRangeCrosshairVisible(true);

    chart.getLegend().setItemFont(new Font("SansSerif", Font.PLAIN, 15));
    chart.getTitle().setFont(new Font("SansSerif", Font.PLAIN, job.fontSize));
    chart.getTitle().setPaint(job.bgColor==Color.red?Color.white:Color.black);
    
    //If there is only one Y axis, color all datasets red (so top, bottom, left, and right will be the same)
    if(job.Y2headers == null || job.Y2headers.isEmpty())
    {
      for(int i = 0; i < dataSet1.getSeriesCount(); i++)
      {
        renderer1.setSeriesStroke(i, wideLine); 
        renderer1.setBaseShapesVisible(false);
        renderer1.setSeriesFillPaint(i, Color.red); 
        renderer1.setSeriesPaint(i, Color.red);
        if(dataSet1.getSeries(i).getKey() != null && dataSet1.getSeries(i).getKey().toString().equalsIgnoreCase("REMOVE"))
          renderer1.setSeriesVisibleInLegend(i, false);
      }
    }
    //If there are 2 Y axes, we should color the axes to correspond with the data so it isn't (as) confusing
    else
    {
      StandardXYItemRenderer renderer2 = new StandardXYItemRenderer();
      plot.setRenderer(1, renderer2);
      
      for(int i = 0; i < dataSet1.getSeriesCount(); i++)
      {
        renderer1.setSeriesStroke(i, wideLine); 
        renderer1.setBaseShapesVisible(false);
        renderer1.setSeriesFillPaint(i, Color.red); 
        renderer1.setSeriesPaint(i, Color.red);
        if(dataSet1.getSeries(i).getKey() != null && dataSet1.getSeries(i).getKey().toString().equalsIgnoreCase("REMOVE"))
          renderer1.setSeriesVisibleInLegend(i, false);
      }
      for(int i = 0; i < dataSet2.getSeriesCount(); i++)
      {
        renderer2.setSeriesStroke(i, wideLine); 
        renderer2.setBaseShapesVisible(false);
        renderer2.setSeriesFillPaint(i, Color.blue); 
        renderer2.setSeriesPaint(i, Color.blue);
        if(dataSet2.getSeries(i).getKey() != null && dataSet2.getSeries(i).getKey().toString().equalsIgnoreCase("REMOVE"))
          renderer2.setSeriesVisibleInLegend(i, false);
      }
      plot.getRangeAxis(0).setLabelPaint(Color.red);
      plot.getRangeAxis(0).setTickLabelPaint(Color.red);
      plot.getRangeAxis(1).setLabelPaint(Color.blue);
      plot.getRangeAxis(1).setTickLabelPaint(Color.blue);
    }
  }


}
