package tim.prune.save;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.FlowLayout;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.List;

import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.border.EtchedBorder;

import tim.prune.App;
import tim.prune.GenericFunction;
import tim.prune.I18nManager;
import tim.prune.UpdateMessageBroker;
import tim.prune.config.Config;
import tim.prune.data.FileInfo;
import tim.prune.data.FileType;
import tim.prune.data.TrackInfo;
import tim.prune.gui.DialogCloser;
import tim.prune.gui.GuiGridLayout;
import tim.prune.gui.ProgressDialog;
import tim.prune.load.GenericFileFilter;
import tim.prune.save.xml.GpxWriter;
import tim.prune.save.xml.GpxWriter10;
import tim.prune.save.xml.GpxWriter11;
import tim.prune.save.xml.XmlUtils;


/**
 * Class to export track information
 * into a specified Gpx file
 */
public class GpxExporter extends GenericFunction
{
	private final TrackInfo _trackInfo;
	private JDialog _dialog = null;
	private JComboBox<String> _titleField = null;
	private JComboBox<String> _descriptionField = null;
	private JComboBox<String> _versionField = null;
	private PointTypeSelector _pointTypeSelector = null;
	private JCheckBox _timestampsCheckbox = null;
	private JCheckBox _descsToCommentsCheckbox = null;
	private JPanel _encodingsPanel = null;
	private JRadioButton _useSystemRadio = null, _forceUtf8Radio = null;
	private File _exportFile = null;
	private boolean _cancelled = false;
	private ProgressDialog _progress = null;


	/**
	 * Constructor
	 * @param inApp app object
	 */
	public GpxExporter(App inApp)
	{
		super(inApp);
		_trackInfo = inApp.getTrackInfo();
	}

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

	/**
	 * Show the dialog to select options and export file
	 */
	public void begin()
	{
		// 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();
		}
		_pointTypeSelector.init(_app.getTrackInfo());
		_encodingsPanel.setVisible(!XmlUtils.isSystemUtf8());
		if (!XmlUtils.isSystemUtf8())
		{
			String systemEncoding = XmlUtils.getSystemEncoding();
			_useSystemRadio.setText(I18nManager.getText("dialog.exportgpx.encoding.system")
				+ " (" + (systemEncoding == null ? "unknown" : systemEncoding) + ")");
		}
		populateComboboxes();
		_dialog.setVisible(true);
	}


	/**
	 * Create dialog components
	 * @return Panel containing all gui elements in dialog
	 */
	private Component makeDialogComponents()
	{
		JPanel dialogPanel = new JPanel();
		dialogPanel.setLayout(new BorderLayout());
		JPanel mainPanel = new JPanel();
		mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));

		// Make a panel for the name/desc text boxes
		final String longStringToDefineWidth = "Long enough for a reasonable description";
		JPanel descPanel = new JPanel();
		double[] weights = new double[] {0.2, 0.8};
		boolean[] aligns = new boolean[] {true, false};
		GuiGridLayout comboGrid = new GuiGridLayout(descPanel, weights, aligns);
		comboGrid.add(new JLabel(I18nManager.getText("dialog.exportgpx.name")));
		_titleField = new JComboBox<>();
		_titleField.setPrototypeDisplayValue(longStringToDefineWidth);
		_titleField.setEditable(true);
		comboGrid.add(_titleField);
		comboGrid.add(new JLabel(I18nManager.getText("dialog.exportgpx.desc")));
		_descriptionField = new JComboBox<>();
		_descriptionField.setPrototypeDisplayValue(longStringToDefineWidth);
		_descriptionField.setEditable(true);
		comboGrid.add(_descriptionField);
		// file version
		comboGrid.add(new JLabel(I18nManager.getText("dialog.exportxml.version")));
		_versionField = new JComboBox<>();
		_versionField.setPrototypeDisplayValue(longStringToDefineWidth);
		_versionField.setEditable(false);
		comboGrid.add(_versionField);
		mainPanel.add(descPanel);
		mainPanel.add(Box.createVerticalStrut(15));
		// point type selection (track points, waypoints, photo points)
		_pointTypeSelector = new PointTypeSelector();
		mainPanel.add(_pointTypeSelector);
		mainPanel.add(Box.createVerticalStrut(15));
		// checkboxes for timestamps and copying
		JPanel checkPanel = new JPanel();
		weights = new double[] {0.5, 0.5};
		aligns = new boolean[] {false, false};
		GuiGridLayout checkGrid = new GuiGridLayout(checkPanel, weights, aligns);
		_timestampsCheckbox = new JCheckBox(I18nManager.getText("dialog.exportgpx.includetimestamps"));
		_timestampsCheckbox.setSelected(true);
		checkGrid.add(_timestampsCheckbox);
		_descsToCommentsCheckbox = new JCheckBox(I18nManager.getText("dialog.exportgpx.descriptionstocomments"));
		checkGrid.add(_descsToCommentsCheckbox, 2, true);
		mainPanel.add(checkPanel);
		// panel for selecting character encoding
		_encodingsPanel = new JPanel();
		if (!XmlUtils.isSystemUtf8())
		{
			// only add this panel if system isn't utf8 (or can't be identified yet)
			_encodingsPanel.setBorder(BorderFactory.createCompoundBorder(
				BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), BorderFactory.createEmptyBorder(4, 4, 4, 4)));
			_encodingsPanel.setLayout(new BorderLayout());
			_encodingsPanel.add(new JLabel(I18nManager.getText("dialog.exportgpx.encoding")), BorderLayout.NORTH);
			JPanel radioPanel = new JPanel();
			radioPanel.setLayout(new FlowLayout());
			ButtonGroup radioGroup = new ButtonGroup();
			_useSystemRadio = new JRadioButton(I18nManager.getText("dialog.exportgpx.encoding.system"));
			_forceUtf8Radio = new JRadioButton(I18nManager.getText("dialog.exportgpx.encoding.utf8"));
			radioGroup.add(_useSystemRadio);
			radioGroup.add(_forceUtf8Radio);
			radioPanel.add(_useSystemRadio);
			radioPanel.add(_forceUtf8Radio);
			_useSystemRadio.setSelected(true);
			_encodingsPanel.add(radioPanel, BorderLayout.CENTER);
			mainPanel.add(_encodingsPanel);
		}
		dialogPanel.add(mainPanel, BorderLayout.CENTER);

		// close dialog if escape pressed
		_titleField.addKeyListener(new DialogCloser(_dialog));
		// button panel at bottom
		JPanel buttonPanel = new JPanel();
		buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
		JButton okButton = new JButton(I18nManager.getText("button.ok"));
		ActionListener okListener = e -> startExport();
		okButton.addActionListener(okListener);
		buttonPanel.add(okButton);
		JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
		cancelButton.addActionListener(e -> _dialog.dispose());
		buttonPanel.add(cancelButton);
		dialogPanel.add(buttonPanel, BorderLayout.SOUTH);
		dialogPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 15));
		return dialogPanel;
	}

	/**
	 * Set the suggestions for the title, description and version with which to export
	 */
	private void populateComboboxes()
	{
		final FileInfo fileInfo = _app.getTrackInfo().getFileInfo();
		populateCombobox(_titleField, fileInfo.getAllTitles());
		populateCombobox(_descriptionField, fileInfo.getAllDescriptions());
		_versionField.removeAllItems();
		// Check whether any extensions are used or not
		final boolean hasExtensions = fileInfo.hasExtensions(FileType.GPX);
		_versionField.addItem(I18nManager.getText("dialog.exportgpx.version10" + (hasExtensions ? "withextensions" : "")));
		_versionField.addItem(I18nManager.getText("dialog.exportgpx.version11" + (hasExtensions ? "withextensions" : "")));
		// Find best GPX version and pre-select item from combo
		boolean isGpx10 = shouldExportGpx10(fileInfo);
		_versionField.setSelectedIndex(isGpx10 ? 0 : 1);
	}

	/** @return true if Gpx version 1.0 should be pre-selected in the combobox */
	private boolean shouldExportGpx10(FileInfo inFileInfo)
	{
		VersionCombiner combiner = new VersionCombiner(List.of("1.0", "1.1"));
		for (String version : inFileInfo.getVersions(FileType.GPX)) {
			combiner.addVersion(version);
		}
		return combiner.getBestVersion().equals("1.0");
	}

	/** Add the specified values to the combobox */
	private static void populateCombobox(JComboBox<String> inCombobox, List<String> inValues)
	{
		final String initialValue = getEnteredText(inCombobox);
		inCombobox.removeAllItems();
		for (String value : inValues) {
			inCombobox.addItem(value);
		}
		if (initialValue.isEmpty() && inCombobox.getItemCount() > 0) {
			inCombobox.setSelectedIndex(0);
		}
		else {
			inCombobox.setSelectedItem(initialValue);
		}
	}

	/**
	 * Start the export process based on the input parameters
	 */
	private void startExport()
	{
		// OK pressed, so check selections
		if (!_pointTypeSelector.getAnythingSelected())
		{
			JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.save.notypesselected"),
				I18nManager.getText("dialog.saveoptions.title"), JOptionPane.WARNING_MESSAGE);
			return;
		}
		// Choose output file
		String configDir = getConfig().getConfigString(Config.KEY_TRACK_DIR);
		File saveFile = chooseGpxFile(_parentFrame, getEnteredName(), configDir);
		if (saveFile != null)
		{
			// New file or overwrite confirmed, so initiate export in separate thread
			_exportFile = saveFile;
			_cancelled = false;
			_progress = new ProgressDialog(_parentFrame, getNameKey(), null, () -> _cancelled = true);
			_progress.show();
			new Thread(this::run).start();
			_dialog.dispose();
		}
	}


	/**
	 * Select a GPX file to save to
	 * @param inParentFrame parent frame for file chooser dialog
	 * @param inTrackName track name entered earlier
	 * @param inDirectory directory to save to, or null if unknown
	 * @return selected File, or null if selection cancelled
	 */
	public static File chooseGpxFile(JFrame inParentFrame, String inTrackName, String inDirectory)
	{
		File saveFile = null;
		JFileChooser fileChooser = new JFileChooser();
		fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
		fileChooser.setFileFilter(new GenericFileFilter("filetypefilter.gpx", new String[] {"gpx"}));
		fileChooser.setAcceptAllFileFilterUsed(false);
		// start from directory in config which should be set
		if (inDirectory != null) {
			fileChooser.setCurrentDirectory(new File(inDirectory));
		}
		// Make suggestion of filename based on already selected track name
		final String suggestedFilename = makeFilenameFromTrackName(inTrackName);
		if (suggestedFilename != null) {
			fileChooser.setSelectedFile(new File(suggestedFilename));
		}

		// Allow choose again if an existing file is selected
		boolean chooseAgain = false;
		do
		{
			chooseAgain = false;
			if (fileChooser.showSaveDialog(inParentFrame) == JFileChooser.APPROVE_OPTION)
			{
				// OK pressed and file chosen
				File file = fileChooser.getSelectedFile();
				// Check file extension
				if (!file.getName().toLowerCase().endsWith(".gpx"))
				{
					file = new File(file.getAbsolutePath() + ".gpx");
				}
				// 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(inParentFrame,
						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)
				{
					// new file or overwrite confirmed
					saveFile = file;
				}
				else
				{
					// file exists and overwrite cancelled - select again
					chooseAgain = true;
				}
			}
		} while (chooseAgain);
		return saveFile;
	}

	/**
	 * @param inTrackName track name entered by user
	 * @return suggested filename (without spaces), or null
	 */
	private static String makeFilenameFromTrackName(String inTrackName)
	{
		String filename = (inTrackName == null ? "" : inTrackName.trim());
		if (filename.equals("")) {
			return null;
		}
		filename = filename.replaceAll(" ", "_");
		if (filename.length() > 20) {
			filename = filename.substring(0, 20);
		}
		if (!filename.toLowerCase().endsWith(".gpx")) {
			filename = filename + ".gpx";
		}
		return filename;
	}


	/**
	 * Run method for controlling separate thread for exporting
	 */
	public void run()
	{
		OutputStreamWriter writer = null;
		// TODO: Try with resources
		try
		{
			// normal writing to file - firstly specify UTF8 encoding if requested
			if (_forceUtf8Radio != null && _forceUtf8Radio.isSelected()) {
				writer = new OutputStreamWriter(new FileOutputStream(_exportFile), StandardCharsets.UTF_8);
			}
			else {
				writer = new OutputStreamWriter(new FileOutputStream(_exportFile));
			}
			SettingsForExport settings = new SettingsForExport();
			settings.setExportTrackPoints(_pointTypeSelector.getTrackpointsSelected());
			settings.setExportWaypoints(_pointTypeSelector.getWaypointsSelected());
			settings.setExportPhotoPoints(_pointTypeSelector.getPhotopointsSelected());
			settings.setExportAudiopoints(_pointTypeSelector.getAudiopointsSelected());
			settings.setExportJustSelection(_pointTypeSelector.getJustSelection());
			settings.setExportTimestamps(_timestampsCheckbox.isSelected());
			settings.setCopyDescriptionsToComments(_descsToCommentsCheckbox.isSelected());
			// write file
			final String name = getEnteredName();
			final String description = getEnteredDescription();
			final boolean useGpx10 = _versionField.getSelectedIndex() == 0;
			GpxWriter gpxWriter = useGpx10 ? new GpxWriter10(_progress, settings)
				: new GpxWriter11(_progress, settings);
			final int numPoints = gpxWriter.exportData(writer, _trackInfo, name, description, _app.getTrackInfo().getFileInfo());

			// close file
			writer.close();
			_progress.close();
			if (_cancelled) {
				UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.cancelled"));
				return;
			}
			// Store directory in config for later
			getConfig().setConfigString(Config.KEY_TRACK_DIR, _exportFile.getParentFile().getAbsolutePath());
			// Add to recent file list
			_app.addRecentFile(_exportFile, true);
			// Show confirmation
			UpdateMessageBroker.informSubscribers();
			UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.save.ok1")
				 + " " + numPoints + " " + I18nManager.getText("confirm.save.ok2")
				 + " " + _exportFile.getAbsolutePath());
			// export successful
			_app.informDataSaved();
			return;
		}
		catch (IOException ioe)
		{
			try {
				if (writer != null) writer.close();
			}
			catch (IOException ioe2) {}
			JOptionPane.showMessageDialog(_parentFrame,
				I18nManager.getText("error.save.failed") + " : " + ioe.getMessage(),
				I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
		}
		// if not returned already, export failed so need to recall the file selection
		startExport();
	}

	private String getEnteredName() {
		return getEnteredText(_titleField);
	}

	private String getEnteredDescription() {
		return getEnteredText(_descriptionField);
	}

	private static String getEnteredText(JComboBox<String> inCombo)
	{
		Object item = inCombo.getSelectedItem();
		return item == null ? "" : item.toString();
	}
}
