package tim.prune.function.charts;

import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.awt.GridLayout;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;

import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JTextField;
import javax.swing.SwingConstants;

import tim.prune.App;
import tim.prune.ExternalTools;
import tim.prune.GenericFunction;
import tim.prune.I18nManager;
import tim.prune.config.Config;
import tim.prune.data.DataPoint;
import tim.prune.data.Distance;
import tim.prune.data.Field;
import tim.prune.data.Timestamp;
import tim.prune.data.Track;
import tim.prune.data.Unit;
import tim.prune.data.UnitSet;
import tim.prune.gui.profile.SpeedData;
import tim.prune.gui.profile.VerticalSpeedData;
import tim.prune.load.GenericFileFilter;

/**
 * Class to manage the generation of charts using gnuplot
 */
public class Charter extends GenericFunction
{
	/** dialog object, cached */
	private JDialog _dialog = null;
	/** radio button for distance axis */
	private JRadioButton _distanceRadio = null;
	/** radio button for time axis */
	private JRadioButton _timeRadio = null;
	/** array of checkboxes for specifying y axes */
	private JCheckBox[] _yAxesBoxes = null;
	/** radio button for svg output */
	private JRadioButton _svgRadio = null;
	/** file chooser for saving svg file */
	private JFileChooser _fileChooser = null;
	/** text field for svg width */
	private JTextField _svgWidthField = null;
	/** text field for svg height */
	private JTextField _svgHeightField = null;

	/** Default dimensions of Svg file */
	private static final String DEFAULT_SVG_WIDTH  = "800";
	private static final String DEFAULT_SVG_HEIGHT = "400";


	/**
	 * Constructor from superclass
	 * @param inApp app object
	 */
	public Charter(App inApp)
	{
		super(inApp);
	}

	/**
	 * @return key for function name
	 */
	public String getNameKey()
	{
		return "function.charts";
	}

	/**
	 * Show the dialog
	 */
	public void begin()
	{
		// First check if gnuplot is available
		if (!ExternalTools.isToolInstalled(getConfig(), ExternalTools.TOOL_GNUPLOT))
		{
			_app.showErrorMessage(getNameKey(), "dialog.charts.gnuplotnotfound");
			return;
		}
		// Make dialog window
		if (_dialog == null)
		{
			_dialog = new JDialog(_parentFrame, getName(), true);
			_dialog.setLocationRelativeTo(_parentFrame);
			_dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
			_dialog.getContentPane().add(makeDialogComponents());
			_dialog.pack();
		}
		if (setupDialog(_app.getTrackInfo().getTrack())) {
			_dialog.setVisible(true);
		}
		else {
			_app.showErrorMessage(getNameKey(), "dialog.charts.needaltitudeortimes");
		}
	}


	/**
	 * Make the dialog components
	 * @return panel containing gui elements
	 */
	private JPanel makeDialogComponents()
	{
		JPanel dialogPanel = new JPanel();
		dialogPanel.setLayout(new BorderLayout());

		JPanel mainPanel = new JPanel();
		mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
		// x axis choice
		JPanel axisPanel = new JPanel();
		axisPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.charts.xaxis")));
		_distanceRadio = new JRadioButton(I18nManager.getText("fieldname.distance"));
		_distanceRadio.setSelected(true);
		_timeRadio = new JRadioButton(I18nManager.getText("fieldname.time"));
		ButtonGroup axisGroup = new ButtonGroup();
		axisGroup.add(_distanceRadio); axisGroup.add(_timeRadio);
		axisPanel.add(_distanceRadio); axisPanel.add(_timeRadio);
		mainPanel.add(axisPanel);

		// y axis choices
		JPanel yPanel = new JPanel();
		yPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.charts.yaxis")));
		_yAxesBoxes = new JCheckBox[4]; // dist altitude speed vertspeed (time not available on y axis)
		_yAxesBoxes[0] = new JCheckBox(I18nManager.getText("fieldname.distance"));
		_yAxesBoxes[1] = new JCheckBox(I18nManager.getText("fieldname.altitude"));
		_yAxesBoxes[1].setSelected(true);
		_yAxesBoxes[2] = new JCheckBox(I18nManager.getText("fieldname.speed"));
		_yAxesBoxes[3] = new JCheckBox(I18nManager.getText("fieldname.verticalspeed"));
		for (int i=0; i<4; i++) {
			yPanel.add(_yAxesBoxes[i]);
		}
		mainPanel.add(yPanel);

		// Add validation to prevent choosing invalid (ie dist/dist) combinations
		ActionListener xAxisListener = (e) -> enableYbox(0, _timeRadio.isSelected());
		_timeRadio.addActionListener(xAxisListener);
		_distanceRadio.addActionListener(xAxisListener);

		// output buttons
		JPanel outputPanel = new JPanel();
		outputPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.charts.output")));
		outputPanel.setLayout(new BorderLayout());
		JPanel radiosPanel = new JPanel();
		JRadioButton screenRadio = new JRadioButton(I18nManager.getText("dialog.charts.screen"));
		screenRadio.setSelected(true);
		_svgRadio = new JRadioButton(I18nManager.getText("dialog.charts.svg"));
		ButtonGroup outputGroup = new ButtonGroup();
		outputGroup.add(screenRadio); outputGroup.add(_svgRadio);
		radiosPanel.add(screenRadio); radiosPanel.add(_svgRadio);
		outputPanel.add(radiosPanel, BorderLayout.NORTH);
		// panel for svg width, height
		JPanel sizePanel = new JPanel();
		sizePanel.setLayout(new GridLayout(2, 2, 10, 1));
		JLabel widthLabel = new JLabel(I18nManager.getText("dialog.charts.svgwidth"));
		widthLabel.setHorizontalAlignment(SwingConstants.RIGHT);
		sizePanel.add(widthLabel);
		_svgWidthField = new JTextField(DEFAULT_SVG_WIDTH, 5);
		sizePanel.add(_svgWidthField);
		JLabel heightLabel = new JLabel(I18nManager.getText("dialog.charts.svgheight"));
		heightLabel.setHorizontalAlignment(SwingConstants.RIGHT);
		sizePanel.add(heightLabel);
		_svgHeightField = new JTextField(DEFAULT_SVG_HEIGHT, 5);
		sizePanel.add(_svgHeightField);

		outputPanel.add(sizePanel, BorderLayout.EAST);
		mainPanel.add(outputPanel);
		dialogPanel.add(mainPanel, BorderLayout.CENTER);

		// button panel on bottom
		JPanel buttonPanel = new JPanel();
		buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
		// ok button
		JButton okButton = new JButton(I18nManager.getText("button.ok"));
		okButton.addActionListener(e -> {
			showChart(_app.getTrackInfo().getTrack());
			_dialog.setVisible(false);
		});
		buttonPanel.add(okButton);
		// Cancel button
		JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
		cancelButton.addActionListener(e -> _dialog.setVisible(false));
		buttonPanel.add(cancelButton);
		dialogPanel.add(buttonPanel, BorderLayout.SOUTH);
		return dialogPanel;
	}


	/**
	 * Set up the dialog according to the track contents
	 * @param inTrack track object
	 * @return true if it's all ok
	 */
	private boolean setupDialog(Track inTrack)
	{
		boolean hasTimes = inTrack.hasData(Field.TIMESTAMP);
		boolean hasAltitudes = inTrack.hasAltitudeData();
		_timeRadio.setEnabled(hasTimes);

		// Add checks to prevent choosing unavailable combinations
		if (!hasTimes) {
			_distanceRadio.setSelected(true);
		}
		enableYbox(0, !_distanceRadio.isSelected());
		enableYbox(1, hasAltitudes);
		enableYbox(2, hasTimes);
		enableYbox(3, hasTimes && hasAltitudes);
		return (hasTimes || hasAltitudes);
	}


	/**
	 * Enable or disable the given y axis checkbox
	 * @param inIndex index of checkbox
	 * @param inFlag true to enable
	 */
	private void enableYbox(int inIndex, boolean inFlag)
	{
		_yAxesBoxes[inIndex].setEnabled(inFlag);
		if (!inFlag) {
			_yAxesBoxes[inIndex].setSelected(false);
		}
	}

	/**
	 * Show the chart for the specified track
	 * @param inTrack track object containing data
	 */
	private void showChart(Track inTrack)
	{
		int numCharts = 0;
		for (JCheckBox yAxesBox : _yAxesBoxes) {
			if (yAxesBox.isSelected()) {
				numCharts++;
			}
		}
		// Select default chart if none selected
		if (numCharts == 0) {
			_yAxesBoxes[1].setSelected(true);
			numCharts = 1;
		}
		int[] heights = getHeights(numCharts);

		boolean showSvg = _svgRadio.isSelected();
		File svgFile = null;
		if (showSvg) {
			svgFile = selectSvgFile();
			if (svgFile == null) {showSvg = false;}
		}
		OutputStreamWriter writer = null;
		try
		{
			final String gnuplotPath = getConfig().getConfigString(Config.KEY_GNUPLOT_PATH);
			Process process = Runtime.getRuntime().exec(new String[] {gnuplotPath, "-persist"});
			writer = new OutputStreamWriter(process.getOutputStream());
			if (showSvg)
			{
				writer.write("set terminal svg size " + getSvgValue(_svgWidthField, DEFAULT_SVG_WIDTH) + " "
					+ getSvgValue(_svgHeightField, DEFAULT_SVG_HEIGHT) + "\n");
				writer.write("set out '" + svgFile.getAbsolutePath() + "'\n");
			}
			else {
				// For screen output, gnuplot should use the default terminal (windows or x11 or wxt or something)
			}
			if (numCharts > 1) {
				writer.write("set multiplot layout " + numCharts + ",1\n");
			}
			// Loop over possible charts
			int chartNum = 0;
			for (int c=0; c<_yAxesBoxes.length; c++)
			{
				if (_yAxesBoxes[c].isSelected())
				{
					writer.write("set size 1," + (0.01*heights[chartNum*2+1]) + "\n");
					writer.write("set origin 0," + (0.01*heights[chartNum*2]) + "\n");
					writeChart(writer, inTrack, _distanceRadio.isSelected(), c);
					chartNum++;
				}
			}
			// Close multiplot if open
			if (numCharts > 1) {
				writer.write("unset multiplot\n");
			}
		}
		catch (Exception e) {
			_app.showErrorMessageNoLookup(getNameKey(), e.getMessage());
		}
		finally
		{
			try {
				// Close writer
				if (writer != null) {writer.close();}
			}
			catch (Exception e) {} // ignore
		}
	}


	/**
	 * Parse the given text field's value and return as string
	 * @param inField text field to read from
	 * @param inDefault default value if not valid
	 * @return value of svg dimension as string
	 */
	private static String getSvgValue(JTextField inField, String inDefault)
	{
		int value = 0;
		try {
			value = Integer.parseInt(inField.getText());
		}
		catch (Exception e) {} // ignore, value stays zero
		if (value > 0) {
			return "" + value;
		}
		return inDefault;
	}


	/**
	 * Write out the selected chart to the given Writer object
	 * @param inWriter writer object
	 * @param inTrack Track containing data
	 * @param inDistance true if x axis is distance
	 * @param inYaxis index of y axis
	 * @throws IOException if writing error occurred
	 */
	private void writeChart(OutputStreamWriter inWriter, Track inTrack, boolean inDistance, int inYaxis)
	throws IOException
	{
		ChartSeries distValues = getDistanceValues(inTrack);
		// Choose x values according to axis
		final ChartSeries xValues;
		if (inDistance) {
			xValues = distValues;
		}
		else {
			xValues = getTimeValues(inTrack);
		}
		// Choose y values according to axis
		final ChartSeries yValues;
		switch (inYaxis)
		{
			case 0: // y axis is distance
				yValues = distValues;
				break;
			case 1: // y axis is altitude
				yValues = getAltitudeValues(inTrack);
				break;
			case 2: // y axis is speed
				yValues = getSpeedValues(inTrack);
				break;
			case 3: // y axis is vertical speed
				yValues = getVertSpeedValues(inTrack);
				break;
			default:
				throw new IllegalArgumentException("y axis must be 0, 1, 2 or 3");
		}
		// Make a temporary data file for the output (one per subchart)
		File tempFile = File.createTempFile("gpsprunedata", null);
		tempFile.deleteOnExit();
		// write out values for x and y to temporary file
		try (FileWriter tempFileWriter = new FileWriter(tempFile))
		{
			tempFileWriter.write("# Temporary data file for GpsPrune charts\n\n");
			for (int i = 0; i < inTrack.getNumPoints(); i++)
			{
				if (xValues.hasData(i) && yValues.hasData(i)) {
					tempFileWriter.write("" + xValues.getData(i) + ", " + yValues.getData(i) + "\n");
				}
			}
		}

		// Sort out units to use
		UnitSet unitSet = getConfig().getUnitSet();
		final String distLabel = I18nManager.getText(unitSet.getDistanceUnit().getShortnameKey());
		final String altLabel  = I18nManager.getText(unitSet.getAltitudeUnit().getShortnameKey());
		final String speedLabel = I18nManager.getText(unitSet.getSpeedUnit().getShortnameKey());
		final String vertSpeedLabel = I18nManager.getText(unitSet.getVerticalSpeedUnit().getShortnameKey());

		// Set x axis label
		if (inDistance) {
			inWriter.write("set xlabel '" + I18nManager.getText("fieldname.distance") + " (" + distLabel + ")'\n");
		}
		else {
			inWriter.write("set xlabel '" + I18nManager.getText("fieldname.time") + " (" + I18nManager.getText("units.hours") + ")'\n");
		}

		// set other labels and plot chart
		String chartTitle = null;
		switch (inYaxis)
		{
		case 0: // y axis is distance
			inWriter.write("set ylabel '" + I18nManager.getText("fieldname.distance") + " (" + distLabel + ")'\n");
			chartTitle = I18nManager.getText("fieldname.distance");
			break;
		case 1: // y axis is altitude
			inWriter.write("set ylabel '" + I18nManager.getText("fieldname.altitude") + " (" + altLabel + ")'\n");
			chartTitle = I18nManager.getText("fieldname.altitude");
			break;
		case 2: // y axis is speed
			inWriter.write("set ylabel '" + I18nManager.getText("fieldname.speed") + " (" + speedLabel + ")'\n");
			chartTitle = I18nManager.getText("fieldname.speed");
			break;
		case 3: // y axis is vertical speed
			inWriter.write("set ylabel '" + I18nManager.getText("fieldname.verticalspeed") + " (" + vertSpeedLabel + ")'\n");
			chartTitle = I18nManager.getText("fieldname.verticalspeed");
			break;
		}
		inWriter.write("set style fill solid 0.5 border -1\n");
		inWriter.write("plot '" + tempFile.getAbsolutePath() + "' title '" + chartTitle + "' with filledcurve y1=0 lt rgb \"#009000\"\n");
	}


	/**
	 * Calculate the distance values for each point in the given track
	 * @param inTrack track object
	 * @return distance values in a ChartSeries object
	 */
	private ChartSeries getDistanceValues(Track inTrack)
	{
		// Calculate distances and fill in in values array
		ChartSeries values = new ChartSeries(inTrack.getNumPoints());
		double totalRads = 0;
		DataPoint prevPoint = null, currPoint = null;
		Unit distUnit = getConfig().getUnitSet().getDistanceUnit();
		for (int i=0; i<inTrack.getNumPoints(); i++)
		{
			currPoint = inTrack.getPoint(i);
			if (prevPoint != null && !currPoint.isWaypoint() && !currPoint.getSegmentStart())
			{
				totalRads += DataPoint.calculateRadiansBetween(prevPoint, currPoint);
			}

			// distance values use currently configured units
			values.setData(i, Distance.convertRadiansToDistance(totalRads, distUnit));

			prevPoint = currPoint;
		}
		return values;
	}

	/**
	 * Calculate the time values for each point in the given track
	 * @param inTrack track object
	 * @return time values in a ChartSeries object
	 */
	private static ChartSeries getTimeValues(Track inTrack)
	{
		// Calculate times and fill in in values array
		ChartSeries values = new ChartSeries(inTrack.getNumPoints());
		double seconds = 0.0;
		Timestamp prevTimestamp = null;
		for (int i=0; i<inTrack.getNumPoints(); i++)
		{
			DataPoint currPoint = inTrack.getPoint(i);
			if (currPoint.hasTimestamp())
			{
				if (!currPoint.getSegmentStart() && prevTimestamp != null) {
					seconds += (currPoint.getTimestamp().getMillisecondsSince(prevTimestamp) / 1000.0);
				}
				values.setData(i, seconds / 60.0 / 60.0);
				prevTimestamp = currPoint.getTimestamp();
			}
		}
		return values;
	}

	/**
	 * Calculate the altitude values for each point in the given track
	 * @param inTrack track object
	 * @return altitude values in a ChartSeries object
	 */
	private ChartSeries getAltitudeValues(Track inTrack)
	{
		ChartSeries values = new ChartSeries(inTrack.getNumPoints());
		final double multFactor = getConfig().getUnitSet().getAltitudeUnit().getMultFactorFromStd();
		for (int i=0; i<inTrack.getNumPoints(); i++) {
			if (inTrack.getPoint(i).hasAltitude()) {
				values.setData(i, inTrack.getPoint(i).getAltitude().getMetricValue() * multFactor);
			}
		}
		return values;
	}

	/**
	 * Calculate the speed values for each point in the given track
	 * @param inTrack track object
	 * @return speed values in a ChartSeries object
	 */
	private ChartSeries getSpeedValues(Track inTrack)
	{
		// Calculate speeds using the same formula as the profile chart
		SpeedData speeds = new SpeedData(inTrack);
		speeds.init(getConfig().getUnitSet());

		final int numPoints = inTrack.getNumPoints();
		ChartSeries values = new ChartSeries(numPoints);
		// Loop over collected points
		for (int i=0; i<numPoints; i++)
		{
			if (speeds.hasData(i)) {
				values.setData(i, speeds.getData(i));
			}
		}
		return values;
	}

	/**
	 * Calculate the vertical speed values for each point in the given track
	 * @param inTrack track object
	 * @return vertical speed values in a ChartSeries object
	 */
	private ChartSeries getVertSpeedValues(Track inTrack)
	{
		// Calculate speeds using the same formula as the profile chart
		VerticalSpeedData speeds = new VerticalSpeedData(inTrack);
		speeds.init(getConfig().getUnitSet());

		final int numPoints = inTrack.getNumPoints();
		ChartSeries values = new ChartSeries(numPoints);
		// Loop over collected points
		for (int i=0; i<numPoints; i++)
		{
			if (speeds.hasData(i))
			{
				values.setData(i, speeds.getData(i));
			}
		}
		return values;
	}


	/**
	 * Select a file to write for the SVG output
	 * @return selected File object or null if cancelled
	 */
	private File selectSvgFile()
	{
		if (_fileChooser == null)
		{
			_fileChooser = new JFileChooser();
			_fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
			_fileChooser.setFileFilter(new GenericFileFilter("filetypefilter.svg", new String[] {"svg"}));
			_fileChooser.setAcceptAllFileFilterUsed(false);
			// start from directory in config which should be set
			String configDir = getConfig().getConfigString(Config.KEY_TRACK_DIR);
			if (configDir != null) {_fileChooser.setCurrentDirectory(new File(configDir));}
		}
		boolean chooseAgain = true;
		while (chooseAgain)
		{
			chooseAgain = false;
			if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
			{
				// OK pressed and file chosen
				File file = _fileChooser.getSelectedFile();
				// Check file extension
				if (!file.getName().toLowerCase().endsWith(".svg")) {
					file = new File(file.getAbsolutePath() + ".svg");
				}
				// Check if file exists and if necessary prompt for overwrite
				Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
				if (!file.exists() || (file.canWrite() && JOptionPane.showOptionDialog(_parentFrame,
						I18nManager.getText("dialog.save.overwrite.text"),
						I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION,
						JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
					== JOptionPane.YES_OPTION))
				{
					return file;
				}
				chooseAgain = true;
			}
		}
		// Cancel pressed so no file selected
		return null;
	}


	/**
	 * @param inNumCharts number of charts to draw
	 * @return array of ints describing position and height of each subchart
	 */
	private static int[] getHeights(int inNumCharts)
	{
		if (inNumCharts <= 1) {return new int[] {0, 100};}
		if (inNumCharts == 2) {return new int[] {25, 75, 0, 25};}
		if (inNumCharts == 3) {return new int[] {40, 60, 20, 20, 0, 20};}
		return new int[] {54, 46, 36, 18, 18, 18, 0, 18};
	}
}
