package tim.mkgmapgui;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.FlowLayout;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.lang.reflect.Method;

import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.border.EtchedBorder;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;

/**
 * Class for controlling the gui window for the Mkgmap wrapper
 */
public class GuiWindow implements GuiController
{
	/** Classloader to get mkgmap jar */
	private ClassLoader _classLoader = null;
	/** Main dialog window */
	private JFrame _mainWindow = null;
	/** Text field for displaying/entering osm file path */
	private JTextField _osmField = null;
	/** Text field for entering map number */
	private JTextField _mapNumField = null;
	/** Button to start conversion */
	private JButton _convertButton = null;
	/** File chooser for osm files */
	private JFileChooser _osmFileChooser = null;
	/** File chooser for img files */
	private JFileChooser _imgFileChooser = null;
	/** File list model for img files */
	private FileListModel _fileListModel = null;
	/** List for img files */
	private JList _fileList = null;
	/** Button to remove file from list */
	private JButton _removeImgFileButton = null;
	/** Button to start combination */
	private JButton _combineButton = null;
	/** Operation to perform */
	private Operation _operation = null;
	/** Flag for operation failure */
	private boolean _operationFailed = false;
	/** Progress bar */
	private JProgressBar _progressBar = null;


	/**
	 * Constructor
	 * @param inClassLoader class loader
	 */
	public GuiWindow(ClassLoader inClassLoader)
	{
		_classLoader = inClassLoader;
		_osmFileChooser = new JFileChooser();
		_osmFileChooser.setFileFilter(new SingleFileFilter("OSM files", ".osm"));
		_osmFileChooser.setAcceptAllFileFilterUsed(false);
		_imgFileChooser = new JFileChooser();
		_imgFileChooser.setFileFilter(new SingleFileFilter("IMG files", ".img"));
		_imgFileChooser.setAcceptAllFileFilterUsed(false);
	}

	/**
	 * Show the dialog
	 */
	public void showDialog()
	{
		_mainWindow = new JFrame("Mkgmap GUI 1.1");
		_mainWindow.addWindowListener(new WindowAdapter() {
			public void windowClosing(WindowEvent e) {
				System.exit(0);
			}
		});
		_mainWindow.getContentPane().add(makeComponents());
		_mainWindow.pack();
		_mainWindow.setVisible(true);
	}

	/**
	 * @return GUI components
	 */
	private Component makeComponents()
	{
		JPanel gridPanel = new JPanel();
		gridPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
		gridPanel.setLayout(new GridLayout(1, 2, 5, 5));
		// Convert panel
		JPanel convertPanel = makePanel();
		// Add file chooser for osm file
		JPanel chooseOsmPanel = new JPanel();
		chooseOsmPanel.setLayout(new BorderLayout(0, 4));
		chooseOsmPanel.add(new JLabel("Converts an osm file to an img file"), BorderLayout.NORTH);
		JPanel chooseOptionsPanel = new JPanel();
		GridBagLayout gridbag = new GridBagLayout();
		chooseOptionsPanel.setLayout(gridbag);
		GridBagConstraints c = new GridBagConstraints();
		c.gridx = 0;     c.gridy = 0;
		c.weightx = 0.0; c.weighty = 0.0;
		c.insets = new Insets(1, 2, 1, 2);
		JLabel label = new JLabel("Osm file : ");
		gridbag.setConstraints(label, c);
		chooseOptionsPanel.add(label);
		// text field for input file
		_osmField = new JTextField("", 11);
		_osmField.addKeyListener(new KeyAdapter() {
			public void keyReleased(KeyEvent e) {
				_convertButton.setEnabled(_osmField.getText().length() > 0);
				super.keyReleased(e);
			}
		});
		c.gridx = 1;
		c.weightx = 1;
		c.fill = GridBagConstraints.HORIZONTAL;
		gridbag.setConstraints(_osmField, c);
		chooseOptionsPanel.add(_osmField);
		// browse button
		JButton chooseOsmButton = new JButton("Browse ...");
		chooseOsmButton.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent arg0) {
				int answer = _osmFileChooser.showOpenDialog(_mainWindow);
				if (answer == JFileChooser.APPROVE_OPTION) {
					_osmField.setText(_osmFileChooser.getSelectedFile().getAbsolutePath());
					_convertButton.setEnabled(true);
				}
			}
		});
		c.gridx = 2;
		c.weightx = 0;
		c.fill = GridBagConstraints.NONE;
		gridbag.setConstraints(chooseOsmButton, c);
		chooseOptionsPanel.add(chooseOsmButton);
		// second row
		label = new JLabel("Map number : ");
		c.gridx = 0; c.gridy = 1;
		gridbag.setConstraints(label, c);
		chooseOptionsPanel.add(label);
		_mapNumField = new JTextField("", 10);
		c.gridx = 1;
		c.weightx = 1.0;
		c.fill = GridBagConstraints.HORIZONTAL;
		gridbag.setConstraints(_mapNumField, c);
		chooseOptionsPanel.add(_mapNumField);
		// put together
		chooseOsmPanel.add(chooseOptionsPanel, BorderLayout.CENTER);
		convertPanel.add(chooseOsmPanel, BorderLayout.NORTH);
		// (and maybe for config file too?)
		_convertButton = addOkButton("Convert", convertPanel);
		_convertButton.setEnabled(false);
		_convertButton.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				convertOsmFile(_osmField.getText());
			}
		});
		gridPanel.add(convertPanel);

		// Combine panel
		JPanel combinePanel = makePanel();
		combinePanel.add(new JLabel("Combines several img files into one"), BorderLayout.NORTH);
		// Add list of osm files to combine, buttons to add new one, remove one
		_fileListModel = new FileListModel();
		_fileList = new JList(_fileListModel);
		_fileList.addListSelectionListener(new ListSelectionListener() {
			public void valueChanged(ListSelectionEvent inEvent) {
				if (!inEvent.getValueIsAdjusting()) {
					checkCombineButtons();
				}
			}
		});
		combinePanel.add(new JScrollPane(_fileList), BorderLayout.CENTER);
		JPanel combineButtonPanel = new JPanel();
		combineButtonPanel.setLayout(new BoxLayout(combineButtonPanel, BoxLayout.Y_AXIS));
		JButton addImgButton = new JButton("Add file");
		addImgButton.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent arg0) {
				addImgFile();
			}
		});
		combineButtonPanel.add(addImgButton);
		_removeImgFileButton = new JButton("Remove file");
		_removeImgFileButton.setEnabled(false);
		_removeImgFileButton.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent arg0) {
				removeImgFile();
			}
		});
		combineButtonPanel.add(_removeImgFileButton);
		combinePanel.add(combineButtonPanel, BorderLayout.EAST);
		_combineButton = addOkButton("Combine", combinePanel);
		_combineButton.setEnabled(false);
		_combineButton.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				combineImgFiles(_fileListModel.getFiles());
			}
		});
		gridPanel.add(combinePanel);

		// Add progress bar
		JPanel mainPanel = new JPanel();
		mainPanel.setLayout(new BorderLayout());
		mainPanel.add(gridPanel, BorderLayout.CENTER);
		_progressBar = new JProgressBar();
		_progressBar.setVisible(false);
		_progressBar.setIndeterminate(true);
		mainPanel.add(_progressBar, BorderLayout.SOUTH);
		return mainPanel;
	}


	/**
	 * @return a panel for each of the two options
	 */
	private JPanel makePanel()
	{
		JPanel panel = new JPanel();
		panel.setLayout(new BorderLayout(1, 2));
		panel.setBorder(BorderFactory.createCompoundBorder(
			BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), BorderFactory.createEmptyBorder(3, 3, 3, 3))
		);
		return panel;
	}

	/**
	 * Add an OK button to the given panel
	 * @param inText text for button
	 * @param inParent parent panel with borderlayout
	 * @return button (which is already added)
	 */
	private JButton addOkButton(String inText, JPanel inParent)
	{
		JPanel buttonPanel = new JPanel();
		buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
		JButton button = new JButton(inText);
		buttonPanel.add(button);
		inParent.add(buttonPanel, BorderLayout.SOUTH);
		return button;
	}

	/**
	 * Do the conversion using the specified osm file
	 * @param inOsmPath path to osm file
	 */
	private void convertOsmFile(String inOsmPath)
	{
		// Check osm path is ok
		if (inOsmPath == null) {return;}
		File osmFile = new File(inOsmPath);
		if (osmFile.exists() && osmFile.canRead() && osmFile.isFile()
			&& osmFile.getAbsolutePath().toLowerCase().endsWith(".osm"))
		{
			// Select output file
			int answer = _imgFileChooser.showSaveDialog(_mainWindow);
			// Maybe set default here
			if (answer == JFileChooser.APPROVE_OPTION)
			{
				File saveFile = _imgFileChooser.getSelectedFile();
				if (!saveFile.getName().toLowerCase().endsWith(".img")) {
					saveFile = new File(saveFile.getAbsolutePath() + ".img");
				}
				// Check whether to overwrite file
				if (saveFile.exists()
					&& JOptionPane.showConfirmDialog(_mainWindow, "Overwrite file " + saveFile.getAbsolutePath() + " ?",
					"File exists", JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION)
				{
					// Don't overwrite file, abort
					return;
				}
				// Use given map number if any
				int mapNum = 12345678;
				try
				{
					int enteredNum = Integer.parseInt(_mapNumField.getText());
					if (enteredNum > 0 && enteredNum < 1e9) {
						mapNum = enteredNum;
					}
					else {
						JOptionPane.showMessageDialog(_mainWindow,
							"Map number '" + enteredNum + "' is out of range - using '" + mapNum + "' instead.",
							"Problem with map number", JOptionPane.WARNING_MESSAGE);
					}
				} catch (NumberFormatException nfe) {
					JOptionPane.showMessageDialog(_mainWindow,
						"Couldn't turn '" + _mapNumField.getText() + "' into a number - using '" + mapNum + "' instead.",
						"Problem with map number", JOptionPane.WARNING_MESSAGE);
				}
				String mapName = "" + mapNum;
				try
				{
					// Make Operation object and start in new thread
					Thread mkgmapThread = launchOperation(Operation.makeConvertOperation(osmFile, mapName, saveFile));
					// Start new polling thread
					new Thread(new ThreadPoller(this, mkgmapThread)).start();
				}
				catch (Exception e) {
					JOptionPane.showMessageDialog(_mainWindow, "Invocation error: " + e.getMessage(),
						"Error", JOptionPane.ERROR_MESSAGE);
				}
			}
		}
		else {
			JOptionPane.showMessageDialog(_mainWindow, "Selected file is not a readable osm file",
				"Error", JOptionPane.ERROR_MESSAGE);
		}
	}

	/**
	 * Add an img file to the list
	 */
	private void addImgFile()
	{
		int result = _imgFileChooser.showOpenDialog(_mainWindow);
		if (result == JFileChooser.APPROVE_OPTION) {
			_fileListModel.addFile(_imgFileChooser.getSelectedFile());
		}
		checkCombineButtons();
	}

	/**
	 * Remove img file from list
	 */
	private void removeImgFile()
	{
		int selIndex = _fileList.getSelectedIndex();
		if (selIndex >= 0 && selIndex < _fileListModel.getSize()) {
			_fileListModel.removeFile(selIndex);
		}
		checkCombineButtons();
	}

	/**
	 * Enable or disable the combine buttons
	 */
	private void checkCombineButtons()
	{
		_removeImgFileButton.setEnabled(_fileListModel.getSize() > 0
			&& _fileList.getSelectedIndex() >= 0
			&& _fileList.getSelectedIndex() < _fileListModel.getSize());
		_combineButton.setEnabled(_fileListModel.getSize() > 0);
	}

	/**
	 * Combine the selected files to a new img file
	 * @param inFiles array of filenames of img files to combine
	 */
	private void combineImgFiles(String[] inFiles)
	{
		// Select output file to save to
		_imgFileChooser.cancelSelection();
		_imgFileChooser.setSelectedFile(new File("gmapsupp.img"));
		int result = _imgFileChooser.showSaveDialog(_mainWindow);
		if (result == JFileChooser.APPROVE_OPTION)
		{
			// Make Operation object and start in new thread
			Thread mkgmapThread = launchOperation(Operation.makeCombineOperation(inFiles, _imgFileChooser.getSelectedFile()));
			// Start new polling thread
			new Thread(new ThreadPoller(this, mkgmapThread)).start();
		}
	}

	/**
	 * Launch the specified operation in a new thread
	 * @param inOperation operation to launch
	 * @return created thread
	 */
	private Thread launchOperation(Operation inOperation)
	{
		_progressBar.setVisible(true);
		_operation = inOperation;
		Thread mkgmapThread = new Thread(new Runnable() {
			public void run() {
				launchOperation();
			}
		});
		mkgmapThread.setContextClassLoader(_classLoader);
		mkgmapThread.start();
		return mkgmapThread;
	}

	/**
	 * Launch the selected operation
	 */
	private void launchOperation()
	{
		_operationFailed = false;
		String[] mainMethodArgs = _operation.getArgs();
		try
		{
			Class<?> mainClass = _classLoader.loadClass("uk.me.parabola.mkgmap.main.Main");
			Method mainMethod = mainClass.getMethod("main", (new String[]{}).getClass());
			// Found main method
			mainMethod.invoke(null, new Object[] {mainMethodArgs});
		}
		catch (Exception e)
		{
			if (e.getCause() != null && e.getCause() instanceof OutOfMemoryError) {
				JOptionPane.showMessageDialog(_mainWindow,
					"Ran out of memory! Try again using the -Xmx=512M parameter to allocate more memory",
					"Out of memory", JOptionPane.ERROR_MESSAGE);
			}
			else {
				JOptionPane.showMessageDialog(_mainWindow,
					e.getClass().getName() + " error occurred" + (e.getMessage() == null?"":"): " + e.getMessage()),
					"Internal error", JOptionPane.ERROR_MESSAGE);
			}
			_operationFailed = true;
		}
	}

	/**
	 * Inform controller that operation is complete
	 */
	public void operationComplete()
	{
		_progressBar.setVisible(false);
		if (!_operationFailed)
		{
			// Rename file using _operation
			if (!_operation.getOutputFile().renameTo(_operation.getRequestedFile())) {
				// Show error if rename failed
				JOptionPane.showMessageDialog(_mainWindow,
					"Failed to rename file. Output file is still at " + _operation.getOutputFile().getAbsolutePath(),
					"Rename failed", JOptionPane.ERROR_MESSAGE);
			}
			_mainWindow.dispose();
		}
	}
}
