/*
 * This file is a part of EpnTAPClient.
 * This program aims to provide EPN-TAP support for software clients, like CASSIS spectrum analyzer.
 * See draft specifications: https://voparis-confluence.obspm.fr/pages/viewpage.action?pageId=559861
 * Copyright (C) 2016 Institut de Recherche en Astrophysique et Planétologie.
 *
 * This program is free software: you can
 * redistribute it and/or modify it under the terms of the GNU General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or (at your option) any later
 * version. This program is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
 * PURPOSE. See the GNU General Public License for more details. You should have received a copy of
 * the GNU General Public License along with this program. If not, see
 * <http://www.gnu.org/licenses/>.
 */

package eu.omp.irap.vespa.epntapclient.gui;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.BoxLayout;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;

import com.google.gson.JsonArray;
import com.google.gson.JsonObject;

import eu.omp.irap.vespa.votable.utils.CantSendQueryException;
import eu.omp.irap.vespa.votable.utils.Network;

/**
 * A field used to set a service parameter to build the query (in the parameter panel). ParamField
 * is an abstract method and all type of parameter field should extend it. See
 * https://voparis-confluence.obspm.fr/display/VES/4+-+EPN-TAP+queries to get all parameters
 * details.
 *
 * @author N. Jourdane
 */
public abstract class ParamField extends JPanel {

	/** The serial version UID. */
	private static final long serialVersionUID = 1L;

	/** The logger for the class ParamField. */
	protected static final Logger logger = Logger.getLogger(ParamField.class.getName());

	/** The minimum width of the field. */
	private static final int MIN_FIELD_WIDTH = 30;

	/** The preferred field height. */
	private static final int FIELD_HEIGHT = 20;

	/** The maximum width of the field. */
	private static final int MAX_FIELD_WIDTH = 400;

	/** The preferred label width. */
	private static final int LABEL_WIDTH = 140;

	/** The URL of the resolver used for the `target name` field. */
	private static final String RESOLVER_URL = "http://voparis-registry.obspm.fr/ssodnet/1/autocomplete";

	/** The date format used in the DateRange field */
	private static final String DATE_FORMAT = "dd/MM/yyyy";

	/** The regex used to validate the Date fields */
	private static final String DATE_REGEX = "(^(((0[1-9]|1[0-9]|2[0-8])[\\/](0[1-9]|1[012]))|((29|30|31)[\\/](0[13578]|1[02]))|((29|30)[\\/](0[4,6,9]|11)))[\\/](19|[2-9][0-9])\\d\\d$)|(^29[\\/]02[\\/](19|[2-9][0-9])(00|04|08|12|16|20|24|28|32|36|40|44|48|52|56|60|64|68|72|76|80|84|88|92|96)$)";

	/** The suffix used in REG-TAP parameters names, indicating that it's a beginning of a range. */
	private static final String MIN_SUFFIX = "min";

	/** The suffix used in REG-TAP parameters names, indicating that it is a end of a range. */
	private static final String MAX_SUFFIX = "max";

	/** The main view of the application. */
	protected ViewListener viewListener;

	/** The parameter name of the field */
	protected String paramName;


	/**
	 * Method constructor for the parameter field abstract class, which do all common action for all
	 * type of field, such as displaying the name of the parameter.
	 *
	 * @param mainView The main view of the application.
	 * @param paramName The name of the parameter.
	 */
	public ParamField(ViewListener viewListener, String paramName) {
		super();

		this.viewListener = viewListener;
		this.paramName = paramName;

		buildParamField();
	}

	private void buildParamField() {
		setLayout(new BoxLayout(this, BoxLayout.X_AXIS));

		setMaximumSize(new Dimension(ParamField.MAX_FIELD_WIDTH, ParamField.FIELD_HEIGHT));

		String strLabel = paramName.replaceAll("_", " ").trim();
		JLabel label = new JLabel(strLabel.substring(0, 1).toUpperCase() + strLabel.substring(1));
		label.setPreferredSize(new Dimension(ParamField.LABEL_WIDTH, ParamField.FIELD_HEIGHT));

		this.add(label);
	}


	/**
	 * The string field is used for parameter with a `String` class. It is a simple JTextField with
	 * no verification. The parameter is sent to the controller each time it is modified.
	 *
	 * @author N. Jourdane
	 */
	public static class StringField extends ParamField implements TextFieldListener {

		/** The serial version UID. */
		private static final long serialVersionUID = 1L;

		/** The JTextField used to put the parameter value. */
		JTextField field;


		/**
		 * Method constructor for the string field.
		 *
		 * @param mainView The main view of the application.
		 * @param paramName The name of the parameter.
		 */
		public StringField(ViewListener viewListener, String paramName) {
			super(viewListener, paramName);
			field = new JTextField();
			ParamField.addChangeListener(this, field);
			this.add(field);
		}

		/**
		 * This method is called each time the field is modified.
		 */
		@Override
		public void update(JTextField textField) {
			if (textField.getText().isEmpty()) {
				viewListener.onParameterChanged(paramName, null);
			} else {
				viewListener.onParameterChanged(paramName, textField.getText());
			}
		}
	}

	/**
	 * The float field is used for parameter with a `Float` class. It is a JTextField which checks
	 * if the content is a valid float. If the parameter is valid or if it is empty, then the float
	 * value is sent to the controller.
	 *
	 * @author N. Jourdane
	 */
	public static class FloatField extends ParamField implements TextFieldListener {

		/** The serial version UID. */
		private static final long serialVersionUID = 1L;

		/** The JTextField used to put the parameter value. */
		JTextField field;


		/**
		 * Method constructor
		 *
		 * @param mainView The main view of the application.
		 * @param paramName The name of the parameter.
		 */
		public FloatField(ViewListener viewListener, String paramName) {
			super(viewListener, paramName);
			field = new JTextField();
			ParamField.addChangeListener(this, field);
			this.add(field);
		}

		/**
		 * This method is called each time the field is modified.
		 */
		@Override
		public void update(JTextField textField) {
			if (textField.getText().isEmpty()) {
				textField.setBackground(Color.WHITE);
				viewListener.onParameterRemoved(paramName);
			} else {
				try {
					float value = Float.parseFloat(textField.getText());
					viewListener.onParameterChanged(paramName, value);
					textField.setBackground(Color.WHITE);
				} catch (@SuppressWarnings("unused") NumberFormatException e) {
					textField.setBackground(Color.PINK);
				}
			}
		}
	}

	/**
	 * The date range field is used for couples of parameter with both a `Date` type (actually only
	 * `time_min` and `time_max` parameters is concerned for now). These are JTextFields which check
	 * if the content is a valid date, according to DATE_FORMAT. If the parameter is valid or if it
	 * is empty, then the dates value are sent to the controller, in Julian Day format.
	 *
	 * @author N. Jourdane
	 */
	public static class DateRangeField extends ParamField implements TextFieldListener {

		/** The serial version UID. */
		private static final long serialVersionUID = 1L;

		/** The JTextField used to put the parameter minimum value of the range. */
		JTextField fieldMin;

		/** The JTextField used to put the parameter maximum value of the range. */
		JTextField fieldMax;


		/**
		 * Method constructor
		 *
		 * @param mainView The main view of the application.
		 * @param paramName The name of the parameter.
		 */
		public DateRangeField(ViewListener viewListener, String paramName) {
			super(viewListener, paramName);
			this.add(new JLabel("min "));
			fieldMin = new JTextField();
			fieldMin.setName(ParamField.MIN_SUFFIX);
			fieldMin.setPreferredSize(
					new Dimension(ParamField.MIN_FIELD_WIDTH, ParamField.FIELD_HEIGHT));
			ParamField.addChangeListener(this, fieldMin);
			this.add(fieldMin);

			this.add(new JLabel("max "));
			fieldMax = new JTextField();
			fieldMax.setName(ParamField.MAX_SUFFIX);
			fieldMax.setPreferredSize(
					new Dimension(ParamField.MIN_FIELD_WIDTH, ParamField.FIELD_HEIGHT));
			ParamField.addChangeListener(this, fieldMin);
			this.add(fieldMax);
		}

		/**
		 * This method is called each time the field is modified.
		 */
		@Override
		public void update(JTextField field) {
			DateFormat df = new SimpleDateFormat(ParamField.DATE_FORMAT, Locale.ENGLISH);
			if (field.getText().isEmpty()) {
				field.setBackground(Color.WHITE);
				viewListener.onParameterRemoved(paramName + field.getName());
			} else if (field.getText().matches(ParamField.DATE_REGEX)) {
				try {
					long date = df.parse(field.getText()).getTime();
					date = Math.round(date / 86400000.0 + 2440587.5); // to JD
					viewListener.onParameterChanged(paramName + field.getName(),
							date);
					field.setBackground(Color.WHITE);
				} catch (@SuppressWarnings("unused") ParseException e) {
					field.setBackground(Color.PINK);
				}
				// TODO: check if date min < date max
			} else {
				field.setBackground(Color.PINK);
			}
		}
	}

	/**
	 * The float range field is used for couples of parameter with both a `Float` class. These are
	 * JTextFields which check if the content is a valid float. If the parameter is valid or if it
	 * is empty, then the float value are sent to the controller.
	 *
	 * @author N. Jourdane
	 */
	public static class FloatRangeField extends ParamField implements TextFieldListener {

		/** The serial version UID. */
		private static final long serialVersionUID = 1L;

		/** The JTextField used to put the parameter minimum value of the range. */
		JTextField fieldMin;

		/** The JTextField used to put the parameter maximum value of the range. */
		JTextField fieldMax;


		/**
		 * Method constructor
		 *
		 * @param mainView The main view of the application.
		 * @param paramName The name of the parameter.
		 */
		public FloatRangeField(ViewListener viewListener, String paramName) {
			super(viewListener, paramName);
			fieldMin = new JTextField();
			fieldMin.setName(ParamField.MIN_SUFFIX);
			ParamField.addChangeListener(this, fieldMin);
			this.add(fieldMin);

			fieldMax = new JTextField();
			fieldMax.setName(ParamField.MAX_SUFFIX);
			ParamField.addChangeListener(this, fieldMax);
			this.add(fieldMax);
		}

		/**
		 * This method is called each time the field is modified.
		 */
		@Override
		public void update(JTextField field) {
			if (field.getText().isEmpty()) {
				field.setBackground(Color.WHITE);
				viewListener.onParameterRemoved(paramName + field.getName());
			} else {
				try {
					viewListener.onParameterChanged(paramName + field.getName(),
							Float.parseFloat(field.getText()));
					field.setBackground(Color.WHITE);
				} catch (@SuppressWarnings("unused") NumberFormatException e) {
					field.setBackground(Color.PINK);
				}
			}
		}
	}

	/**
	 * The target name field is used only for the `target_name` parameter. It is a ComboBox which is
	 * automatically filled with actual target names which begins by the entered characters, by
	 * asking to an online resolver (RESOLVER_URL). The parameter is sent to the controller each
	 * time it is updated, so it is possible to enter a parameter that the resolver do not know.
	 *
	 * @author N. Jourdane
	 */
	public static class TargetNameField extends ParamField implements TextFieldListener {

		/** The serial version UID. */
		private static final long serialVersionUID = 1L;

		/** The comboBox to enter the target_name and display target name propositions. */
		JComboBox<String> comboBox;

		/** The JTextField related to the ComboBox, allowing to listen for text content update. */
		JTextField field;

		/**
		 * The content of the last entered value. It is used to avoid recursions, because each time
		 * an update event is detected, the resolver is called and the ComboBox is filled with new
		 * values, which trigger a new event.
		 */
		String lastContent;

		/**
		 * This method is called each time the field is modified. A Runnable is used it is
		 * impossible to modify the comboBox from a DocumentEvent.
		 */
		Runnable updateComboBox = new Runnable() {

			@Override
			public void run() {
				String content = field.getText();
				if (!content.equals(lastContent)) {
					if (content.length() >= 2) {
						lastContent = content;
						comboBox.removeAllItems();
						try {
							for (String s : TargetNameField.getItems(content)) {
								comboBox.addItem(s);
							}
						} catch (CantSendQueryException e) {
							ParamField.logger.log(Level.WARNING,
									"Can't get table names for the resolver", e);
						}
						comboBox.getEditor().setItem(content);
						comboBox.showPopup();
					}
					if (content.isEmpty()) {
						viewListener.onParameterRemoved(paramName);
					} else {
						viewListener.onParameterChanged(paramName, content);
					}
				}
			}
		};


		/**
		 * Method constructor
		 *
		 * @param mainView The main view of the application.
		 * @param paramName The name of the parameter.
		 */
		public TargetNameField(ViewListener viewListener, String paramName) {
			super(viewListener, paramName);
			comboBox = new JComboBox<>();
			comboBox.setPreferredSize(
					new Dimension(ParamField.MIN_FIELD_WIDTH, ParamField.FIELD_HEIGHT));

			comboBox.setEditable(true);
			field = (JTextField) comboBox.getEditor().getEditorComponent();
			ParamField.addChangeListener(this, field);
			this.add(comboBox);
		}

		/**
		 * The method used to get target names propositions by asking to the resolver.
		 *
		 * @param begining The beginning of the target_name.
		 * @return An array of Strings corresponding to the target names got.
		 * @throws CantSendQueryException
		 * @throws CantGetXMLException If the resolver do not work.
		 */
		static String[] getItems(String begining) throws CantSendQueryException {
			Map<String, String> params = new HashMap<>();
			params.put("q", "\"" + begining + "\"");

			String query = Network.buildQuery(ParamField.RESOLVER_URL, params);
			JsonObject root = Network.readJson(query);
			int count = Integer.parseInt(root.get("count").toString());
			String[] targetNames = new String[count];
			JsonArray hits = root.getAsJsonArray("hits");
			for (int i = 0; i < count; i++) {
				JsonObject elmt = hits.get(i).getAsJsonObject();
				targetNames[i] = elmt.get("name").toString().replace("\"", "");
			}
			return targetNames;
		}

		/**
		 * This method is called each time the field is modified.
		 */
		@Override
		public void update(JTextField textField) {
			SwingUtilities.invokeLater(updateComboBox);
		}
	}

	/**
	 * The data product type field is used only for the `dataproduct_type` parameter. It is a
	 * ComboBox filled with a list of static data product types (see
	 * https://voparis-confluence.obspm.fr/display/VES/4+-+EPN-TAP+queries#id-4-EPN-TAPqueries-
	 * __RefHeading__35_312326667_Toc3037660444.2.4DataProductType). The parameter is sent to the
	 * controller each time it is updated.
	 *
	 * @author N. Jourdane
	 */
	public static class DataProductTypeField extends ParamField {

		/** The serial version UID. */
		private static final long serialVersionUID = 1L;

		/** The comboBox used to select the data product type. */
		JComboBox<DataProductType> comboBox;


		/**
		 * An enumeration of all available data product types. Each values comes with an id because
		 * EPN-TAP table can possibly be filled with the id instead of the name, so the query have
		 * to ask for both of them.
		 *
		 * @author N. Jourdane
		 */
		@SuppressWarnings("javadoc")
		enum DataProductType {
			// @noformat
			ALL("All", "all"),            IM("Image", "im"),         SP("Spectrum", "sp"),
			DS("Dynamic spectrum", "ds"), SC("Spectral cube", "sc"), PR("Profile", "pr"),
			VO("Volume", "vo"),           MO("Movie", "mo"),         CU("Cube", "cu"),
			TS("Time series", "ts"),      CA("Catalog", "ca"),       SV("Spatial vector", "sv");
			// @format

			/** The full name of the data product type, such as `Dynamic spectrum`. */
			private String name = "";

			/** The id of the data product type, such as `ds`. */
			private String id = "";


			/**
			 * Method constructor for the enumeration.
			 *
			 * @param name The full name of the data product type, such as `Dynamic spectrum`.
			 * @param id The id of the data product type, such as `ds`.
			 */
			DataProductType(String name, String id) {
				this.name = name;
				this.id = id;
			}

			/**
			 * @return A list of two strings, containing the name (formated for the query) and the
			 *         id, used in the query. A list is used instead of a array because the getQuery
			 *         function ( @see Queries) needs to know the parameter type.
			 */
			public List<String> query() {
				List<String> item = new ArrayList<>();
				item.add(name.replace(" ", "-").toLowerCase());
				item.add(id);
				return item;
			}

			@Override
			public String toString() {
				return name;
			}
		}


		/**
		 * Method constructor
		 *
		 * @param mainView The main view of the application.
		 * @param paramName The name of the parameter.
		 */
		public DataProductTypeField(ViewListener viewListener, String paramName) {
			super(viewListener, paramName);
			comboBox = new JComboBox<>(DataProductType.values());
			comboBox.setSelectedItem(DataProductType.ALL);
			comboBox.setPreferredSize(
					new Dimension(ParamField.MIN_FIELD_WIDTH, ParamField.FIELD_HEIGHT));
			comboBox.addActionListener(new ActionListener() {

				@Override
				public void actionPerformed(ActionEvent e) {
					update();
				}
			});
			this.add(comboBox);
		}

		/**
		 * This method is called each time the field is modified.
		 */
		void update() {
			DataProductType item = (DataProductType) comboBox.getSelectedItem();
			if (DataProductType.ALL.equals(item)) {
				viewListener.onParameterRemoved(paramName);
			} else {
				viewListener.onParameterChanged(paramName, item.query());
			}
		}
	}

	/**
	 * The target class field is used only for the `target_class` parameter. It is a ComboBox filled
	 * with a list of static target classes (see
	 * https://voparis-confluence.obspm.fr/display/VES/4+-+EPN-TAP+queries#id-4-EPN-TAPqueries-
	 * __RefHeading__39_312326667_Toc3037660464.2.6TargetClass). The parameter is sent to the
	 * controller each time it is updated.
	 *
	 * @author N. Jourdane
	 */
	public static class TargetClassField extends ParamField {

		/** The serial version UID. */
		private static final long serialVersionUID = 1L;

		/** The comboBox used to select the target class. */
		JComboBox<TargetClass> comboBox;


		/**
		 * An enumeration of all available target classes.
		 *
		 * @author N. Jourdane
		 */
		@SuppressWarnings("javadoc")
		enum TargetClass {
			// @noformat
			ALL("All"),        COMET("Comet"),   EXOPLANET("Exoplanet"),    RING("Ring"),
			SAMPLE("Sample"),  SKY("Sky"),       SPACECRAFT("Spacecraft"),  SPACEJUNK("Spacejunk"),
			STAR("Star"),      INTERPLANETARY_MEDIUM("Interplanetary medium");
			// @format

			/** The name of the target class. */
			String name;


			/**
			 * Method constructor for the enumeration.
			 *
			 * @param name The name of the target class.
			 */
			TargetClass(String name) {
				this.name = name;
			}

			/**
			 * @return The name formated for the query.
			 */
			String query() {
				return name.replace(" ", "_").toLowerCase();
			}
		}


		/**
		 * Method constructor
		 *
		 * @param mainView The main view of the application.
		 * @param paramName The name of the parameter.
		 */
		public TargetClassField(ViewListener viewListener, String paramName) {
			super(viewListener, paramName);
			comboBox = new JComboBox<>(TargetClass.values());
			comboBox.setPreferredSize(
					new Dimension(ParamField.MIN_FIELD_WIDTH, ParamField.FIELD_HEIGHT));
			comboBox.addActionListener(new ActionListener() {

				@Override
				public void actionPerformed(ActionEvent e) {
					update();
				}
			});
			this.add(comboBox);
		}

		/**
		 * This method is called each time the field is modified.
		 */
		void update() {
			TargetClass value = (TargetClass) comboBox.getSelectedItem();
			if (TargetClass.ALL.equals(value)) {
				viewListener.onParameterRemoved(paramName);
			} else {
				viewListener.onParameterChanged(paramName, value.query());
			}
		}
	}

	/**
	 * The listener of text field, it aims to provide a simple way to listen a text field without to
	 * override removeUpdate(DocumentEvent de), insertUpdate(DocumentEvent de) and
	 * changedUpdate(DocumentEvent de) on each field to listen.
	 *
	 * @author N. Jourdane
	 */
	interface TextFieldListener {

		/**
		 * When the content of the JTextField is updated.
		 *
		 * @param field The JTextField. Is useful for classes containing several text fields, such
		 *            as DateRangeField, to know which one triggered the event.
		 */
		void update(JTextField field);
	}


	/**
	 * To add the listener. @see TextFieldListener
	 *
	 * @param changeListener The listener of text fields.
	 * @param field The field to listen.
	 */
	static void addChangeListener(final TextFieldListener changeListener, final JTextField field) {
		field.getDocument().addDocumentListener(new DocumentListener() {

			@Override
			public void removeUpdate(DocumentEvent de) {
				changeListener.update(field);
			}

			@Override
			public void insertUpdate(DocumentEvent de) {
				changeListener.update(field);
			}

			@Override
			public void changedUpdate(DocumentEvent de) {
				changeListener.update(field);
			}
		});
	}
}