/*
 * Licensed to The Apereo Foundation under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for
 * additional information regarding copyright ownership.
 *
 * The Apereo Foundation licenses this file to you under the Apache License,
 * Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at:
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
*/
package org.unitime.timetable.gwt.client.widgets;

import java.util.List;

import org.unitime.timetable.gwt.client.aria.AriaStatus;
import org.unitime.timetable.gwt.client.aria.AriaTextBox;
import org.unitime.timetable.gwt.resources.GwtAriaMessages;
import org.unitime.timetable.gwt.resources.GwtConstants;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.event.dom.client.BlurEvent;
import com.google.gwt.event.dom.client.BlurHandler;
import com.google.gwt.event.dom.client.FocusEvent;
import com.google.gwt.event.dom.client.FocusHandler;
import com.google.gwt.event.dom.client.HasAllFocusHandlers;
import com.google.gwt.event.dom.client.HasAllKeyHandlers;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.dom.client.KeyPressHandler;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.event.logical.shared.HasSelectionHandlers;
import com.google.gwt.event.logical.shared.SelectionEvent;
import com.google.gwt.event.logical.shared.SelectionHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.Focusable;
import com.google.gwt.user.client.ui.HasValue;
import com.google.gwt.user.client.ui.MenuBar;
import com.google.gwt.user.client.ui.MenuItem;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.ScrollPanel;

/**
 * @author Tomas Muller
 */
public class TimeSelector extends Composite implements HasValue<Integer>, Focusable, HasAllKeyHandlers, HasAllFocusHandlers, HasSelectionHandlers<Integer> {
	private static GwtAriaMessages ARIA = GWT.create(GwtAriaMessages.class);
	private static final GwtConstants CONSTANTS = GWT.create(GwtConstants.class);
	private TimeSelector iStart;
	
	private TimeMenu iTimes;
	private ScrollPanel iTimeScroll;
	
	private PopupPanel iPopup;
	private AriaTextBox iText;
	private Integer iDiff = null;
	
	public TimeSelector() {
		this(null);
	}
	
	public TimeSelector(TimeSelector start) {
		iStart = start;
		iText = new AriaTextBox();
		iText.setStyleName("gwt-SuggestBox");
		iText.addStyleName("unitime-TimeSelector");
		iText.setAriaLabel(start == null ? ARIA.startTime() : ARIA.endTime());
		
		iTimes = new TimeMenu();
		
		iTimeScroll = new ScrollPanel(iTimes);
		iTimeScroll.addStyleName("scroll");
		
		iPopup = new PopupPanel(true, false);
		iPopup.setPreviewingAllNativeEvents(true);
		iPopup.setStyleName("unitime-TimeSelectorPopup");
		iPopup.setWidget(iTimeScroll);
		
		initWidget(iText);
		
		createSuggestions();
		
		iText.addKeyDownHandler(new KeyDownHandler() {
			@Override
			public void onKeyDown(KeyDownEvent event) {
				if (isSuggestionsShowing()) {
					switch (event.getNativeEvent().getKeyCode()) {
					case KeyCodes.KEY_DOWN:
						iTimes.selectItem(iTimes.getSelectedItemIndex() + 1);
						updateSuggestionStatus();
						break;
					case KeyCodes.KEY_UP:
						if (iTimes.getSelectedItemIndex() == -1) {
							iTimes.selectItem(iTimes.getNumItems() - 1);
						} else {
							iTimes.selectItem(iTimes.getSelectedItemIndex() - 1);
						}
						updateSuggestionStatus();
						break;
					case KeyCodes.KEY_ENTER:
						iTimes.executeSelected();
						hideSuggestions();
						break;
					case KeyCodes.KEY_TAB:
						hideSuggestions();
						break;
					case KeyCodes.KEY_ESCAPE:
						hideSuggestions();
						break;
					}
					switch (event.getNativeEvent().getKeyCode()) {
					case KeyCodes.KEY_DOWN:
					case KeyCodes.KEY_UP:
					case KeyCodes.KEY_ENTER:
					case KeyCodes.KEY_ESCAPE:
						event.preventDefault();
						event.stopPropagation();
					}
				} else {
					if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_DOWN && (event.getNativeEvent().getAltKey() || iText.getCursorPos() == iText.getText().length())) {
						showSuggestions();
						updateSuggestionStatus();
						event.preventDefault();
						event.stopPropagation();
					}
				}
			}
		});
		
        iText.addKeyUpHandler(new KeyUpHandler() {
			@Override
			public void onKeyUp(KeyUpEvent event) {
				if (selectASuggestion()) {
					if (!isSuggestionsShowing()) showSuggestions();
					updateSuggestionStatus();
				}
			}
		});
        
        iText.addValueChangeHandler(new ValueChangeHandler<String>() {
			@Override
			public void onValueChange(ValueChangeEvent<String> event) {
				setValue(parseTime(event.getValue()), true);
			}
		});
        
        if (iStart != null)
        	iStart.addValueChangeHandler(new ValueChangeHandler<Integer>() {
				@Override
				public void onValueChange(ValueChangeEvent<Integer> event) {
					createSuggestions();
					if (iDiff != null && event.getValue() != null)
						setValue(Math.min(event.getValue() + iDiff, 288));
				}
			});
        
        iText.addFocusHandler(new FocusHandler() {
			@Override
			public void onFocus(FocusEvent event) {
				showSuggestions();
			}
		});
	}
	
	private String iLastSelected = null;
	TimeMenuItem iInsertedSuggestion = null;
	private boolean selectASuggestion() {
		if (iText.getText().equals(iLastSelected)) return false;
		if (iInsertedSuggestion != null) {
			iTimes.removeItem(iInsertedSuggestion);
			iInsertedSuggestion = null;
		}
		iLastSelected = iText.getText();
		Integer slot = getValue();
		if (slot == null) {
			if (iStart == null)
				slot = CONSTANTS.eventStartDefault(); // 730a
			else if (iStart.getValue() == null)
				slot = CONSTANTS.eventStopDefault(); // 530p
			else
				slot = iStart.getValue() + CONSTANTS.eventLengthDefault(); // 1 hour
		}
		int select = -1, diff = 0;
		for (int i = 0; i < iTimes.getNumItems(); i++) {
			int suggestion = iTimes.getSlot(i);
			if (select < 0 || Math.abs(slot - suggestion) < diff && slot >= suggestion) {
				diff = Math.abs(slot - suggestion); select = i;
			}
		}
		if (getValue() != null && diff != 0) {
			iInsertedSuggestion = new TimeMenuItem(slot);
			if (select == 0 && iStart != null && slot < iTimes.getSlot(0)) select --;
			iTimes.insertItem(iInsertedSuggestion, select + 1);
			iTimes.selectItem(select + 1);
		} else {
			iTimes.selectItem(select);	
		}
		return true;
	}
	
	private void createSuggestions() {
		iLastSelected = null;
		iTimes.clearItems();
		if (iStart == null) {
			for (int t = 0; t < 288; t += CONSTANTS.eventSlotIncrement()) {
				iTimes.addItem(new TimeMenuItem(t));
			}
			iTimeScroll.setWidth("77px");
		} else {
			Integer prev = iStart.getValue();
			for (int t = CONSTANTS.eventSlotIncrement() + (prev == null ? 0 : prev); t <= 288; t += CONSTANTS.eventSlotIncrement()) {
				iTimes.addItem(new TimeMenuItem(t));
			}
			iTimeScroll.setWidth(prev == null ? "77px" : "137px");
		}
		selectASuggestion();
	}
	
	private void hideSuggestions() {
		if (iPopup.isShowing()) iPopup.hide();
	}
	
	private void showSuggestions() {
		iPopup.showRelativeTo(iText);
		Integer value = getValue();
		if (value != null && (iTimes.getSelectedItemIndex() < 0 || iTimes.getItem(iTimes.getSelectedItemIndex()).getSlot() != value)) {
			for (int i = 0; i < iTimes.getNumItems(); i++) {
				if (iTimes.getItem(i).getSlot() == value) {
					iTimes.selectItem(i);
					break;
				}
			}
		}
		iTimes.scrollToView();
	}
	
	private boolean isSuggestionsShowing() {
		return iPopup.isShowing();
	}
	
	public TimeMenu getTimeMenu() {
		return iTimes;
	}
	
	public static String slot2time(int slot, int diff) {
		if (diff <= 0) return TimeUtils.slot2time(slot);
		if (diff < 24 && diff != 12) return TimeUtils.slot2time(slot) + " (" + (5 * diff) + " mins)";
		else if (diff == 12) return TimeUtils.slot2time(slot) + " (1 hr)";
		else if (diff % 12 == 0) return TimeUtils.slot2time(slot) + " (" + (diff/12) + " hrs)";
		else if (diff % 12 == 3) return TimeUtils.slot2time(slot) + " (" + (diff/12) + "&frac14; hrs)";
		else if (diff % 12 == 6) return TimeUtils.slot2time(slot) + " (" + (diff/12) + "&frac12; hrs)";
		else if (diff % 12 == 9) return TimeUtils.slot2time(slot) + " (" + (diff/12) + "&frac34; hrs)";
		return TimeUtils.slot2time(slot) + " (" + diff / 12 + ":" + (diff % 12 == 1 ? "0" : "") + (5 * (diff % 12)) + ")";
	}
	
	public static String slot2aria(int slot, int diff) {
		if (diff <= 0) return TimeUtils.slot2aria(slot);
		if (diff < 24 && diff != 12) return TimeUtils.slot2aria(slot) + " (" + (5 * diff) + " minutes)";
		else if (diff == 12) return TimeUtils.slot2aria(slot) + " (1 hour)";
		else if (diff % 12 == 0) return TimeUtils.slot2aria(slot) + " (" + (diff/12) + " hours)";
		else if (diff % 12 == 3) return TimeUtils.slot2aria(slot) + " (" + (diff/12) + "&frac14; hours)";
		else if (diff % 12 == 6) return TimeUtils.slot2aria(slot) + " (" + (diff/12) + "&frac12; hours)";
		else if (diff % 12 == 9) return TimeUtils.slot2aria(slot) + " (" + (diff/12) + "&frac34; hours)";
		return TimeUtils.slot2aria(slot) + " (" + diff / 12 + ":" + (diff % 12 == 1 ? "0" : "") + (5 * (diff % 12)) + ")";
	}
	
	public Integer parseTime(String text) {
		return TimeUtils.parseTime(CONSTANTS, text, (iStart == null ? null : iStart.getValue()));
	}
	
	public class TimeMenu extends MenuBar implements HasAllFocusHandlers {
		TimeMenu() {
			super(true);
			setStyleName("");
			setFocusOnHoverEnabled(false);
			setFocusOnHoverEnabled(false);
			sinkEvents(Event.ONBLUR);
			sinkEvents(Event.ONFOCUS);
		}
		
		@Override
		public void onBrowserEvent(Event event) {
			switch (DOM.eventGetType(event)) {
		    case Event.ONBLUR:
		    	BlurEvent.fireNativeEvent(event, this);
		    	break;
		    case Event.ONFOCUS:
		    	FocusEvent.fireNativeEvent(event, this);
		    	break;
			}
			super.onBrowserEvent(event);
		}
		
		public int getNumItems() {
			return getItems().size();
		}
		
		public int getSelectedItemIndex() {
			MenuItem selectedItem = getSelectedItem();
			if (selectedItem != null)
				return getItems().indexOf(selectedItem);
			return -1;
		}
		
		public void selectItem(int index) {
			List<MenuItem> items = getItems();
			if (index > -1 && index < items.size()) {
				selectItem(items.get(index));
				iTimeScroll.ensureVisible(items.get(index));
			}
		}
		
		public void scrollToView() {
			List<MenuItem> items = getItems();
			int index = getSelectedItemIndex();
			if (index > -1 && index < items.size()) {
				iTimeScroll.ensureVisible(items.get(index));
			}
		}
		
		public void executeSelected() {
			MenuItem selected = getSelectedItem();
			if (selected != null) {
				selected.getScheduledCommand().execute();
				setStatus(ARIA.suggestionSelected(((TimeMenuItem)selected).toAriaString()));
			}
		}
		
		public int getSlot(int index) {
			List<MenuItem> items = getItems();
			if (index > -1 && index < items.size()) {
				return ((TimeMenuItem)items.get(index)).getSlot();
			} else {
				return -1;
			}
		}
		
		public TimeMenuItem getItem(int index) {
			List<MenuItem> items = getItems();
			if (index > -1 && index < items.size()) {
				return (TimeMenuItem)items.get(index);
			} else {
				return null;
			}
		}
		
		@Override
		public HandlerRegistration addBlurHandler(BlurHandler handler) {
			return addHandler(handler, BlurEvent.getType());
		}

		@Override
		public HandlerRegistration addFocusHandler(FocusHandler handler) {
			return addHandler(handler, FocusEvent.getType());
		}
	}
	
	private class TimeMenuItem extends MenuItem {
		private int iSlot;
		
		private TimeMenuItem(final int slot) {
			super(slot2time(slot, iStart == null || iStart.getValue() == null ? 0 : slot - iStart.getValue()),
				true,
				new ScheduledCommand() {
					@Override
					public void execute() {
						hideSuggestions();
						setValue(slot, true);
						iLastSelected = iText.getText();
						fireSuggestionEvent(slot);
					}
				}
			);
			setStyleName("item");
			getElement().setAttribute("whiteSpace", "nowrap");
			iSlot = slot;
		}
		
		public int getSlot() {
			return iSlot;
		}
		
		public String toAriaString() {
			return slot2aria(iSlot, iStart == null || iStart.getValue() == null ? 0 : iSlot - iStart.getValue());
		}
	}


	@Override
	public HandlerRegistration addValueChangeHandler(ValueChangeHandler<Integer> handler) {
		return addHandler(handler, ValueChangeEvent.getType());
	}

	@Override
	public Integer getValue() {
		return parseTime(iText.getText());
	}

	@Override
	public void setValue(Integer value) {
		setValue(value, false);
	}

	@Override
	public void setValue(Integer value, boolean fireEvents) {
		if (value == null)
			iText.setText("");
		else {
			if (iStart != null && iStart.getValue() != null)
				iDiff = value - iStart.getValue();
			iText.setText(TimeUtils.slot2time(value));
		}
		if (fireEvents)
			ValueChangeEvent.fire(this, value);
	}
	
	public void setDiff(Integer diff) {
		iDiff = diff;
	}
	
	public static class TimeUtils {

		public static Integer parseTime(GwtConstants constants, String text, Integer start) {
			if (!text.isEmpty()) {
				for (String noon: constants.parseTimeNoon()) {
					if (noon.startsWith(text.toLowerCase())) return 144;
				}
				for (String midnight: constants.parseTimeMidnight()) {
					if (midnight.startsWith(text.toLowerCase())) return (start == null ? 0 : 288);
				}
			}
			if (start != null) {
				for (String h: constants.parseTimeHours()) {
					if (text.toLowerCase().endsWith(h)) {
						try {
							return start + Math.round(12 * Float.parseFloat(text.substring(0, text.length() - h.length()).trim()));
						} catch (Exception e) {}
					}
				}
				for (String m: constants.parseTimeMinutes()) {
					if (text.toLowerCase().endsWith(m)) {
						try {
							return start + Integer.parseInt(text.substring(0, text.length() - m.length()).trim()) / 5;
						} catch (Exception e) {}
					}
				}
			}
			int startHour = 0, startMin = 0;
			String token = text.trim();
			String number = "";
			boolean plus = false;
			if (token.startsWith("+")) { plus = true; token = token.substring(1).trim(); }
			while (!token.isEmpty() && token.charAt(0) >= '0' && token.charAt(0) <= '9') { number += token.substring(0, 1); token = token.substring(1); }
			if (number.isEmpty()) return null;
			if (number.length()>2) {
				try {
					startHour = Integer.parseInt(number) / 100;
					startMin = Integer.parseInt(number) % 100;
				} catch (NumberFormatException e) {
					return null;
				}
			} else {
				try {
					startHour = Integer.parseInt(number);
				} catch (NumberFormatException e) {
					return null;
				}
			}
			while (token.startsWith(" ")) token = token.substring(1);
			if (token.startsWith(":") || token.startsWith(".") || (!token.isEmpty() && token.charAt(0) >= '0' && token.charAt(0) <= '9')) {
				token = token.substring(1);
				while (token.startsWith(" ")) token = token.substring(1);
				number = "";
				while (!token.isEmpty() && token.charAt(0) >= '0' && token.charAt(0) <= '9') { number += token.substring(0, 1); token = token.substring(1); }
				if (!number.isEmpty()) {
					try {
						startMin = Integer.parseInt(number);
					} catch (NumberFormatException e) {
						return null;
					}
				}
			}
			while (token.startsWith(" ")) token = token.substring(1);
			boolean hasAmOrPm = false;
			if (token.isEmpty() && plus && start != null) {
				int slot = start + (60 * startHour + startMin) / 5;
				if (slot > start && slot <= 288) return slot;
				if (startMin == 0) {
					slot = start + startHour / 5;
					if (slot > start && slot <= 288) return slot;
				}
			}
			for (String am: constants.parseTimeAm()) {
				if (token.toLowerCase().startsWith(am)) {
					token = token.substring(2); hasAmOrPm = true;
					if (startHour == 12) startHour = 24;
					break;
				}
			}
			for (String pm: constants.parseTimePm()) {
				if (token.toLowerCase().startsWith(pm)) {
					token = token.substring(2); hasAmOrPm = true;
					if (startHour<12) startHour += 12;
					break;
				}
			}
			if (!token.isEmpty()) return null;
			// if (startHour < 7 && !hasAmOrPm) startHour += 12;
			if (startMin % 5 != 0) startMin = 5 * ((startMin + 2)/ 5);
			// if (startHour == 7 && startMin == 0 && !hasAmOrPm) startHour += 12;
			int slot = (60 * startHour + startMin) / 5;
			if (start != null && slot <= start && slot <= 144 && !hasAmOrPm) slot += 144;
			if (start != null && slot <= start) return null;
			if (slot < 0 || slot > 288) {
				if (!text.endsWith("000"))
					return parseTime(constants, text + "0", start);
				else
					return null;
			}
			return slot;
		}
		
		public static Integer parseMilitary(String text) {
			if (text == null || text.isEmpty()) return null;
			try {
				int time = Integer.parseInt(text);
				int hour = time / 100;
				int min = time % 100;
				int slot = (60 * hour + min) / 5;
				return (slot < 0 || slot > 288 ? null : slot);
			} catch (NumberFormatException e) {
				return null;
			}
		}
		
		public static Integer parseTime2(GwtConstants constants, String text, Integer start) {
			Integer slot = parseMilitary(text);
			if (slot != null) return slot;
			return parseTime(constants, text, start);
		}
		
		public static String slot2time(int slot) {
			if (CONSTANTS.useAmPm()) {
				if (slot == 0) return CONSTANTS.timeMidnight();
				if (slot == 144) return CONSTANTS.timeNoon();
				if (slot == 288) return CONSTANTS.timeMidnightEnd();
			}
			int h = slot / 12;
	        int m = 5 * (slot % 12);
	        if (CONSTANTS.useAmPm())
	        	return (h > 12 ? h - 12 : h) + ":" + (m < 10 ? "0" : "") + m + " " + (h == 24 ? CONSTANTS.timeAm() : h >= 12 ? CONSTANTS.timePm() : CONSTANTS.timeAm());
	        else
				return h + ":" + (m < 10 ? "0" : "") + m;
		}
		
		public static String slot2military(int slot) {
			int h = slot / 12;
	        int m = 5 * (slot % 12);
	        return h + (m < 10 ? "0" : "") + m;
		}
		
		public static String slot2short(int slot) {
			if (CONSTANTS.useAmPm()) {
				if (slot == 0) return CONSTANTS.timeMidnight();
				if (slot == 144) return CONSTANTS.timeNoon();
				if (slot == 288) return CONSTANTS.timeMidnightEnd();
			}
			int h = slot / 12;
	        int m = 5 * (slot % 12);
	        if (CONSTANTS.useAmPm())
	        	return (h > 12 ? h - 12 : h) + ":" + (m < 10 ? "0" : "") + m + (h == 24 ? CONSTANTS.timeShortAm() : h >= 12 ? CONSTANTS.timeShortPm() : CONSTANTS.timeShortAm());
	        else
				return h + ":" + (m < 10 ? "0" : "") + m;
		}
		
		public static String slot2aria(int slot) {
			if (CONSTANTS.useAmPm()) {
				if (slot == 0) return CONSTANTS.timeMidnight();
				if (slot == 144) return CONSTANTS.timeNoon();
				if (slot == 288) return CONSTANTS.timeMidnightEnd();
			}
			int h = slot / 12;
	        int m = 5 * (slot % 12);
	        if (CONSTANTS.useAmPm())
	        	return (h > 12 ? h - 12 : h) + (m == 0 ? "" : (m < 10 ? " 0" : " ") + m) + (h == 24 ? " " + CONSTANTS.timeAm() : h >= 12 ? " " + CONSTANTS.timePm() : " " + CONSTANTS.timeAm());
	        else
	        	return h + (m == 0 ? "" : (m < 10 ? " 0" : " ") + m);
		}
	}
	
	public void setStatus(String text) {
		AriaStatus.getInstance().setText(text);
	}
	
	public void updateSuggestionStatus() {
		if (iPopup.isShowing()) {
			int index = iTimes.getSelectedItemIndex();
			if (index < 0) {
				setStatus(ARIA.showingMultipleSuggestionsNoQueryNoneSelected(iTimes.getNumItems()));
				return;
			}
			int slot = iTimes.getSlot(index);
			int count = iTimes.getNumItems();
			String text = iTimes.getItem(index).toAriaString();
			if (iInsertedSuggestion != null) {
				if (iInsertedSuggestion.getSlot() == slot) {
					setStatus(ARIA.onSuggestionNoCount(text));
					return;
				}
				if (iInsertedSuggestion.getSlot() < slot) {
					index --;
				}
				count--;
			}
			setStatus(ARIA.onSuggestion(index + 1, count, text));
		}
	}

	@Override
	public int getTabIndex() {
		return iText.getTabIndex();
	}

	@Override
	public void setAccessKey(char key) {
		iText.setAccessKey(key);
	}

	@Override
	public void setFocus(boolean focused) {
		iText.setFocus(focused);
	}

	@Override
	public void setTabIndex(int index) {
		iText.setTabIndex(index);
	}

	@Override
	public HandlerRegistration addKeyUpHandler(KeyUpHandler handler) {
		return iText.addKeyUpHandler(handler);
	}

	@Override
	public HandlerRegistration addKeyDownHandler(KeyDownHandler handler) {
		return iText.addKeyDownHandler(handler);
	}

	@Override
	public HandlerRegistration addKeyPressHandler(KeyPressHandler handler) {
		return iText.addKeyPressHandler(handler);
	}

	@Override
	public HandlerRegistration addFocusHandler(FocusHandler handler) {
		return iText.addFocusHandler(handler);
	}

	@Override
	public HandlerRegistration addBlurHandler(BlurHandler handler) {
		return iText.addBlurHandler(handler);
	}

	@Override
	public HandlerRegistration addSelectionHandler(SelectionHandler<Integer> handler) {
		return addHandler(handler, SelectionEvent.getType());
	}
	
	private void fireSuggestionEvent(Integer selectedTimeSlot) {
		SelectionEvent.fire(this, selectedTimeSlot);
	}
}