package eu.schudt.javafx.controls.calendar;

import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.*;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.effect.DropShadow;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.stage.Popup;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Timer;
import java.util.TimerTask;

import eu.schudt.javafx.controls.calendar.CalendarView;
/**
 * @author Christian Schudt
 */
public class DatePicker extends HBox {

	private static final String CSS_DATE_PICKER_VALID = "datepicker-valid";
	private static final String CSS_DATE_PICKER_INVALID = "datepicker-invalid";

	/**
	 * Initializes the date picker with the default locale.
	 */
	public DatePicker() {
		this(Locale.US);
	}

	public DatePicker(String id) {
		this(Locale.US, id);
	}

	private Timer timer;

	/**
	 * Initializes the date picker with the given locale.
	 * 
	 * @param locale
	 *            The locale.
	 */
	public DatePicker(Locale locale) {
		calendarView = new CalendarView(locale);

		textField = new TextField();
		this.locale.set(locale);

		calendarView.setEffect(new DropShadow());

		// Use the same locale.
		calendarView.localeProperty().bind(localeProperty());

		// Bind the current date of the calendar view with the selected date, so
		// that the calendar shows up with the same month as in the text field.
		calendarView.currentDateProperty().bind(selectedDateProperty());

		// When the user selects a date in the calendar view, hide it.
		calendarView.selectedDateProperty().addListener(
				new InvalidationListener() {
					public void invalidated(Observable observable) {
						selectedDate.set(calendarView.selectedDateProperty().get());
						hidePopup();
					}
				});

		// Let the prompt text property listen to locale or date format changes.
		textField.promptTextProperty().bind(new StringBinding() {
			{
				super.bind(localeProperty(), promptTextProperty(), dateFormatProperty());
			}

			@Override
			protected String computeValue() {
				// First check, if there is a custom prompt text.
				if (promptTextProperty().get() != null) {
					return promptTextProperty().get();
				}

				// If not, use the the date format's pattern.
				DateFormat dateFormat = getActualDateFormat();
				if (dateFormat instanceof SimpleDateFormat) {
					return ((SimpleDateFormat) dateFormat).toPattern();
				}

				return "";
			}
		});

		// Change the CSS styles, when this control becomes invalid.
		invalid.addListener(new InvalidationListener() {
			public void invalidated(Observable observable) {
				if (invalid.get()) {
					textField.getStyleClass().add(CSS_DATE_PICKER_INVALID);
					textField.getStyleClass().remove(CSS_DATE_PICKER_VALID);
				} else {
					textField.getStyleClass().remove(CSS_DATE_PICKER_INVALID);
					textField.getStyleClass().add(CSS_DATE_PICKER_VALID);
				}
			}
		});

		// When the text field no longer has the focus, try to parse the date.
		textField.addEventHandler(MouseEvent.MOUSE_PRESSED,
				new EventHandler<MouseEvent>() {
					public void handle(MouseEvent event) {
						if (!textField.focusedProperty().get()) {
							if (!textField.getText().equals("")) {
								tryParse(true);
							}
						} else {
							showHidePopup();
						}
					}
				});

		// Listen to user input.
		textField.textProperty().addListener(new ChangeListener<String>() {
			public void changed(ObservableValue<? extends String> observableValue, String s, String s1) {
				// Only evaluate the input, it it wasn't set programmatically.
				if (textSetProgrammatically) {
					return;
				}
				if (timer != null) {
					timer.cancel();
				}
				// If the user clears the text field, set the date to null and the field to valid.
				if (s1.equals("")) {
					selectedDate.set(null);
					invalid.set(false);
				} else {
					// Start a cyclic timer, so that when the text field no longer has the focus, try to parse the date.
					timer = new Timer();
					timer.schedule(new TimerTask() {
						@Override
						public void run() {
							Platform.runLater(new Runnable() {
								public void run() {
									if (!textField.focusedProperty().get()) {
										tryParse(false);
									}
								}
							});
						}
					}, 1000, 1000);
				}
			}
		});

		selectedDateProperty().addListener(new InvalidationListener() {
			public void invalidated(Observable observable) {
				updateTextField();
				invalid.set(false);
			}
		});

		localeProperty().addListener(new InvalidationListener() {
			public void invalidated(Observable observable) {
				updateTextField();
			}
		});

		textField.addEventHandler(KeyEvent.KEY_PRESSED,
				new EventHandler<KeyEvent>() {
					public void handle(KeyEvent keyEvent) {
						// When the user click on DOWN key, show calendar popup 
						if (keyEvent.getCode() == KeyCode.DOWN) {
							showPopup();
						}
						// When the user click on ENTER key, try to parse the date.
						else if (keyEvent.getCode() == KeyCode.ENTER) {
							tryParse(false);
						}
					}
				});

		Button button = new Button(">");
		button.setFocusTraversable(false);
		button.setOnAction(new EventHandler<ActionEvent>() {
			public void handle(ActionEvent actionEvent) {
				showPopup();
			}
		});
		getChildren().add(textField);
	}

	public DatePicker(Locale locale, String id) {
		this(locale);
		textField.setId(id);
	}

	private void hidePopup() {
		if (popup != null) {
			popup.hide();
		}
	}

	/**
	 * Tries to parse the text field for a valid date.
	 * 
	 * @param setDateToNullOnException
	 *            True, if the date should be set to null, when a
	 *            {@link ParseException} occurs. This is the case, when the text
	 *            field loses focus.
	 */
	private void tryParse(boolean setDateToNullOnException) {
		if (timer != null) {
			timer.cancel();
		}
		try {
			// Double parse the date here, since e.g. 01.01.1 is parsed as year 1, and then formatted as 01.01.01 and then parsed as year 2001.
			// This might lead to an undesired date.
			DateFormat dateFormat = getActualDateFormat();
			Date parsedDate = dateFormat.parse(textField.getText());

			parsedDate = dateFormat.parse(dateFormat.format(parsedDate));
			if (selectedDate.get() == null || selectedDate.get() != null
					&& parsedDate.getTime() != selectedDate.get().getTime()) {
				selectedDate.set(parsedDate);
			}
			invalid.set(false);
			updateTextField();
		} catch (ParseException e) {
			invalid.set(true);
			if (setDateToNullOnException) {
				selectedDate.set(null);
			}
		}

	}

    // 10965 - CDPP interface behavior : Check and update startDate
    /**
     * Call private function tryParse.
     *
     * @param setDateToNullOnException True, if the date should be set to null,
     * when a {@link ParseException} occurs. This is the case, when the text
     * field loses focus.
     */
    public void checkAndUpdateDate(boolean setDateToNullOnException) {
        tryParse(setDateToNullOnException);
    }
       
	private boolean textSetProgrammatically;

	/**
	 * Updates the text field.
	 */
	private void updateTextField() {
		// Mark the we update the text field (and not the user), so that it can be ignored, by textField.textProperty()
		textSetProgrammatically = true;
		if (selectedDateProperty().get() != null) {
			String date = getActualDateFormat().format(selectedDateProperty().get());
			if (!textField.getText().equals(date)) {
				textField.setText(date);
			}
		} else {
			textField.setText("");
		}
		textSetProgrammatically = false;
	}

	private final static DateFormat yyyymmddThhMMss = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");

	/**
	 * Gets the actual date format. If {@link #dateFormatProperty()} is set,
	 * take it, otherwise get a default format for the current locale.
	 * 
	 * @return The date format.
	 */
	private DateFormat getActualDateFormat() {
		if (dateFormat.get() != null) {
			return dateFormat.get();
		}
		return yyyymmddThhMMss;
	}

	private CalendarView calendarView;

	/**
	 * Use this to set further properties of the calendar.
	 * 
	 * @return The calendar view.
	 */
	public CalendarView getCalendarView() {
		return calendarView;
	}

	private TextField textField;

	private BooleanProperty invalid = new SimpleBooleanProperty();

	/**
	 * States whether the user input is invalid (is no valid date).
	 * 
	 * @return The property.
	 */
	public ReadOnlyBooleanProperty invalidProperty() {
		return invalid;
	}

	/**
	 * The locale.
	 * 
	 * @return The property.
	 */
	public ObjectProperty<Locale> localeProperty() {
		return locale;
	}

	private ObjectProperty<Locale> locale = new SimpleObjectProperty<Locale>();

	public void setLocale(Locale locale) {
		this.locale.set(locale);
	}

	public Locale getLocale() {
		return locale.get();
	}

	/**
	 * The selected date.
	 * 
	 * @return The property.
	 */
	public ObjectProperty<Date> selectedDateProperty() {
		return selectedDate;
	}

	private ObjectProperty<Date> selectedDate = new SimpleObjectProperty<Date>();

	/**
	 * Gets the date format.
	 * 
	 * @return The date format.
	 */
	public ObjectProperty<DateFormat> dateFormatProperty() {
		return dateFormat;
	}

	private ObjectProperty<DateFormat> dateFormat = new SimpleObjectProperty<DateFormat>();

	public void setDateFormat(DateFormat dateFormat) {
		this.dateFormat.set(dateFormat);
	}

	public DateFormat getDateFormat() {
		return dateFormat.get();
	}

	private StringProperty promptText = new SimpleStringProperty();

	/**
	 * The prompt text for the text field. By default, the prompt text is taken
	 * from the date format pattern.
	 * 
	 * @return The property.
	 */
	public StringProperty promptTextProperty() {
		return promptText;
	}

	public void setPromptText(String promptText) {
		this.promptText.set(promptText);
	}

	public String getPromptText() {
		return promptText.get();
	}

	private Popup popup;

	/**
	 * Shows/Hides the pop up.
	 */
	private void showHidePopup() {
		if (popup == null) {
			showPopup();
		} else {
			if (popup.isShowing()) {
				popup.hide();
			}
			else {
				showPopup();
			}
		}
	}

	/**
	 * Shows the pop up.
	 */
	private void showPopup() {

		if (popup == null) {
			popup = new Popup();
			popup.setAutoHide(true);
			popup.setHideOnEscape(true);
			popup.setAutoFix(true);
			popup.getContent().add(calendarView);
		}

		Bounds calendarBounds = calendarView.getBoundsInLocal();
		Bounds bounds = localToScene(getBoundsInLocal());

		double posX = calendarBounds.getMinX() + bounds.getMinX() + getScene().getX() + getScene().getWindow().getX();
		double posY = calendarBounds.getMinY() + bounds.getHeight() + bounds.getMinY() + getScene().getY() + getScene().getWindow().getY();

		popup.show(this, posX, posY);

	}

	public TextField getTextField() {
		return textField;
	}

	public void refresh() {
		calendarView.refresh();
	}
}
