package tim.prune.function.weather;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.io.InputStream;

import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingUtilities;
import javax.swing.table.TableCellRenderer;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.SAXException;

import tim.prune.App;
import tim.prune.GenericFunction;
import tim.prune.I18nManager;
import tim.prune.data.DataPoint;
import tim.prune.data.Track;
import tim.prune.function.browser.BrowserLauncher;

/**
 * Function to display a weather forecast for the current location
 * using the services of openweathermap.org
 */
public class GetWeatherForecastFunction extends GenericFunction
{
	/** Dialog object */
	private JDialog _dialog = null;
	/** Label for location */
	private JLabel _locationLabel = null;
	/** Label for the forecast update time */
	private JLabel _updateTimeLabel = null;
	/** Label for the sunrise and sunset times */
	private JLabel _sunriseLabel = null;
	/** Radio button for selecting current weather */
	private JRadioButton _currentForecastRadio = null;
	/** Radio button for selecting daily forecasts */
	private JRadioButton _dailyForecastRadio = null;
	/** Dropdown for selecting celsius / fahrenheit */
	private JComboBox<String> _tempUnitsDropdown = null;
	/** Table to hold the forecasts */
	private JTable _forecastsTable = null;
	/** Table model */
	private final WeatherTableModel _tableModel = new WeatherTableModel();
	/** Set of previously obtained results, to avoid repeating calls */
	private final ResultSet _resultSet = new ResultSet();
	/** Location id obtained from current forecast */
	private String _locationId = null;
	/** Access to xml streams */
	private StreamProvider _streamProvider;
	/** Flag to show that forecast is currently running, don't start another */
	private boolean _isRunning = false;


	/** Constructor */
	public GetWeatherForecastFunction(App inApp)
	{
		super(inApp);
		_streamProvider = new OnlineStreamProvider();
	}

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

	/**
	 * Begin the function
	 */
	public void begin()
	{
		// Initialise dialog, show empty list
		if (_dialog == null)
		{
			_dialog = new JDialog(_parentFrame, getName(), true);
			_dialog.setLocationRelativeTo(_parentFrame);
			_dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
			_dialog.getContentPane().add(makeDialogComponents());
			_dialog.pack();
		}
		// Clear results
		_locationId = null;
		_tableModel.clear();
		_locationLabel.setText(I18nManager.getText("confirm.running"));
		_updateTimeLabel.setText("");
		_sunriseLabel.setText("");
		_currentForecastRadio.setSelected(true);

		// Start new thread to load list asynchronously
		new Thread(this::run).start();

		_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(0, 4));

		JPanel topPanel = new JPanel();
		topPanel.setLayout(new BoxLayout(topPanel, BoxLayout.Y_AXIS));
		_locationLabel = new JLabel(I18nManager.getText("confirm.running"));
		_locationLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
		topPanel.add(_locationLabel);
		_updateTimeLabel = new JLabel(" ");
		_updateTimeLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
		topPanel.add(_updateTimeLabel);
		_sunriseLabel = new JLabel(" ");
		_sunriseLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
		topPanel.add(_sunriseLabel);
		JPanel radioPanel = new JPanel();
		radioPanel.setLayout(new BoxLayout(radioPanel, BoxLayout.X_AXIS));
		radioPanel.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
		ButtonGroup forecastTypeGroup = new ButtonGroup();
		_currentForecastRadio = new JRadioButton(I18nManager.getText("dialog.weather.currentforecast"));
		_dailyForecastRadio = new JRadioButton(I18nManager.getText("dialog.weather.dailyforecast"));
		JRadioButton threeHourlyRadio = new JRadioButton(I18nManager.getText("dialog.weather.3hourlyforecast"));
		forecastTypeGroup.add(_currentForecastRadio);
		forecastTypeGroup.add(_dailyForecastRadio);
		forecastTypeGroup.add(threeHourlyRadio);
		radioPanel.add(_currentForecastRadio);
		radioPanel.add(Box.createHorizontalStrut(8));
		radioPanel.add(_dailyForecastRadio);
		radioPanel.add(Box.createHorizontalStrut(8));
		radioPanel.add(threeHourlyRadio);
		_currentForecastRadio.setSelected(true);
		ActionListener radioListener = e -> {
			if (!_isRunning) {
				new Thread(this::run).start();
			}
		};
		_currentForecastRadio.addActionListener(radioListener);
		_dailyForecastRadio.addActionListener(radioListener);
		threeHourlyRadio.addActionListener(radioListener);
		radioPanel.add(Box.createHorizontalGlue());
		radioPanel.add(Box.createHorizontalStrut(40));

		// Dropdown for temperature units
		radioPanel.add(new JLabel(I18nManager.getText("dialog.weather.temperatureunits") + ": "));
		_tempUnitsDropdown = new JComboBox<>(new String[] {
			I18nManager.getText("units.degreescelsius"), I18nManager.getText("units.degreesfahrenheit")
		});
		_tempUnitsDropdown.setMaximumSize(_tempUnitsDropdown.getPreferredSize());
		_tempUnitsDropdown.addActionListener(radioListener);
		radioPanel.add(_tempUnitsDropdown);
		radioPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
		topPanel.add(radioPanel);
		dialogPanel.add(topPanel, BorderLayout.NORTH);

		final IconRenderer iconRenderer = new IconRenderer(getIconManager());
		_forecastsTable = new JTable(_tableModel)
		{
			public TableCellRenderer getCellRenderer(int row, int column) {
				if ((row == WeatherTableModel.ROW_ICON)) {
					return iconRenderer;
				}
				return super.getCellRenderer(row, column);
			}
		};
		_forecastsTable.setRowSelectionAllowed(false);
		_forecastsTable.setRowHeight(2, 55); // make just that row high enough to see icons
		_forecastsTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
		_forecastsTable.getTableHeader().setReorderingAllowed(false);
		_forecastsTable.setShowHorizontalLines(false);

		JScrollPane scroller = new JScrollPane(_forecastsTable);
		scroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
		scroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER);
		scroller.setPreferredSize(new Dimension(500, 210));
		scroller.getViewport().setBackground(Color.white);

		dialogPanel.add(scroller, BorderLayout.CENTER);

		// button panel at bottom
		JPanel buttonPanel = new JPanel();
		buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
		JButton launchButton = new JButton(I18nManager.getText("button.showwebpage"));
		launchButton.addActionListener(e -> {
			BrowserLauncher.launchBrowser("http://openweathermap.org/city/" + (_locationId == null ? "" : _locationId));
		});
		buttonPanel.add(launchButton);
		// close
		JButton closeButton = new JButton(I18nManager.getText("button.close"));
		closeButton.addActionListener(e -> _dialog.dispose());
		buttonPanel.add(closeButton);
		// Add a holder panel with a static label to credit openweathermap
		JPanel southPanel = new JPanel();
		southPanel.setLayout(new BoxLayout(southPanel, BoxLayout.Y_AXIS));
		southPanel.add(new JLabel(I18nManager.getText("dialog.weather.creditnotice")));
		southPanel.add(buttonPanel);
		dialogPanel.add(southPanel, BorderLayout.SOUTH);
		dialogPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 15));
		return dialogPanel;
	}

	/**
	 * Get the weather forecast in a separate thread
	 */
	public void run()
	{
		if (_isRunning) {return;} // don't run twice
		_isRunning = true;

		// Are we getting the current details, or getting a forecast?
		final boolean isCurrent = _locationId == null || _currentForecastRadio.isSelected();
		final boolean isDailyForecast = _dailyForecastRadio.isSelected() && !isCurrent;
		final boolean isHourlyForecast = !isCurrent && !isDailyForecast;
		final boolean isUsingCelsius  = _tempUnitsDropdown.getSelectedIndex() == 0;

		// Have we got these results already?  Look in store
		WeatherResults results = _resultSet.getWeather(_locationId, isCurrent, isDailyForecast, isHourlyForecast, isUsingCelsius);
		if (results == null)
		{
			if (isCurrent)
			{
				// Get the current details using either lat/long or locationId
				results = getCurrentWeather(isUsingCelsius);
				// If the current radio isn't selected, select it
				if (!_currentForecastRadio.isSelected()) {
					_currentForecastRadio.setSelected(true);
				}
			}
			else
			{
				// Get the specified forecast using the retrieved locationId
				results = getWeatherForecast(isDailyForecast, isUsingCelsius);
			}
			// If it's a valid answer, store it for later
			if (results != null)
			{
				_resultSet.setWeather(results, _locationId, isCurrent, isDailyForecast, isHourlyForecast, isUsingCelsius);
			}
		}

		// update table contents and labels
		if (results != null)
		{
			final WeatherResults finalResults = results;
			SwingUtilities.invokeLater(() -> {
				_tableModel.setResults(finalResults);
				adjustTable();
			});
			_locationLabel.setText(I18nManager.getText("dialog.weather.location") + ": " + results.getLocationName());
			final String ut = results.getUpdateTime();
			_updateTimeLabel.setText(I18nManager.getText("dialog.weather.update") + ": " + (ut == null ? "" : ut));
			if (results.getSunriseTime() != null && results.getSunsetTime() != null)
			{
				_sunriseLabel.setText(I18nManager.getText("dialog.weather.sunrise") + ": " + results.getSunriseTime()
					+ ", " + I18nManager.getText("dialog.weather.sunset") + ": " + results.getSunsetTime());
			}
			else {
				_sunriseLabel.setText("");
			}
		}

		// finished running
		_isRunning = false;
	}


	/**
	 * Adjust the column widths and row heights to fit the displayed data
	 */
	private void adjustTable()
	{
		if (!_tableModel.isEmpty())
		{
			// adjust column widths for all columns
			for (int i=0; i<_forecastsTable.getColumnCount(); i++)
			{
				double maxWidth = 0.0;
				for (int j=0; j<_forecastsTable.getRowCount(); j++)
				{
					final String value = _tableModel.getValueAt(j, i).toString();
					maxWidth = Math.max(maxWidth, _forecastsTable.getCellRenderer(0, 0).getTableCellRendererComponent(
						_forecastsTable, value, false, false, 0, 0).getPreferredSize().getWidth());
				}
				_forecastsTable.getColumnModel().getColumn(i).setMinWidth((int) maxWidth + 2);
			}
			// Set minimum row heights
			final int labelHeight = (int) (_forecastsTable.getCellRenderer(0, 0).getTableCellRendererComponent(
				_forecastsTable, "M", false, false, 0, 0).getMinimumSize().getHeight() * 1.2f + 4);
			for (int i=0; i<_forecastsTable.getRowCount(); i++)
			{
				if (i == WeatherTableModel.ROW_ICON) {
					_forecastsTable.setRowHeight(i, 55);
				}
				else {
					_forecastsTable.setRowHeight(i, labelHeight);
				}
			}
		}
	}

	/**
	 * Get the current weather using the lat/long and return the results
	 * @param inUseCelsius true for celsius, false for fahrenheit
	 * @return weather results
	 */
	WeatherResults getCurrentWeather(boolean inUseCelsius)
	{
		final Track track = _app.getTrackInfo().getTrack();
		if (track.getNumPoints() < 1) {return null;}
		// Get coordinates to lookup
		final double lat, lon;
		// See if a point is selected, if so use that
		DataPoint currPoint = _app.getTrackInfo().getCurrentPoint();
		if (currPoint != null)
		{
			// Use selected point
			lat = currPoint.getLatitude().getDouble();
			lon = currPoint.getLongitude().getDouble();
		}
		else
		{
			lat = track.getLatRange().getMidValue();
			lon = track.getLonRange().getMidValue();
		}

		// Create a handler to parse the returned XML
		OWMCurrentHandler xmlHandler = new OWMCurrentHandler();
		try (InputStream inStream = _streamProvider.getCurrentWeatherStream(lat, lon, _locationId, inUseCelsius))
		{
			SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser();
			saxParser.parse(inStream, xmlHandler);
		}
		catch (IOException | SAXException | ParserConfigurationException e)
		{
			// Show error message but don't close dialog
			_app.showErrorMessageNoLookup(getNameKey(), e.getClass().getName() + " - " + e.getMessage());
			_isRunning = false;
			return null;
		}

		// Save the location id
		if (xmlHandler.getLocationId() != null) {
			_locationId = xmlHandler.getLocationId();
		}
		// Get the results from the handler and return
		WeatherResults results = new WeatherResults();
		results.setForecast(xmlHandler.getCurrentWeather());
		results.setLocationName(xmlHandler.getLocationName());
		results.setUpdateTime(xmlHandler.getUpdateTime());
		results.setSunriseSunsetTimes(xmlHandler.getSunriseTime(), xmlHandler.getSunsetTime());
		results.setTempsCelsius(inUseCelsius);
		return results;
	}


	/**
	 * Get the weather forecast for the current location id and populate in _results
	 * @param inDaily true for daily, false for 3-hourly
	 * @param inCelsius true for celsius, false for fahrenheit
	 * @return weather results
	 */
	WeatherResults getWeatherForecast(boolean inDaily, boolean inCelsius)
	{
		if (_locationId == null) {
			return null;
		}

		// Create a handler to parse the returned XML
		OWMForecastHandler xmlHandler = new OWMForecastHandler();
		try (InputStream inStream = _streamProvider.getForecastStream(_locationId, inDaily, inCelsius))
		{
			SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser();
			saxParser.parse(inStream, xmlHandler);
		}
		catch (Exception e)
		{
			// Show error message but don't close dialog
			_app.showErrorMessageNoLookup(getNameKey(), e.getClass().getName() + " - " + e.getMessage());
			_isRunning = false;
			return null;
		}

		// Get results from handler, put in model
		WeatherResults results = new WeatherResults();
		results.setForecasts(xmlHandler.getForecasts());
		results.setLocationName(xmlHandler.getLocationName());
		results.setUpdateTime(xmlHandler.getUpdateTime());
		results.setTempsCelsius(inCelsius);
		return results;
	}

	/** Used for testing */
	void setStreamProvider(StreamProvider inProvider) {
		_streamProvider = inProvider;
	}
}
