package tim.prune.save;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;
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 tim.prune.App;
import tim.prune.GenericFunction;
import tim.prune.I18nManager;
import tim.prune.config.ColourScheme;
import tim.prune.config.Config;
import tim.prune.data.DataPoint;
import tim.prune.data.DoubleRange;
import tim.prune.data.Track;
import tim.prune.gui.BaseImageDefinitionPanel;
import tim.prune.gui.GuiGridLayout;
import tim.prune.gui.WholeNumberField;
import tim.prune.gui.colour.PointColourer;
import tim.prune.gui.map.MapSource;
import tim.prune.gui.map.MapSourceLibrary;
import tim.prune.gui.map.MapUtils;
import tim.prune.gui.map.WpIconDefinition;
import tim.prune.gui.map.WpIconLibrary;
import tim.prune.load.GenericFileFilter;
import tim.prune.threedee.ImageDefinition;

/**
 * Class to handle the exporting of map images, optionally with track data drawn on top.
 * This allows images larger than the screen to be generated.
 */
public class ImageExporter extends GenericFunction implements BaseImageConsumer
{
	private JDialog   _dialog = null;
	private JCheckBox _drawDataCheckbox = null;
	private JCheckBox _drawTrackPointsCheckbox = null;
	private WholeNumberField _textScaleField = null;
	private BaseImageDefinitionPanel _baseImagePanel = null;
	private JFileChooser _fileChooser = null;
	private JButton   _okButton = null;

	/**
	 * Constructor
	 * @param inApp App object
	 */
	public ImageExporter(App inApp) {
		super(inApp);
	}

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

	/**
	 * Begin the function by showing the input dialog
	 */
	public void begin()
	{
		// Make dialog window
		if (_dialog == null)
		{
			_dialog = new JDialog(_parentFrame, getName(), true);
			_dialog.setLocationRelativeTo(_parentFrame);
			_dialog.getContentPane().add(makeDialogComponents());
			_dialog.pack();
			_textScaleField.setValue(100);
		}

		// Check if there is a cache to use
		if (!_baseImagePanel.isImagePossible())
		{
			_app.showErrorMessage(getNameKey(), "dialog.exportimage.noimagepossible");
			return;
		}

		_baseImagePanel.updateBaseImageDetails();
		baseImageChanged();
		// Show dialog
		_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(4, 4));
		// Checkbox for drawing track or not
		_drawDataCheckbox = new JCheckBox(I18nManager.getText("dialog.exportimage.drawtrack"));
		_drawDataCheckbox.setSelected(true); // draw by default
		// Also whether to draw track points or not
		_drawTrackPointsCheckbox = new JCheckBox(I18nManager.getText("dialog.exportimage.drawtrackpoints"));
		_drawTrackPointsCheckbox.setSelected(true);
		// Add listener to en/disable trackpoints checkbox
		_drawDataCheckbox.addActionListener(e ->
			_drawTrackPointsCheckbox.setEnabled(_drawDataCheckbox.isSelected()));

		// TODO: Maybe have other controls such as line width, symbol scale factor
		JPanel controlsPanel = new JPanel();
		GuiGridLayout grid = new GuiGridLayout(controlsPanel);
		grid.add(new JLabel(I18nManager.getText("dialog.exportimage.textscalepercent") + ": "));
		_textScaleField = new WholeNumberField(3);
		_textScaleField.setText("888");
		grid.add(_textScaleField);

		// OK, Cancel buttons
		JPanel buttonPanel = new JPanel();
		buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
		_okButton = new JButton(I18nManager.getText("button.ok"));
		_okButton.addActionListener(e -> {
			doExport();
			_baseImagePanel.getGrouter().clearMapImage();
			_dialog.dispose();
		});
		buttonPanel.add(_okButton);
		JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
		cancelButton.addActionListener(e -> {
			_baseImagePanel.getGrouter().clearMapImage();
			_dialog.dispose();
		});
		buttonPanel.add(cancelButton);
		panel.add(buttonPanel, BorderLayout.SOUTH);

		// Listener to close dialog if escape pressed
		KeyAdapter closer = new KeyAdapter() {
			public void keyReleased(KeyEvent e)
			{
				if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
					_dialog.dispose();
					_baseImagePanel.getGrouter().clearMapImage();
				}
			}
		};
		_drawDataCheckbox.addKeyListener(closer);

		// Panel for the base image
		_baseImagePanel = new BaseImageDefinitionPanel(this, _dialog, _app.getTrackInfo().getTrack(), getConfig());
		_baseImagePanel.setImageDefaultYes();

		// Panel for the checkboxes at the top
		JPanel checkPanel = new JPanel();
		checkPanel.setLayout(new BoxLayout(checkPanel, BoxLayout.Y_AXIS));
		checkPanel.add(_drawDataCheckbox);
		checkPanel.add(_drawTrackPointsCheckbox);

		// add these panels to the holder panel
		JPanel holderPanel = new JPanel();
		holderPanel.setLayout(new BorderLayout(5, 5));
		holderPanel.add(checkPanel, BorderLayout.NORTH);
		holderPanel.add(controlsPanel, BorderLayout.CENTER);
		holderPanel.add(_baseImagePanel, BorderLayout.SOUTH);
		holderPanel.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));

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


	/**
	 * Select the file and export data to it
	 */
	private void doExport()
	{
		_okButton.setEnabled(false);
		// OK pressed, so choose output file
		if (_fileChooser == null)
		{
			_fileChooser = new JFileChooser();
			_fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
			_fileChooser.setFileFilter(new GenericFileFilter("filetypefilter.png", new String[] {"png"}));
			_fileChooser.setAcceptAllFileFilterUsed(false);
			// start from directory in config which should be set
			final String configDir = getConfig().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 pngFile = _fileChooser.getSelectedFile();
				if (!pngFile.getName().toLowerCase().endsWith(".png"))
				{
					pngFile = new File(pngFile.getAbsolutePath() + ".png");
				}
				// Check if file exists and if necessary prompt for overwrite
				Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
				if (!pngFile.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(pngFile))
					{
						// 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 inPngFile File object to save to
	 * @return true if successful
	 */
	private boolean exportFile(File inPngFile)
	{
		// Get the image file from the grouter
		ImageDefinition imageDef = _baseImagePanel.getImageDefinition();
		MapSource source = MapSourceLibrary.getSource(imageDef.getSourceIndex());
		MapGrouter grouter = _baseImagePanel.getGrouter();
		GroutedImage baseImage = grouter.getMapImage(_app.getTrackInfo().getTrack(), source,
			imageDef.getZoom(), getConfig());
		if (baseImage == null || !baseImage.isValid())
		{
			_app.showErrorMessage(getNameKey(), "dialog.exportpov.cannotmakebaseimage");
			return true;
		}
		try
		{
			if (_drawDataCheckbox.isSelected())
			{
				// Draw the track on top of this image
				drawData(baseImage);
			}
			// Write composite image to file
			if (!ImageIO.write(baseImage.getImage(), "png", inPngFile)) {
				_app.showErrorMessage(getNameKey(), "dialog.exportpov.cannotmakebaseimage");
				return false; // choose again - the image creation worked but the save failed
			}
		}
		catch (IOException ioe) {
			System.err.println("Can't write image: " + ioe.getClass().getName());
		}
		return true;
	}

	/**
	 * Draw the track and waypoint data from the current Track onto the given image
	 * @param inImage GroutedImage from map tiles
	 */
	private void drawData(GroutedImage inImage)
	{
		// Work out x, y limits for drawing
		DoubleRange xRange = inImage.getXRange();
		DoubleRange yRange = inImage.getYRange();
		final int zoomFactor = 1 << _baseImagePanel.getImageDefinition().getZoom();
		Graphics g = inImage.getImage().getGraphics();
		// TODO: Set line width, style etc
		final PointColourer pointColourer = _app.getPointColourer();
		final Color defaultPointColour = getConfig().getColourScheme().getColour(ColourScheme.IDX_POINT);
		g.setColor(defaultPointColour);

		// Loop to draw all track points
		final Track track = _app.getTrackInfo().getTrack();
		final int numPoints = track.getNumPoints();
		int prevX = 0, prevY = 0;
		boolean gotPreviousPoint = false;
		for (int i=0; i<numPoints; i++)
		{
			DataPoint point = track.getPoint(i);
			if (!point.isWaypoint())
			{
				// Determine what colour to use to draw the track point
				if (pointColourer != null)
				{
					Color c = pointColourer.getColour(i);
					g.setColor(c == null ? defaultPointColour : c);
				}
				double x = track.getX(i) - xRange.getMinimum();
				double y = track.getY(i) - yRange.getMinimum();
				// use zoom level to calculate pixel coords on image
				int px = (int) (x * zoomFactor * 256), py = (int) (y * zoomFactor * 256);
				if (!point.getSegmentStart() && gotPreviousPoint) {
					// draw from previous point to this one
					g.drawLine(prevX, prevY, px, py);
				}
				// Only draw points if requested
				if (_drawTrackPointsCheckbox.isSelected())
				{
					g.drawRect(px-2, py-2, 3, 3);
				}
				// save coordinates
				prevX = px; prevY = py;
				gotPreviousPoint = true;
			}
		}

		// Now the waypoints
		final Color textColour = getConfig().getColourScheme().getColour(ColourScheme.IDX_TEXT);
		g.setColor(textColour);
		WpIconDefinition wpIconDefinition = null;
		final int wpType = getConfig().getConfigInt(Config.KEY_WAYPOINT_ICONS);
		if (wpType != WpIconLibrary.WAYPT_DEFAULT) {
			wpIconDefinition = WpIconLibrary.getFixedIconDefinition(wpType, getIconManager());
		}
		// Loop again to draw waypoints
		for (int i=0; i<numPoints; i++)
		{
			DataPoint point = track.getPoint(i);
			if (point.isWaypoint())
			{
				// use zoom level to calculate pixel coords on image
				double x = track.getX(i) - xRange.getMinimum();
				double y = track.getY(i) - yRange.getMinimum();
				int px = (int) (x * zoomFactor * 256), py = (int) (y * zoomFactor * 256);
				// Fill Rect or draw icon image?
				g.fillRect(px-3, py-3, 6, 6);
				if (wpIconDefinition == null) {
					g.fillRect(px-3, py-3, 6, 6);
				}
				else
				{
					g.drawImage(wpIconDefinition.getImageIcon().getImage(), px-wpIconDefinition.getXOffset(),
						py-wpIconDefinition.getYOffset(), null);
				}
			}
		}
		// Set text size according to input
		int fontScalePercent = _textScaleField.getValue();
		if (fontScalePercent > 0 && fontScalePercent <= 999)
		{
			Font gFont = g.getFont();
			g.setFont(gFont.deriveFont((float) (gFont.getSize() * 0.01 * fontScalePercent)));
		}
		FontMetrics fm = g.getFontMetrics();
		final int nameHeight = fm.getHeight();
		final int imageSize = inImage.getImageSize();

		// Loop over points again, draw photo points
		final Color photoColour = getConfig().getColourScheme().getColour(ColourScheme.IDX_SECONDARY);
		g.setColor(photoColour);
		for (int i=0; i<numPoints; i++)
		{
			DataPoint point = track.getPoint(i);
			if (point.hasMedia())
			{
				// draw blob for each photo
				double x = track.getX(i) - xRange.getMinimum();
				double y = track.getY(i) - yRange.getMinimum();
				// use zoom level to calculate pixel coords on image
				int px = (int) (x * zoomFactor * 256), py = (int) (y * zoomFactor * 256);
				g.fillRect(px-3, py-3, 6, 6);
			}
		}

		// Loop over points again, now draw names for waypoints
		g.setColor(textColour);
		for (int i=0; i<numPoints; i++)
		{
			DataPoint point = track.getPoint(i);
			if (point.isWaypoint() && fontScalePercent > 0)
			{
				double x = track.getX(i) - xRange.getMinimum();
				double y = track.getY(i) - yRange.getMinimum();
				int px = (int) (x * zoomFactor * 256), py = (int) (y * zoomFactor * 256);

				// Figure out where to draw waypoint name so it doesn't obscure track
				String waypointName = point.getWaypointName();
				int nameWidth = fm.stringWidth(waypointName);
				boolean drawnName = false;
				// Make arrays for coordinates right left up down
				int[] nameXs = {px + 2, px - nameWidth - 2, px - nameWidth/2, px - nameWidth/2};
				int[] nameYs = {py + (nameHeight/2), py + (nameHeight/2), py - 2, py + nameHeight + 2};
				for (int extraSpace = 4; extraSpace < 13 && !drawnName; extraSpace+=2)
				{
					// Shift arrays for coordinates right left up down
					nameXs[0] += 2; nameXs[1] -= 2;
					nameYs[2] -= 2; nameYs[3] += 2;
					// Check each direction in turn right left up down
					for (int a=0; a<4; a++)
					{
						if (nameXs[a] > 0 && (nameXs[a] + nameWidth) < imageSize
							&& nameYs[a] < imageSize && (nameYs[a] - nameHeight) > 0
							&& !MapUtils.overlapsPoints(inImage.getImage(), nameXs[a], nameYs[a],
								nameWidth, nameHeight, textColour))
						{
							// Found a rectangle to fit - draw name here and quit
							g.drawString(waypointName, nameXs[a], nameYs[a]);
							drawnName = true;
							break;
						}
					}
				}
			}
		}

		// Maybe draw note at the bottom, export from GpsPrune?  Filename?
		// Note: Differences from main map: No mapPosition (modifying position and visible points),
		//       no selection, no opacities, maybe different scale/text factors
	}

	/**
	 * Base image has changed, need to enable/disable ok button
	 */
	public void baseImageChanged()
	{
		final boolean useImage = _baseImagePanel.getImageDefinition().getUseImage();
		final int zoomLevel = _baseImagePanel.getImageDefinition().getZoom();
		final boolean okEnabled = useImage && _baseImagePanel.getFoundData()
			&& ImageSizeLimits.isZoomLevelOk(_app.getTrackInfo().getTrack(), zoomLevel);
		_okButton.setEnabled(okEnabled);
	}
}
