package tim.prune.save;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.FlowLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.text.NumberFormat;
import java.util.Iterator;
import java.util.TreeSet;

import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
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.JTextField;
import javax.swing.SwingConstants;

import tim.prune.App;
import tim.prune.I18nManager;
import tim.prune.UpdateMessageBroker;
import tim.prune.config.Config;
import tim.prune.data.Track;
import tim.prune.function.Export3dFunction;
import tim.prune.gui.DialogCloser;
import tim.prune.load.GenericFileFilter;
import tim.prune.threedee.ThreeDModel;

/**
 * Class to export a 3d scene of the track to a specified Svg file
 */
public class SvgExporter extends Export3dFunction
{
	private Track _track = null;
	private JDialog _dialog = null;
	private JFileChooser _fileChooser = null;
	private double _phi = 0.0, _theta = 0.0;
	private JTextField _phiField = null, _thetaField = null;
	private JTextField _altitudeFactorField = null;
	private JCheckBox _gradientsCheckbox = null;
	private static final double _scaleFactor = 200.0;


	/**
	 * Constructor
	 * @param inApp App object
	 */
	public SvgExporter(App inApp)
	{
		super(inApp);
		_track = inApp.getTrackInfo().getTrack();
		// Set default rotation angles
		_phi = 30;  _theta = 55;
	}

	/** Get the name key */
	public String getNameKey() {
		return "function.exportsvg";
	}

	/**
	 * Set the rotation angles using coordinates for the camera
	 * @param inX X coordinate of camera
	 * @param inY Y coordinate of camera
	 * @param inZ Z coordinate of camera
	 */
	public void setCameraCoordinates(double inX, double inY, double inZ)
	{
		// Calculate phi and theta based on camera x,y,z
		_phi = Math.toDegrees(Math.atan2(inX, inZ));
		_theta = Math.toDegrees(Math.atan2(inY, Math.sqrt(inX*inX + inZ*inZ)));
	}


	/**
	 * Show the dialog to select options and export file
	 */
	public void begin()
	{
		// Make dialog window to select input parameters
		if (_dialog == null)
		{
			_dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
			_dialog.setLocationRelativeTo(_parentFrame);
			_dialog.getContentPane().add(makeDialogComponents());
		}
		// Get exaggeration factor from config
		final int exaggFactor = Config.getConfigInt(Config.KEY_HEIGHT_EXAGGERATION);
		if (exaggFactor > 0) {
			_altFactor = exaggFactor / 100.0;
		}

		// Set angles
		NumberFormat threeDP = NumberFormat.getNumberInstance();
		threeDP.setMaximumFractionDigits(3);
		_phiField.setText(threeDP.format(_phi));
		_thetaField.setText(threeDP.format(_theta));
		// Set vertical scale
		_altitudeFactorField.setText("" + _altFactor);
		// Show dialog
		_dialog.pack();
		_dialog.setVisible(true);
	}


	/**
	 * Make the dialog components to select the export options
	 * @return Component holding gui elements
	 */
	private Component makeDialogComponents()
	{
		JPanel panel = new JPanel();
		panel.setLayout(new BorderLayout());
		JLabel introLabel = new JLabel(I18nManager.getText("dialog.exportsvg.text"));
		introLabel.setBorder(BorderFactory.createEmptyBorder(4, 4, 6, 4));
		panel.add(introLabel, BorderLayout.NORTH);
		// OK, Cancel buttons
		JPanel buttonPanel = new JPanel();
		buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
		JButton okButton = new JButton(I18nManager.getText("button.ok"));
		okButton.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e)
			{
				doExport();
				_dialog.dispose();
			}
		});
		buttonPanel.add(okButton);
		JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
		cancelButton.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				_dialog.dispose();
			}
		});
		buttonPanel.add(cancelButton);
		panel.add(buttonPanel, BorderLayout.SOUTH);

		// central panel
		JPanel centralPanel = new JPanel();
		centralPanel.setLayout(new GridLayout(0, 2, 10, 4));

		// rotation angles
		JLabel phiLabel = new JLabel(I18nManager.getText("dialog.exportsvg.phi"));
		phiLabel.setHorizontalAlignment(SwingConstants.TRAILING);
		centralPanel.add(phiLabel);
		_phiField = new JTextField("" + _phi);
		_phiField.addKeyListener(new DialogCloser(_dialog));
		centralPanel.add(_phiField);
		JLabel thetaLabel = new JLabel(I18nManager.getText("dialog.exportsvg.theta"));
		thetaLabel.setHorizontalAlignment(SwingConstants.TRAILING);
		centralPanel.add(thetaLabel);
		_thetaField = new JTextField("" + _theta);
		centralPanel.add(_thetaField);
		// Altitude exaggeration
		JLabel altFactorLabel = new JLabel(I18nManager.getText("dialog.3d.altitudefactor"));
		altFactorLabel.setHorizontalAlignment(SwingConstants.TRAILING);
		centralPanel.add(altFactorLabel);
		_altitudeFactorField = new JTextField("" + _altFactor);
		centralPanel.add(_altitudeFactorField);
		// Checkbox for gradients or not
		JLabel gradientsLabel = new JLabel(I18nManager.getText("dialog.exportsvg.gradients"));
		gradientsLabel.setHorizontalAlignment(SwingConstants.TRAILING);
		centralPanel.add(gradientsLabel);
		_gradientsCheckbox = new JCheckBox();
		_gradientsCheckbox.setSelected(true);
		centralPanel.add(_gradientsCheckbox);

		// add this grid to the holder panel
		JPanel holderPanel = new JPanel();
		holderPanel.setLayout(new BorderLayout(5, 5));
		JPanel boxPanel = new JPanel();
		boxPanel.setLayout(new BoxLayout(boxPanel, BoxLayout.Y_AXIS));
		boxPanel.add(centralPanel);
		holderPanel.add(boxPanel, BorderLayout.CENTER);

		panel.add(holderPanel, BorderLayout.CENTER);
		return panel;
	}


	/**
	 * Select the file and export data to it
	 */
	private void doExport()
	{
		// Copy camera coordinates
		_phi = checkAngle(_phiField.getText());
		_theta = checkAngle(_thetaField.getText());

		// OK pressed, so choose output file
		if (_fileChooser == null)
		{
			_fileChooser = new JFileChooser();
			_fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
			_fileChooser.setFileFilter(new GenericFileFilter("filetype.svg", new String[] {"svg"}));
			_fileChooser.setAcceptAllFileFilterUsed(false);
			// start from directory in config which should be set
			final String configDir = Config.getConfigString(Config.KEY_TRACK_DIR);
			if (configDir != null) {_fileChooser.setCurrentDirectory(new File(configDir));}
		}

		// Allow choose again if an existing file is selected
		boolean chooseAgain = false;
		do
		{
			chooseAgain = false;
			if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
			{
				// OK pressed and file chosen
				File file = _fileChooser.getSelectedFile();
				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() || 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)
				{
					// Export the file
					if (exportFile(file))
					{
						// file saved - store directory in config for later
						Config.setConfigString(Config.KEY_TRACK_DIR, file.getParentFile().getAbsolutePath());
						// also store exaggeration
						Config.setConfigInt(Config.KEY_HEIGHT_EXAGGERATION, (int) (_altFactor * 100));
					}
					else {
						// export failed so need to choose again
						chooseAgain = true;
					}
				}
				else {
					// overwrite cancelled so need to choose again
					chooseAgain = true;
				}
			}
		} while (chooseAgain);
	}


	/**
	 * Export the track data to the specified file
	 * @param inFile File object to save to
	 * @return true if successful
	 */
	private boolean exportFile(File inFile)
	{
		FileWriter writer = null;
		// find out the line separator for this system
		String lineSeparator = System.getProperty("line.separator");
		try
		{
			// create and scale model
			ThreeDModel model = new ThreeDModel(_track);
			try
			{
				// try to use given altitude factor
				_altFactor = Double.parseDouble(_altitudeFactorField.getText());
				model.setAltitudeFactor(_altFactor);
			}
			catch (NumberFormatException nfe) {}
			model.scale();

			boolean useGradients = _gradientsCheckbox.isSelected();

			// Create file and write basics
			writer = new FileWriter(inFile);
			writeStartOfFile(writer, useGradients, lineSeparator);
			writeBasePlane(writer, lineSeparator);
			// write out cardinal letters NESW
			writeCardinals(writer, lineSeparator);

			// write out points
			writeDataPoints(writer, model, useGradients, lineSeparator);
			writeEndOfFile(writer, lineSeparator);

			// everything worked
			UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.save.ok1")
				 + " " + _track.getNumPoints() + " " + I18nManager.getText("confirm.save.ok2")
				 + " " + inFile.getAbsolutePath());
			return true;
		}
		catch (IOException ioe)
		{
			JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.save.failed") + " : " + ioe.getMessage(),
				I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
		}
		finally
		{
			// close file ignoring exceptions
			try {
				writer.close();
			}
			catch (Exception e) {}
		}
		return false;
	}


	/**
	 * Write the start of the Svg file
	 * @param inWriter Writer to use for writing file
	 * @param inUseGradients true to use gradients, false for flat fills
	 * @param inLineSeparator line separator to use
	 * @throws IOException on file writing error
	 */
	private static void writeStartOfFile(FileWriter inWriter, boolean inUseGradients,
		String inLineSeparator)
	throws IOException
	{
		inWriter.write("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>");
		inWriter.write(inLineSeparator);
		inWriter.write("<!-- Svg file produced by GpsPrune - see http://activityworkshop.net/ -->");
		inWriter.write(inLineSeparator);
		inWriter.write("<svg width=\"800\" height=\"700\">");
		inWriter.write(inLineSeparator);
		if (inUseGradients)
		{
			final String defs = "<defs>" +
				"<radialGradient id=\"wayfill\" cx=\"0.5\" cy=\"0.5\" r=\"0.5\" fx=\"0.5\" fy=\"0.5\">" +
				"<stop offset=\"0%\" stop-color=\"#2323aa\"/>" +
				"<stop offset=\"100%\" stop-color=\"#000080\"/>" +
				"</radialGradient>" + inLineSeparator +
				"<radialGradient id=\"trackfill\" cx=\"0.5\" cy=\"0.5\" r=\"0.5\" fx=\"0.5\" fy=\"0.5\">" +
				"<stop offset=\"0%\" stop-color=\"#23aa23\"/>" +
				"<stop offset=\"100%\" stop-color=\"#008000\"/>" +
				"</radialGradient>" +
				"</defs>";
			inWriter.write(defs);
			inWriter.write(inLineSeparator);
		}
		inWriter.write("<g inkscape:label=\"Layer 1\" inkscape:groupmode=\"layer\" id=\"layer1\">");
		inWriter.write(inLineSeparator);
	}

	/**
	 * Write the base plane
	 * @param inWriter Writer to use for writing file
	 * @param inLineSeparator line separator to use
	 * @throws IOException on file writing error
	 */
	private void writeBasePlane(FileWriter inWriter, String inLineSeparator)
	throws IOException
	{
		// Use model size and camera angles to draw path for base rectangle (using 3d transform)
		int[] coords1 = convertCoordinates(-1.0, -1.0, 0);
		int[] coords2 = convertCoordinates(1.0, -1.0, 0);
		int[] coords3 = convertCoordinates(1.0, 1.0, 0);
		int[] coords4 = convertCoordinates(-1.0, 1.0, 0);
		final String corners = "M " + coords1[0] + "," + coords1[1]
			+ " L " + coords2[0] + "," + coords2[1]
			+ " L " + coords3[0] + "," + coords3[1]
			+ " L " + coords4[0] + "," + coords4[1] + " z";
		inWriter.write("<path style=\"fill:#446666;stroke:#000000;\" d=\"" + corners + "\" id=\"rect1\" />");
		inWriter.write(inLineSeparator);
	}

	/**
	 * Write the cardinal letters NESW
	 * @param inWriter Writer to use for writing file
	 * @param inLineSeparator line separator to use
	 * @throws IOException on file writing error
	 */
	private void writeCardinals(FileWriter inWriter, String inLineSeparator)
	throws IOException
	{
		// Use model size and camera angles to calculate positions
		int[] coordsN = convertCoordinates(0, 1.0, 0);
		writeCardinal(inWriter, coordsN[0], coordsN[1], "cardinal.n", inLineSeparator);
		int[] coordsE = convertCoordinates(1.0, 0, 0);
		writeCardinal(inWriter, coordsE[0], coordsE[1], "cardinal.e", inLineSeparator);
		int[] coordsS = convertCoordinates(0, -1.0, 0);
		writeCardinal(inWriter, coordsS[0], coordsS[1], "cardinal.s", inLineSeparator);
		int[] coordsW = convertCoordinates(-1.0, 0, 0);
		writeCardinal(inWriter, coordsW[0], coordsW[1], "cardinal.w", inLineSeparator);
	}

	/**
	 * Write a single cardinal letter
	 * @param inWriter Writer to use for writing file
	 * @param inX x coordinate
	 * @param inY y coordinate
	 * @param inKey key for string to write
	 * @param inLineSeparator line separator to use
	 * @throws IOException on file writing error
	 */
	private static void writeCardinal(FileWriter inWriter, int inX, int inY, String inKey, String inLineSeparator)
	throws IOException
	{
		inWriter.write("<text x=\"" + inX + "\" y=\"" + inY + "\" font-size=\"26\" fill=\"black\" " +
			"stroke=\"white\" stroke-width=\"0.5\">");
		inWriter.write(I18nManager.getText(inKey));
		inWriter.write("</text>");
		inWriter.write(inLineSeparator);
	}

	/**
	 * Convert the given 3d coordinates into 2d coordinates by rotating and mapping
	 * @param inX x coordinate (east)
	 * @param inY y coordinate (north)
	 * @param inZ z coordinate (up)
	 * @return 2d coordinates as integer array
	 */
	private int[] convertCoordinates(double inX, double inY, double inZ)
	{
		// Rotate by phi degrees around vertical axis
		final double cosPhi = Math.cos(Math.toRadians(_phi));
		final double sinPhi = Math.sin(Math.toRadians(_phi));
		final double x2 = inX * cosPhi + inY * sinPhi;
		final double y2 = inY * cosPhi - inX * sinPhi;
		final double z2 = inZ;
		// Rotate by theta degrees around horizontal axis
		final double cosTheta = Math.cos(Math.toRadians(_theta));
		final double sinTheta = Math.sin(Math.toRadians(_theta));
		double x3 = x2;
		double y3 = y2 * sinTheta + z2 * cosTheta;
		// don't need to calculate z3
		// Scale results to sensible scale for svg
		x3 = x3 * _scaleFactor + 400;
		y3 = -y3 * _scaleFactor + 350;
		return new int[] {(int) x3, (int) y3};
	}

	/**
	 * Finish off the file by closing the tags
	 * @param inWriter Writer to use for writing file
	 * @param inLineSeparator line separator to use
	 * @throws IOException on file writing error
	 */
	private static void writeEndOfFile(FileWriter inWriter, String inLineSeparator)
	throws IOException
	{
		inWriter.write(inLineSeparator);
		inWriter.write("</g></svg>");
		inWriter.write(inLineSeparator);
	}

	/**
	 * Write out all the data points to the file in the balls-and-sticks style
	 * @param inWriter Writer to use for writing file
	 * @param inModel model object for getting data points
	 * @param inUseGradients true to use gradients, false for flat fills
	 * @param inLineSeparator line separator to use
	 * @throws IOException on file writing error
	 */
	private void writeDataPoints(FileWriter inWriter, ThreeDModel inModel, boolean inUseGradients,
		String inLineSeparator)
	throws IOException
	{
		final int numPoints = inModel.getNumPoints();
		TreeSet<SvgFragment> fragments = new TreeSet<SvgFragment>();
		for (int i=0; i<numPoints; i++)
		{
			StringBuilder builder = new StringBuilder();
			int[] coords = convertCoordinates(inModel.getScaledHorizValue(i), inModel.getScaledVertValue(i),
				inModel.getScaledAltValue(i));
			// vertical rod (if altitude positive)
			if (inModel.getScaledAltValue(i) > 0.0)
			{
				int[] baseCoords = convertCoordinates(inModel.getScaledHorizValue(i), inModel.getScaledVertValue(i), 0);
				builder.append("<line x1=\"").append(baseCoords[0]).append("\" y1=\"").append(baseCoords[1])
					.append("\" x2=\"").append(coords[0]).append("\" y2=\"").append(coords[1])
					.append("\" stroke=\"gray\" stroke-width=\"3\" />");
				builder.append(inLineSeparator);
			}
			// ball (different according to type)
			if (inModel.getPointType(i) == ThreeDModel.POINT_TYPE_WAYPOINT)
			{
				// waypoint ball
				builder.append("<circle cx=\"").append(coords[0]).append("\" cy=\"").append(coords[1])
					.append("\" r=\"11\" ").append(inUseGradients?"fill=\"url(#wayfill)\"":"fill=\"blue\"")
					.append(" stroke=\"green\" stroke-width=\"0.2\" />");
			}
			else
			{
				// normal track point ball
				builder.append("<circle cx=\"").append(coords[0]).append("\" cy=\"").append(coords[1])
					.append("\" r=\"7\" ").append(inUseGradients?"fill=\"url(#trackfill)\"":"fill=\"green\"")
					.append(" stroke=\"blue\" stroke-width=\"0.2\" />");
			}
			builder.append(inLineSeparator);
			// add to set
			fragments.add(new SvgFragment(builder.toString(), coords[1]));
		}

		// Iterate over the sorted set and write to file
		Iterator<SvgFragment> iterator = fragments.iterator();
		while (iterator.hasNext()) {
			inWriter.write(iterator.next().getFragment());
		}
	}


	/**
	 * Check the given angle value
	 * @param inString String entered by user
	 * @return validated value
	 */
	private static double checkAngle(String inString)
	{
		double value = 0.0;
		try {
			value = Double.parseDouble(inString);
		}
		catch (Exception e) {} // ignore parse failures
		return value;
	}
}
