package tim.prune.function.estimate;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Font;
import java.awt.GridLayout;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.Calendar;

import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

import tim.prune.App;
import tim.prune.GenericFunction;
import tim.prune.I18nManager;
import tim.prune.cmd.EditSingleFieldCmd;
import tim.prune.config.Config;
import tim.prune.data.DataPoint;
import tim.prune.data.Field;
import tim.prune.data.RangeStatsWithGradients;
import tim.prune.data.Selection;
import tim.prune.data.Unit;
import tim.prune.data.UnitSet;
import tim.prune.data.UnitSetLibrary;
import tim.prune.function.edit.PointEdit;
import tim.prune.gui.DecimalNumberField;
import tim.prune.gui.DisplayUtils;
import tim.prune.gui.GuiGridLayout;
import tim.prune.tips.TipManager;

/**
 * Class to calculate and show the results of estimating (hike) time for the current range
 */
public class EstimateTime extends GenericFunction
{
	/** Dialog */
	private JDialog _dialog = null;
	/** Labels for distances */
	private JLabel _distanceLabel = null;
	/** Labels for durations */
	private JLabel _estimatedDurationLabel = null, _actualDurationLabel = null;
	/** Labels for climbs */
	private JLabel _gentleClimbLabel = null, _steepClimbLabel = null;
	/** Labels for descents */
	private JLabel _gentleDescentLabel = null, _steepDescentLabel = null;
	/** Labels and text fields for parameters */
	private JLabel _flatSpeedLabel = null;
	private DecimalNumberField _flatSpeedField = null;
	private JLabel _climbParamLabel = null;
	private DecimalNumberField _gentleClimbField = null, _steepClimbField = null;
	private JLabel _descentParamLabel = null;
	private DecimalNumberField _gentleDescentField = null, _steepDescentField = null;
	/** Range stats */
	private RangeStatsWithGradients _stats = null;
	private JButton _applyButton = null;


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

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

	/**
	 * Begin the function
	 */
	public void begin()
	{
		// Get the stats on the selection before launching the dialog
		Selection selection = _app.getTrackInfo().getSelection();
		final int altitudeTolerance = getConfig().getConfigInt(Config.KEY_ALTITUDE_TOLERANCE) / 100;
		_stats = new RangeStatsWithGradients(_app.getTrackInfo().getTrack(),
			selection.getStart(), selection.getEnd(), altitudeTolerance);

		if (_stats.getMovingDistance(UnitSetLibrary.UNITS_METRES) < 0.01)
		{
			_app.showErrorMessage(getNameKey(), "dialog.estimatetime.error.nodistance");
			return;
		}
		if (_dialog == null)
		{
			// First time in, check whether params are at default, show tip message if unaltered
			showTip();
			_dialog = new JDialog(_parentFrame, getName(), true);
			_dialog.setLocationRelativeTo(_parentFrame);
			_dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
			_dialog.getContentPane().add(makeDialogComponents());
			_dialog.pack();
		}
		updateDetails();
		_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(5, 5));

		// main panel with a box layout
		JPanel mainPanel = new JPanel();
		mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
		// Label at top
		JLabel introLabel = new JLabel(I18nManager.getText("dialog.fullrangedetails.intro") + ":");
		introLabel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
		introLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
		mainPanel.add(introLabel);
		mainPanel.add(Box.createVerticalStrut(4));

		// Details panel in a grid
		JPanel detailsPanel = new JPanel();
		detailsPanel.setLayout(new GridLayout(0, 4, 6, 2));
		detailsPanel.setBorder(BorderFactory.createTitledBorder(
			I18nManager.getText("dialog.estimatetime.details")));

		// Distance
		JLabel distLabel = new JLabel(I18nManager.getText("fieldname.distance") + ": ");
		distLabel.setHorizontalAlignment(JLabel.RIGHT);
		detailsPanel.add(distLabel);
		_distanceLabel = new JLabel("5 km");
		detailsPanel.add(_distanceLabel);
		detailsPanel.add(new JLabel("")); detailsPanel.add(new JLabel("")); // two blank cells

		detailsPanel.add(new JLabel(""));
		detailsPanel.add(new JLabel(I18nManager.getText("dialog.estimatetime.gentle")));
		detailsPanel.add(new JLabel(I18nManager.getText("dialog.estimatetime.steep")));
		detailsPanel.add(new JLabel("")); // blank cells

		// Climb
		JLabel climbLabel = new JLabel(I18nManager.getText("dialog.estimatetime.climb") + ": ");
		climbLabel.setHorizontalAlignment(JLabel.RIGHT);
		detailsPanel.add(climbLabel);
		_gentleClimbLabel = new JLabel("1500 m");
		detailsPanel.add(_gentleClimbLabel);
		_steepClimbLabel = new JLabel("1500 m");
		detailsPanel.add(_steepClimbLabel);
		detailsPanel.add(new JLabel(""));

		// Descent
		JLabel descentLabel = new JLabel(I18nManager.getText("dialog.estimatetime.descent") + ": ");
		descentLabel.setHorizontalAlignment(JLabel.RIGHT);
		detailsPanel.add(descentLabel);
		_gentleDescentLabel = new JLabel("1500 m");
		detailsPanel.add(_gentleDescentLabel);
		_steepDescentLabel = new JLabel("1500 m");
		detailsPanel.add(_steepDescentLabel);
		detailsPanel.add(new JLabel(""));

		detailsPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
		mainPanel.add(detailsPanel);
		mainPanel.add(Box.createVerticalStrut(4));

		// Parameters panel in a flexible grid
		JPanel paramsPanel = new JPanel();
		GuiGridLayout paramsGrid = new GuiGridLayout(paramsPanel, new double[] {1.5, 0.2, 1.0, 0.2, 0.5},
			new boolean[] {true, false, false, false, false});
		paramsPanel.setBorder(BorderFactory.createTitledBorder(
			I18nManager.getText("dialog.estimatetime.parameters")));
		KeyAdapter paramChangeListener = new KeyAdapter() {
			public void keyTyped(KeyEvent inE) {
				SwingUtilities.invokeLater(() -> calculateEstimatedTime());
			}
			public void keyPressed(KeyEvent inE) {
				if (inE.getKeyCode() == KeyEvent.VK_ESCAPE) {_dialog.dispose();}
			}
		};

		// Flat speed
		_flatSpeedLabel = new JLabel(I18nManager.getText("dialog.estimatetime.parameters.timefor") + ": "); // (filled in later)
		_flatSpeedLabel.setHorizontalAlignment(JLabel.RIGHT);
		paramsGrid.add(_flatSpeedLabel);
		_flatSpeedField = new DecimalNumberField(); // positive only
		_flatSpeedField.addKeyListener(paramChangeListener);
		paramsGrid.add(_flatSpeedField);
		JLabel minsLabel = new JLabel(I18nManager.getText("units.minutes"));
		paramsGrid.add(minsLabel);
		paramsGrid.nextRow();
		// Headers for gentle and steep
		paramsGrid.add(new JLabel(""));
		paramsGrid.add(new JLabel(I18nManager.getText("dialog.estimatetime.gentle")));
		paramsGrid.add(new JLabel("")); // blank cell
		paramsGrid.add(new JLabel(I18nManager.getText("dialog.estimatetime.steep")));
		paramsGrid.nextRow();
		// Gentle climb
		_climbParamLabel = new JLabel(I18nManager.getText("dialog.estimatetime.parameters.timefor") + ": "); // (filled in later)
		_climbParamLabel.setHorizontalAlignment(JLabel.RIGHT);
		paramsGrid.add(_climbParamLabel);
		_gentleClimbField = new DecimalNumberField(true); // negative numbers allowed
		_gentleClimbField.addKeyListener(paramChangeListener);
		paramsGrid.add(_gentleClimbField);
		paramsGrid.add(new JLabel(minsLabel.getText()));
		// Steep climb
		_steepClimbField = new DecimalNumberField(true); // negative numbers allowed
		_steepClimbField.addKeyListener(paramChangeListener);
		paramsGrid.add(_steepClimbField);
		paramsGrid.add(new JLabel(minsLabel.getText()));

		// Gentle descent
		_descentParamLabel = new JLabel(I18nManager.getText("dialog.estimatetime.parameters.timefor") + ": "); // (filled in later)
		_descentParamLabel.setHorizontalAlignment(JLabel.RIGHT);
		paramsGrid.add(_descentParamLabel);
		_gentleDescentField = new DecimalNumberField(true); // negative numbers allowed
		_gentleDescentField.addKeyListener(paramChangeListener);
		paramsGrid.add(_gentleDescentField);
		paramsGrid.add(new JLabel(minsLabel.getText()));
		// Steep climb
		_steepDescentField = new DecimalNumberField(true); // negative numbers allowed
		_steepDescentField.addKeyListener(paramChangeListener);
		paramsGrid.add(_steepDescentField);
		paramsGrid.add(new JLabel(minsLabel.getText()));

		paramsPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
		mainPanel.add(paramsPanel);
		mainPanel.add(Box.createVerticalStrut(12));

		// Results panel
		JPanel resultsPanel = new JPanel();
		resultsPanel.setBorder(BorderFactory.createTitledBorder(
			I18nManager.getText("dialog.estimatetime.results")));
		resultsPanel.setLayout(new GridLayout(0, 2, 3, 3));
		// estimated time
		_estimatedDurationLabel = new JLabel(I18nManager.getText("dialog.estimatetime.results.estimatedtime") + ": "); // filled in later
		Font origFont = _estimatedDurationLabel.getFont();
		_estimatedDurationLabel.setFont(origFont.deriveFont(Font.BOLD, origFont.getSize2D() + 2.0f));

		resultsPanel.add(_estimatedDurationLabel);
		// actual time (if available)
		_actualDurationLabel = new JLabel(I18nManager.getText("dialog.estimatetime.results.actualtime") + ": "); // filled in later
		resultsPanel.add(_actualDurationLabel);

		resultsPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
		mainPanel.add(resultsPanel);
		mainPanel.add(Box.createVerticalStrut(4));

		dialogPanel.add(mainPanel, BorderLayout.NORTH);
		// button panel at bottom
		JPanel buttonPanel = new JPanel();
		buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS));
		buttonPanel.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
		_applyButton = new JButton(I18nManager.getText("button.apply"));
		_applyButton.addActionListener(e -> applyTimestampsToRange());
		buttonPanel.add(_applyButton);
		buttonPanel.add(Box.createGlue());
		// Close (and save parameters)
		JButton closeButton = new JButton(I18nManager.getText("button.close"));
		closeButton.addActionListener(e -> finishDialog());
		closeButton.addKeyListener(new KeyAdapter() {
			public void keyPressed(KeyEvent inE) {
				if (inE.getKeyCode() == KeyEvent.VK_ESCAPE) {_dialog.dispose();}
			}
		});
		buttonPanel.add(closeButton);
		// cancel
		JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
		cancelButton.addActionListener(e -> _dialog.dispose());
		buttonPanel.add(cancelButton);
		dialogPanel.add(buttonPanel, BorderLayout.SOUTH);
		return dialogPanel;
	}


	/**
	 * Recalculate the values and update the labels
	 */
	private void updateDetails()
	{
		// Warn if the current track hasn't got any height information
		if (!_stats.getMovingAltitudeRange().hasRange()) {
			_app.showErrorMessage(getNameKey(), "dialog.estimatetime.error.noaltitudes");
		}

		// Distance in current units
		final Unit distUnit = getConfig().getUnitSet().getDistanceUnit();
		final String distUnitsStr = I18nManager.getText(distUnit.getShortnameKey());
		final double movingDist = _stats.getMovingDistance(distUnit);
		_distanceLabel.setText(DisplayUtils.roundedNumber(movingDist) + " " + distUnitsStr);

		// Climb and descent values
		final Unit altUnit = getConfig().getUnitSet().getAltitudeUnit();
		final String altUnitsStr = " " + I18nManager.getText(altUnit.getShortnameKey());
		_gentleClimbLabel.setText(_stats.getGentleAltitudeRange().getClimb(altUnit) + altUnitsStr);
		_steepClimbLabel.setText(_stats.getSteepAltitudeRange().getClimb(altUnit) + altUnitsStr);
		_gentleDescentLabel.setText(_stats.getGentleAltitudeRange().getDescent(altUnit) + altUnitsStr);
		_steepDescentLabel.setText(_stats.getSteepAltitudeRange().getDescent(altUnit) + altUnitsStr);

		// Try to get parameters from config
		EstimationParameters estParams = EstimationParameters.fromConfigString(
			getConfig().getConfigString(Config.KEY_ESTIMATION_PARAMS));
		if (estParams == null) {
			estParams = EstimationParameters.DEFAULT_PARAMS;
		}

		// Flat time is either for 5 km, 3 miles or 3 nautical miles
		_flatSpeedLabel.setText(I18nManager.getText("dialog.estimatetime.parameters.timefor") +
			" " + EstimationParameters.getStandardDistance(distUnit) + ": ");
		_flatSpeedField.setValue(estParams.getFlatMinutesLocal(distUnit));

		final String heightString = " " + EstimationParameters.getStandardClimb(altUnit) + ": ";
		_climbParamLabel.setText(I18nManager.getText("dialog.estimatetime.climb") + heightString);
		_gentleClimbField.setValue(estParams.getGentleClimbMinutesLocal(altUnit));
		_steepClimbField.setValue(estParams.getSteepClimbMinutesLocal(altUnit));
		_descentParamLabel.setText(I18nManager.getText("dialog.estimatetime.descent") + heightString);
		_gentleDescentField.setValue(estParams.getGentleDescentMinutesLocal(altUnit));
		_steepDescentField.setValue(estParams.getSteepDescentMinutesLocal(altUnit));

		// Use the entered parameters to estimate the time
		calculateEstimatedTime();

		// Get the actual time if available, for comparison
		if (_stats.getMovingDurationInSeconds() > 0)
		{
			_actualDurationLabel.setText(I18nManager.getText("dialog.estimatetime.results.actualtime") + ": "
				+ DisplayUtils.buildDurationString(_stats.getMovingDurationInSeconds()));
		}
		else {
			_actualDurationLabel.setText("");
		}
	}


	/**
	 * Use the current parameter and the range stats to calculate the estimated time
	 * and populate the answer in the dialog
	 */
	private void calculateEstimatedTime()
	{
		// Populate an EstimationParameters object from the four strings
		EstimationParameters params = EstimationParameters.fromLocalUnits(getConfig().getUnitSet(),
			_flatSpeedField.getValue(),
			_gentleClimbField.getValue(), _steepClimbField.getValue(),
			_gentleDescentField.getValue(), _steepDescentField.getValue());
		final long numSeconds = (long) (params.applyToStats(_stats) * 60.0);
		_estimatedDurationLabel.setText(I18nManager.getText("dialog.estimatetime.results.estimatedtime") + ": "
			+ DisplayUtils.buildDurationString(numSeconds));
		_applyButton.setEnabled(numSeconds > 0L);
	}


	/**
	 * Finish with the dialog, by pressing the "Close" button
	 */
	private void finishDialog()
	{
		// Make estimation parameters from entered values, if valid save to config
		EstimationParameters params = EstimationParameters.fromLocalUnits(getConfig().getUnitSet(),
			_flatSpeedField.getValue(),
			_gentleClimbField.getValue(), _steepClimbField.getValue(),
			_gentleDescentField.getValue(), _steepDescentField.getValue());
		if (params != null) {
			getConfig().setConfigString(Config.KEY_ESTIMATION_PARAMS, params.toConfigString());
		}
		_dialog.dispose();
	}

	/**
	 * Show a tip to use the learn function, if appropriate
	 */
	private void showTip()
	{
		EstimationParameters currParams = EstimationParameters.fromConfigString(
			getConfig().getConfigString(Config.KEY_ESTIMATION_PARAMS));
		if (currParams == null || currParams.sameAsDefaults()) {
			_app.showTip(TipManager.Tip_LearnTimeParams);
		}
	}

	/**
	 * Use the current parameters to calculate and overwrite the timestamps
	 * for all the track points in the current range (leave waypoints untouched)
	 */
	private void applyTimestampsToRange()
	{
		// Populate an EstimationParameters object from the five values
		UnitSet unitSet = getConfig().getUnitSet();
		EstimationParameters params = EstimationParameters.fromLocalUnits(unitSet,
			_flatSpeedField.getValue(), _gentleClimbField.getValue(),
			_steepClimbField.getValue(), _gentleDescentField.getValue(), _steepDescentField.getValue());

		// Make list of modified timestamps
		ArrayList<PointEdit> edits = new ArrayList<>();
		long startMillis = getTimeAtMidnight();
		final int altitudeTolerance = getConfig().getConfigInt(Config.KEY_ALTITUDE_TOLERANCE) / 100;
		RangeStatsWithGradients stats = new RangeStatsWithGradients(altitudeTolerance);
		Selection selection = _app.getTrackInfo().getSelection();
		for (int pointIdx=selection.getStart(); pointIdx <= selection.getEnd(); pointIdx++)
		{
			DataPoint point = _app.getTrackInfo().getTrack().getPoint(pointIdx);
			if (!point.isWaypoint())
			{
				stats.addPoint(point);
				final double numSeconds = params.applyToStats(stats) * 60.0;
				final long pointMillis = startMillis + (long) (numSeconds * 1000.0);
				edits.add(new PointEdit(pointIdx, "" + pointMillis));
			}
		}
		if (!edits.isEmpty())
		{
			EditSingleFieldCmd command = new EditSingleFieldCmd(Field.TIMESTAMP, edits, null);
			command.setDescription(I18nManager.getText("undo.applytimestamps"));
			command.setConfirmText(I18nManager.getText("confirm.applytimestamps"));
			_app.execute(command);
		}
	}

	/**
	 * @return timestamp at midnight, beginning of today
	 */
	private static long getTimeAtMidnight()
	{
		Calendar startTime = Calendar.getInstance();
		startTime.set(Calendar.HOUR_OF_DAY, 0);
		startTime.set(Calendar.MINUTE, 0);
		startTime.set(Calendar.SECOND, 0);
		startTime.set(Calendar.MILLISECOND, 0);
		return startTime.getTimeInMillis();
	}
}
