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

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collections;
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.LegendItemCollection;
import org.jfree.chart.LegendItemSource;
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.XYItemRenderer;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.chart.title.LegendTitle;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
import org.jfree.ui.RectangleEdge;
import org.jfree.ui.RectangleInsets;

import com.google.protobuf.InvalidProtocolBufferException;

import pulse.cdm.actions.SEAction;
import pulse.cdm.patient.assessments.SEPulmonaryFunctionTest;
import pulse.cdm.properties.CommonUnits.TimeUnit;
import pulse.cdm.properties.CommonUnits.VolumeUnit;
import pulse.cdm.properties.SEFunctionVolumeVsTime;
import pulse.cdm.scenario.SEScenario;
import pulse.cdm.substance.SESubstanceManager;
import pulse.utilities.DoubleUtils;
import pulse.utilities.FileUtils;
import pulse.utilities.Log;
import pulse.utilities.LogListener;
import pulse.utilities.csv.CSVContents;
import pulse.utilities.csv.plots.PlotDriver.PlotJob;

public class RespiratoryPFTPlotter implements Plotter
{
  protected List<LogEvent> events = new ArrayList<LogEvent>();
  protected Map<String,List<Double>> data = new HashMap<String,List<Double>>();
  protected Map<String,List<Double>> PFTData = new HashMap<String,List<Double>>();  //might be able to get rid of this
  protected List<SEAction> actions = new ArrayList<SEAction>();
  protected SEScenario scenario;
  protected SEPulmonaryFunctionTest pft;

  public static void main(String[] args)
  {
    PlotDriver.main(args);
  }
  
  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.logPath == null || job.logPath.isEmpty())
    {job.logPath = job.verificationDirectory+"/";}
    if(job.scenarioPath == null || job.scenarioPath.isEmpty())
    {job.scenarioPath = job.verificationDirectory+"/";}
    if(job.dataFile == null || job.dataFile.isEmpty())
    {job.dataFile = job.name + "Results.csv";}
    if(job.logFile == null || job.logFile.isEmpty())
    {job.logFile = job.name + ".log";}
    if(job.scenarioFile == null || job.scenarioFile.isEmpty())
    {job.scenarioFile = job.name + ".json";}
    
    //get all events from Log file
    if(!job.skipAllEvents)
    {
      events = getEventsFromLog(job.logPath + job.logFile);
    }
    
    if (!job.skipAllActions)
    {
      try
      {
        this.scenario = new SEScenario(subMgr);
        this.scenario.readFile(job.scenarioPath + job.scenarioFile);
        actions = scenario.getActions();
      } 
      catch(InvalidProtocolBufferException ex)
      {
        Log.error("Could not analyze scenario file " + job.scenarioPath + job.scenarioFile);
      }
    }
    
    //Look for a PFT action
    for(SEAction action : actions)
    {      
      if(action.toString().indexOf("PulmonaryFunctionTest") != -1)
      {
        double PFTtime_s = action.getScenarioTime().getValue();
        
        //extract data from file
        try
        {
          this.pft = new SEPulmonaryFunctionTest();
          this.pft.readFile(job.verificationDirectory+"/"+job.PFTFile);
          
          SEFunctionVolumeVsTime points = this.pft.getLungVolumePlot();
                   
          List<Double> timeValues = new ArrayList<Double>();
          for(double d : points.getTime(TimeUnit.s))
          {
            timeValues.add(d+PFTtime_s);  
          }
          List<Double> volumeValues = new ArrayList<Double>();
          for(double d : points.getVolume(VolumeUnit.mL))
          {
            volumeValues.add(d);
          }
          
          PFTData.put("Time", timeValues);
          PFTData.put("Volume", volumeValues);
        }
        catch(InvalidProtocolBufferException ex)
        {
          Log.error("Couldn't read PFT file.",ex);
        }
      }
    }
    
    if (data.isEmpty() || data == null)
    {
      //Get data contents for desired header(s)
      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);
        }      
        List<Double> timeHeaderData = new ArrayList<Double>();
        csv.readHeader("Time(s)", timeHeaderData);
        data.put("Time(s)", timeHeaderData);
      } catch (IOException e)
      {
        Log.error("Could not analyze file " + job.dataPath + job.dataFile);
      }
    }
    
    //make the graph
    createGraph(job, PFTData, data, events, actions);
  }
  
  public List<LogEvent> getEventsFromLog(String file)
  {
    List<LogEvent> events = new ArrayList<LogEvent>();
    
    try
    {
      BufferedReader br = new BufferedReader(new FileReader(file));
      String line;
      while ((line = br.readLine()) != null)
      {
        if (line.length() == 0)
          continue;
        if(line.indexOf("[Event]") == -1)
          continue;
        else
        {
          LogEvent event = new LogEvent();
          String eventText = line.substring(line.indexOf("[Event]")+7).trim();
          int endTimeIndex = eventText.indexOf("(s)");
          if(endTimeIndex == -1)
            endTimeIndex = eventText.indexOf(",");
          Double dTime = Double.parseDouble(eventText.substring(0, endTimeIndex).trim());
          event.time.setValue(dTime, "s");
          event.text = eventText.substring(eventText.indexOf(",")+1).trim();
          events.add(event);
        }
      }
      br.close();
    }
    catch (IOException e)
    {
      Log.warn("ActionEventPlotter couldn't read the log file "+file);
    }
    catch (NumberFormatException e)
    {
      Log.error("Couldn't correctly parse log file time to double");
    }
    catch (Exception e)
    {
      Log.error("Something went wrong parsing the log",e);
      return null;
    }
    
    return events;
    
  }

  public void createGraph(PlotJob job, Map<String, List<Double>> PFTData, Map<String, List<Double>> data, List<LogEvent> events, List<SEAction> actions)
  {
    CSVPlotTool plotTool = new CSVPlotTool();  //to leverage existing functions
    String title = job.name + "_";
    XYSeriesCollection dataSet = new XYSeriesCollection();
    double maxY = 0;
    double minY = Double.MAX_VALUE;
    for(int i = 0; i < job.headers.size(); i++)
    {      
      title = title + job.headers.get(i) + "_";
      XYSeries dataSeries;
      dataSeries = plotTool.createXYSeries(job.headers.get(i),data.get("Time(s)"),data.get(job.headers.get(i)));
      dataSet.addSeries(dataSeries);
      maxY = maxY < dataSeries.getMaxY() ? dataSeries.getMaxY() : maxY;
      minY = minY > dataSeries.getMinY() ? dataSeries.getMinY() : minY;
    }
    
    //Now make a data series for PFT data and check its max and min
    XYSeries dataSeries = plotTool.createXYSeries("PFT Total Lung Volume (mL)",PFTData.get("Time"),PFTData.get("Volume"));
    dataSet.addSeries(dataSeries);
    maxY = maxY < dataSeries.getMaxY() ? dataSeries.getMaxY() : maxY;
    minY = minY > dataSeries.getMinY() ? dataSeries.getMinY() : minY;
    
    title = title + "vs_Time";
    
    //Override the constructed title if desired
    if(job.titleOverride != null && !job.titleOverride.isEmpty() && !job.titleOverride.equalsIgnoreCase("None"))
      title = job.titleOverride;
    
    double rangeLength = maxY - minY;
    if(Math.abs(rangeLength) < 1e-6)
    {
      rangeLength = .01;
    }
    
    class AEEntry implements Comparable<AEEntry>
    {
      public String name;
      public List<Double> times = new ArrayList<Double>();
      public List<Double> YVals = new ArrayList<Double>();
      public String type = "";
      
      public int compareTo(AEEntry entry)
      {
        return times.get(0) < entry.times.get(0) ? -1
             : times.get(0) > entry.times.get(0) ? 1
             : 0;
      }
    }
    
    List<AEEntry> allActionsAndEvents = new ArrayList<AEEntry>();
    
    if (!job.skipAllEvents)
    {
      //Make points for each event
      //Treat each event like two points on the same vertical line
      for (LogEvent event : events)
      {
        boolean skip = false;
        
        for(String eventToSkip : job.eventOmissions)
        {
          if(event.text.contains(eventToSkip))
            skip = true;
        }
        if(skip)
          continue;
        AEEntry entry = new AEEntry();

        entry.times.add(event.time.getValue());
        if(job.logAxis)
          entry.YVals.add(maxY);
        else if(job.forceZeroYAxisBound && maxY < 0)
          entry.YVals.add(-.01);
        else
          entry.YVals.add(maxY + 0.15 * rangeLength);

        entry.times.add(event.time.getValue());
        if(job.logAxis)
          entry.YVals.add(minY);
        else if(job.forceZeroYAxisBound && minY > 0)
          entry.YVals.add(-.01);
        else
          entry.YVals.add(minY - 0.15 * rangeLength);

        entry.name = event.text + "\r\nt=" + event.time.getValue();
        entry.type = "EVENT:";

        allActionsAndEvents.add(entry);
      }
    }
    
    if (!job.skipAllActions)
    {
      //Make similar entries for actions
      for (SEAction action : actions)
      {
        boolean skip = false;
        
        for(String actionToSkip : job.actionOmissions)
        {
          if(action.toString().contains(actionToSkip))
            skip = true;
        }
        if(skip)
          continue;
        
        if (action.toString().contains("Advance Time"))
          continue;

        AEEntry entry = new AEEntry();

        entry.times.add(action.getScenarioTime().getValue());
        if(job.logAxis)
          entry.YVals.add(maxY);
        else if(job.forceZeroYAxisBound && maxY < 0)
          entry.YVals.add(-.01);
        else
          entry.YVals.add(maxY + 0.15 * rangeLength);

        entry.times.add(action.getScenarioTime().getValue());
        if(job.logAxis)
          entry.YVals.add(minY);
        else if(job.forceZeroYAxisBound && minY > 0)
          entry.YVals.add(-.01);
        else
          entry.YVals.add(minY - 0.15 * rangeLength);

        entry.name = action.toString() + "\r\nt="
            + action.getScenarioTime().getValue();
        entry.type = "ACTION:";

        allActionsAndEvents.add(entry);
      }
    }
    
    //Sort the list
    Collections.sort(allActionsAndEvents);
    
    //Add a series for each entry
    for(AEEntry entry : allActionsAndEvents)
    {
      dataSet.addSeries(plotTool.createXYSeries(entry.type + entry.name,entry.times,entry.YVals));
    }      
    
    //set labels
    String XAxisLabel = "Time(s)";
    String YAxisLabel = job.headers.get(0);

    JFreeChart chart = ChartFactory.createXYLineChart(
        job.titleOverride!=null&&job.titleOverride.equalsIgnoreCase("None")?"":title,           // chart title
        XAxisLabel,         // x axis label
        YAxisLabel,         // y axis label
        dataSet,                   // 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 Y range
      double resMax0 = maxY;
      double resMin0 = minY;
      if (Double.isNaN(resMax0) || Double.isNaN(resMin0))
        plot.getDomainAxis().setLabel("Range is NaN");
      if (DoubleUtils.isZero(resMin0))
        resMin0 = -0.000001;
      if (DoubleUtils.isZero(resMax0))
        resMax0 = 0.000001;
      if(job.forceZeroYAxisBound && resMin0>=0) resMin0 = -.000001;
      if(job.forceZeroYAxisBound && resMax0<=0) resMax0 = .000001;
      rangeLength = resMax0 - resMin0;
      ValueAxis yAxis = plot.getRangeAxis();
      if(rangeLength != 0)
        yAxis.setRange(resMin0 - 0.15 * rangeLength, resMax0 + 0.15 * rangeLength);//15% buffer so we can see top and bottom clearly          
      
      //Add another Y axis to the right side for easier reading
      ValueAxis rightYAxis = new NumberAxis();
      rightYAxis.setRange(resMin0 - 0.15 * rangeLength, resMax0 + 0.15* rangeLength);
      rightYAxis.setLabel("");
      
      //Override the bounds if desired
      try
      {
        if(job.Y1LowerBound != null)
        {
          yAxis.setLowerBound(job.Y1LowerBound);
          rightYAxis.setLowerBound(job.Y1LowerBound);
        }
        if(job.Y1UpperBound != null)
        {
          yAxis.setUpperBound(job.Y1UpperBound);
          rightYAxis.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);
      plot.setRangeAxis(1, rightYAxis);
      
    }
    else
    {
      double resMin = minY;
      double resMax = maxY;
      if(resMin <= 0.0)
        resMin = .00001;
      LogarithmicAxis yAxis = new LogarithmicAxis("Log("+YAxisLabel+")");
      LogarithmicAxis rightYAxis = new LogarithmicAxis("");
      yAxis.setLowerBound(resMin);
      rightYAxis.setLowerBound(resMin);
      yAxis.setUpperBound(resMax);
      rightYAxis.setUpperBound(resMax);
      
      //Override the bounds if desired
      try
      {
        if(job.Y1LowerBound != null)
        {
          yAxis.setLowerBound(job.Y1LowerBound);
          rightYAxis.setLowerBound(job.Y1LowerBound);
        }
        if(job.Y1UpperBound != null)
        {
          yAxis.setUpperBound(job.Y1UpperBound);
          rightYAxis.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);
      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);
    }
    catch(Exception e)
    {
      Log.error("Couldn't set X bounds. You probably tried to set a bound on an axis that doesn't exist.");
    }
    
    //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.Y1Label != null)
      plot.getRangeAxis(0).setLabel(job.Y1Label.equalsIgnoreCase("None")?"":job.Y1Label);

    formatRPFTPlot(job, chart);
    plot.setDomainGridlinesVisible(job.showGridLines);
    plot.setRangeGridlinesVisible(job.showGridLines);
    
    //Changing line widths and colors
    XYItemRenderer r = plot.getRenderer(); 
    BasicStroke wideLine = new BasicStroke( 2.0f ); 
    Color[] AEcolors = {Color.red, Color.green, Color.black, Color.magenta, Color.orange};
    Color[] dataColors = {Color.blue, Color.cyan, Color.gray, Color.black, Color.red};
    for(int i = 0, cIndex = 0; i < dataSet.getSeriesCount(); i++, cIndex++)
    {
      r.setSeriesStroke(i, wideLine); 
      XYLineAndShapeRenderer renderer = (XYLineAndShapeRenderer) plot.getRenderer();
      renderer.setBaseShapesVisible(false);
      if(cIndex > 4) cIndex = 0;
      if(i<job.headers.size())  //Our actual data
      {
        renderer.setSeriesFillPaint(i, dataColors[cIndex]); 
        renderer.setSeriesPaint(i, dataColors[cIndex]);
      }
      else  //actions and events in procession of other colors
      {
        renderer.setSeriesFillPaint(i, AEcolors[cIndex]); 
        renderer.setSeriesPaint(i, AEcolors[cIndex]); 
      }
    }
    
    //Split the auto-generated legend into two legends, one for data and one for actions and events
    LegendItemCollection originalLegendCollection = plot.getLegendItems();
    final LegendItemCollection dataLegendCollection = new LegendItemCollection();
    int i;
    for(i = 0; i < job.headers.size() && i < originalLegendCollection.getItemCount(); i++)
    {
      if(originalLegendCollection.get(i).getLabel().startsWith("ACTION") || originalLegendCollection.get(i).getLabel().startsWith("EVENT"))
        break;
      dataLegendCollection.add(originalLegendCollection.get(i));
    }
    final LegendItemCollection remainingLegendCollection = new LegendItemCollection();
    for(; i < originalLegendCollection.getItemCount(); i++)
    {
      remainingLegendCollection.add(originalLegendCollection.get(i));
    }
    chart.removeLegend();
    LegendItemSource source = new LegendItemSource() 
    {
      LegendItemCollection lic = new LegendItemCollection();
      {
          lic.addAll(dataLegendCollection);
      }

      public LegendItemCollection getLegendItems() 
      {
          return lic;
      }
    };
    LegendTitle dataLegend = new LegendTitle(source);
    dataLegend.setMargin(new RectangleInsets(1.0, 1.0, 1.0, 1.0));
    dataLegend.setBorder(2, 2, 2, 2);
    dataLegend.setBackgroundPaint(Color.white);
    dataLegend.setPosition(RectangleEdge.TOP);
    dataLegend.setItemFont(new Font("SansSerif", Font.PLAIN, 22));
    chart.addLegend(dataLegend);
    
    source = new LegendItemSource() 
    {
      LegendItemCollection lic = new LegendItemCollection();
      {
          lic.addAll(remainingLegendCollection);
      }

      public LegendItemCollection getLegendItems() 
      {
          return lic;
      }
    };
    LegendTitle actionEventsLegend = new LegendTitle(source);
    actionEventsLegend.setMargin(new RectangleInsets(1.0, 1.0, 1.0, 1.0));
    actionEventsLegend.setBorder(2, 2, 2, 2);
    actionEventsLegend.setBackgroundPaint(Color.white);
    actionEventsLegend.setPosition(RectangleEdge.BOTTOM);
    actionEventsLegend.setItemFont(new Font("SansSerif", Font.PLAIN, 22));
    if(!job.hideAELegend && !job.removeAllLegends)
      chart.addLegend(actionEventsLegend);
    
    if(job.removeAllLegends)
      chart.removeLegend();
    
    int verticalPixels = 800 + 170*(allActionsAndEvents.size()/5);

    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 if(!job.hideAELegend && !job.removeAllLegends)
        ChartUtilities.saveChartAsJPEG(JPGFile, chart, 1600, verticalPixels);
      else
        ChartUtilities.saveChartAsJPEG(JPGFile, chart, 1600, 800);
    }
    catch (IOException e)
    {
      Log.error(e.getMessage());
    }
  }
  
  public void formatRPFTPlot(PlotJob job, JFreeChart chart)
  {
    XYPlot plot = (XYPlot) chart.getPlot();
    
    //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);
  }
}
