package com.baselet.gwt.client.view;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import com.baselet.control.basics.geom.Point;
import com.baselet.control.basics.geom.Rectangle;
import com.baselet.control.constants.SharedConstants;
import com.baselet.element.interfaces.GridElement;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.event.dom.client.ContextMenuEvent;
import com.google.gwt.event.dom.client.ContextMenuHandler;
import com.google.gwt.event.dom.client.DoubleClickEvent;
import com.google.gwt.event.dom.client.DoubleClickHandler;
import com.google.gwt.event.dom.client.HumanInputEvent;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseDownHandler;
import com.google.gwt.event.dom.client.MouseEvent;
import com.google.gwt.event.dom.client.MouseMoveEvent;
import com.google.gwt.event.dom.client.MouseMoveHandler;
import com.google.gwt.event.dom.client.MouseOutEvent;
import com.google.gwt.event.dom.client.MouseOutHandler;
import com.google.gwt.event.dom.client.MouseOverEvent;
import com.google.gwt.event.dom.client.MouseOverHandler;
import com.google.gwt.event.dom.client.MouseUpEvent;
import com.google.gwt.event.dom.client.MouseUpHandler;
import com.google.gwt.event.dom.client.TouchEndEvent;
import com.google.gwt.event.dom.client.TouchEndHandler;
import com.google.gwt.event.dom.client.TouchEvent;
import com.google.gwt.event.dom.client.TouchMoveEvent;
import com.google.gwt.event.dom.client.TouchMoveHandler;
import com.google.gwt.event.dom.client.TouchStartEvent;
import com.google.gwt.event.dom.client.TouchStartHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.ui.FocusPanel;

public class EventHandlingUtils {

	private static final List<DragStatus> DRAG_COMMANDS = Arrays.asList(DragStatus.FIRST, DragStatus.CONTINUOUS);

	public static interface EventHandlingTarget {

		HandlerRegistration addMouseOutHandler(MouseOutHandler mouseOutHandler);

		HandlerRegistration addMouseOverHandler(MouseOverHandler mouseOverHandler);

		void handleKeyDown(KeyDownEvent event);

		void handleKeyUp(KeyUpEvent event);

		void onMouseMoveDraggingScheduleDeferred(Point moveStart, int diffX, int diffY, GridElement elementToDrag, boolean shiftKeyDown, boolean controlKeyDown, boolean b);

		void onMouseMove(Point point);

		void onMouseDragEnd(GridElement elementToDrag, Point point);

		void onMouseDownScheduleDeferred(GridElement elementToDrag, boolean controlKeyDown);

		void onDoubleClick(GridElement gridElementOnPosition);

		void onShowMenu(Point p);

		Rectangle getVisibleBounds();

		int getAbsoluteLeft();

		int getAbsoluteTop();

		void setFocus(boolean b);

		GridElement getGridElementOnPosition(Point p);

		Element getElement();

	}

	private static enum DragStatus {
		FIRST, CONTINUOUS, NO
	}

	private static class DragCache {
		private DragStatus dragging = DragStatus.NO;
		private Point moveStart;
		private GridElement elementToDrag;
		private EventHandlingTarget activePanel;
		private EventHandlingTarget mouseContainingPanel;
		private List<HandlerRegistration> nonTouchHandlers = new ArrayList<HandlerRegistration>();
		/**
		 * doubleclicks are only handled if the mouse has moved into the canvas before
		 * this is necessary to void unwanted propagation of suggestbox-selections via doubleclick
		 * TODO: a better fix would be a custom SuggestDisplay which stops mouseevent propagation after handling them
		 */
		private boolean doubleClickEnabled = true;

		// private Timer menuShowTimer; //TODO doesn't really work at the moment (because some move and end events are not processed, therefore it's shown even if not wanted)
	}

	public static void addEventHandler(final FocusPanel handlerTarget, final EventHandlingTarget... panels) {
		final DragCache storage = new DragCache();

		for (final EventHandlingTarget panel : panels) {
			storage.nonTouchHandlers.add(panel.addMouseOutHandler(new MouseOutHandler() {
				@Override
				public void onMouseOut(MouseOutEvent event) {
					storage.mouseContainingPanel = null;
				}
			}));
			storage.nonTouchHandlers.add(panel.addMouseOverHandler(new MouseOverHandler() {
				@Override
				public void onMouseOver(MouseOverEvent event) {
					storage.mouseContainingPanel = panel;
				}
			}));
		}

		handlerTarget.addTouchStartHandler(new TouchStartHandler() {
			@Override
			public void onTouchStart(final TouchStartEvent event) {
				// some mouseevents are interfering with touch events (eg: mousemove is triggered on each touchdown event) therefore they are removed as soon as a touch event is detected
				if (storage.nonTouchHandlers != null) {
					for (HandlerRegistration h : storage.nonTouchHandlers) {
						h.removeHandler();
					}
					storage.nonTouchHandlers = null;
				}
				if (event.getTouches().length() == 1) { // only handle single finger touches (to allow zooming with 2 fingers)
					final Point absolutePos = getPointAbsolute(event);
					storage.activePanel = getPanelWhichContainsPoint(panels, absolutePos);
					if (storage.activePanel != null) {
						handleStart(panels, storage, handlerTarget, event, getPoint(storage.activePanel, event));
					}
					// storage.menuShowTimer = new Timer() {
					// @Override
					// public void run() {
					// handleShowMenu(storage.activePanel, absolutePos);
					// }
					// };
					// storage.menuShowTimer.schedule(1000);
				}
			}
		});

		handlerTarget.addTouchEndHandler(new TouchEndHandler() {
			@Override
			public void onTouchEnd(TouchEndEvent event) {
				// storage.menuShowTimer.cancel();
				if (storage.activePanel != null) {
					handleEnd(storage.activePanel, storage, event);
				}
			}
		});
		handlerTarget.addTouchMoveHandler(new TouchMoveHandler() {
			@Override
			public void onTouchMove(TouchMoveEvent event) {
				// storage.menuShowTimer.cancel();
				if (event.getTouches().length() == 1) { // only handle single finger touches (to allow zooming with 2 fingers)
					handleMove(storage.activePanel, storage, event);
				}
			}
		});

		storage.nonTouchHandlers.add(handlerTarget.addMouseDownHandler(new MouseDownHandler() {
			@Override
			public void onMouseDown(MouseDownEvent event) {
				storage.activePanel = getPanelWhichContainsPoint(panels, getPointAbsolute(event));
				if (storage.activePanel != null) {
					handleStart(panels, storage, handlerTarget, event, getPoint(storage.activePanel, event));
				}
			}
		}));

		storage.nonTouchHandlers.add(handlerTarget.addMouseUpHandler(new MouseUpHandler() {
			@Override
			public void onMouseUp(MouseUpEvent event) {
				if (storage.activePanel != null) {
					handleEnd(storage.activePanel, storage, event);
				}
			}
		}));
		storage.nonTouchHandlers.add(handlerTarget.addMouseOutHandler(new MouseOutHandler() {
			@Override
			public void onMouseOut(MouseOutEvent event) {
				if (storage.activePanel != null) {
					handleEnd(storage.activePanel, storage, event);
					storage.doubleClickEnabled = false;
				}
			}
		}));
		storage.nonTouchHandlers.add(handlerTarget.addMouseOverHandler(new MouseOverHandler() {
			@Override
			public void onMouseOver(MouseOverEvent event) {
				storage.doubleClickEnabled = true;
			}
		}));

		storage.nonTouchHandlers.add(handlerTarget.addMouseMoveHandler(new MouseMoveHandler() {
			@Override
			public void onMouseMove(MouseMoveEvent event) {
				handleMove(storage.activePanel, storage, event);
			}
		}));

		// double tap on mobile devices is not easy to implement because browser zoom on double-tap is not an event which can be canceled
		storage.nonTouchHandlers.add(handlerTarget.addDoubleClickHandler(new DoubleClickHandler() {
			@Override
			public void onDoubleClick(DoubleClickEvent event) {
				if (storage.activePanel != null) {
					if (storage.doubleClickEnabled) {
						handleDoubleClick(storage.activePanel, getPoint(storage.activePanel, event));
					}
				}
			}
		}));

		storage.nonTouchHandlers.add(handlerTarget.addDomHandler(new ContextMenuHandler() {
			@Override
			public void onContextMenu(ContextMenuEvent event) {
				if (storage.activePanel != null) {
					event.preventDefault(); // avoid default contextmenu
					event.stopPropagation();
					handleShowMenu(storage.activePanel, new Point(event.getNativeEvent().getClientX(), event.getNativeEvent().getClientY()));
				}
			}
		}, ContextMenuEvent.getType()));

		storage.nonTouchHandlers.add(handlerTarget.addKeyDownHandler(new KeyDownHandler() {
			@Override
			public void onKeyDown(KeyDownEvent event) {
				if (storage.activePanel != null) {
					storage.activePanel.handleKeyDown(event);
				}
			}
		}));
		storage.nonTouchHandlers.add(handlerTarget.addKeyUpHandler(new KeyUpHandler() {
			@Override
			public void onKeyUp(KeyUpEvent event) {
				if (storage.activePanel != null) {
					storage.activePanel.handleKeyUp(event);
				}
			}
		}));

		Scheduler.get().scheduleDeferred(new ScheduledCommand() {
			@Override
			public void execute() {
				handlerTarget.setFocus(true); // set focus to enable keyboard shortcuts

			}
		});
	}

	private static void handleEnd(EventHandlingTarget panel, final DragCache storage, HumanInputEvent<?> event) {
		// Notification.showInfo("UP");
		if (DRAG_COMMANDS.contains(storage.dragging)) {
			panel.onMouseDragEnd(storage.elementToDrag, getPoint(storage.activePanel, event));
		}
		storage.dragging = DragStatus.NO;
	}

	private static void handleStart(EventHandlingTarget[] panels, final DragCache storage, FocusPanel handlerTarget, HumanInputEvent<?> event, Point p) {
		// Notification.showInfo("DOWN " + p.x);
		handlerTarget.setFocus(true);

		event.preventDefault(); // necessary to avoid showing textcursor and selecting proppanel in chrome AND to avoid scrolling with touch move (problem is it also avoids scrolling with 2 fingers)
		storage.moveStart = new Point(p.x, p.y);
		storage.dragging = DragStatus.FIRST;
		storage.elementToDrag = storage.activePanel.getGridElementOnPosition(storage.moveStart);
		storage.activePanel.onMouseDownScheduleDeferred(storage.elementToDrag, event.isControlKeyDown());
	}

	private static void handleMove(final EventHandlingTarget panel, final DragCache storage, HumanInputEvent<?> event) {
		// Notification.showInfo("MOVE " + getPointAbsolute(event));
		if (storage.activePanel != null && DRAG_COMMANDS.contains(storage.dragging)) {
			Point p = getPoint(storage.activePanel, event);
			int diffX = p.x - storage.moveStart.getX();
			int diffY = p.y - storage.moveStart.getY();
			diffX -= diffX % SharedConstants.DEFAULT_GRID_SIZE;
			diffY -= diffY % SharedConstants.DEFAULT_GRID_SIZE;
			if (diffX != 0 || diffY != 0) {
				panel.onMouseMoveDraggingScheduleDeferred(storage.moveStart, diffX, diffY, storage.elementToDrag, event.isShiftKeyDown(), event.isControlKeyDown(), storage.dragging == DragStatus.FIRST);
				storage.dragging = DragStatus.CONTINUOUS; // after FIRST real drag switch to CONTINUOUS
				storage.moveStart = storage.moveStart.copy().move(diffX, diffY); // make copy because otherwise deferred action will act on wrong position
			}
		}
		else if (storage.mouseContainingPanel != null) {
			storage.mouseContainingPanel.onMouseMove(getPoint(storage.mouseContainingPanel, event));
		}
	}

	private static void handleDoubleClick(final EventHandlingTarget panel, Point p) {
		panel.onDoubleClick(panel.getGridElementOnPosition(p));
	}

	private static void handleShowMenu(final EventHandlingTarget panel, Point p) {
		panel.onShowMenu(p);
	}

	private static EventHandlingTarget getPanelWhichContainsPoint(EventHandlingTarget[] panels, Point p) {
		EventHandlingTarget returnPanel = null;
		for (EventHandlingTarget panel : panels) {
			Rectangle visibleBounds = panel.getVisibleBounds();
			visibleBounds.move(panel.getAbsoluteLeft(), panel.getAbsoluteTop());
			if (visibleBounds.contains(p)) {
				panel.setFocus(true);
				returnPanel = panel;
			}
			else {
				panel.setFocus(false);
			}
		}
		return returnPanel;
	}

	private static Point getPoint(EventHandlingTarget drawPanelCanvas, HumanInputEvent<?> event) {
		Element e = drawPanelCanvas.getElement();
		if (event instanceof MouseEvent<?>) {
			return new Point(((MouseEvent<?>) event).getRelativeX(e), ((MouseEvent<?>) event).getRelativeY(e));
		}
		else if (event instanceof TouchEndEvent) {
			return new Point(((TouchEvent<?>) event).getChangedTouches().get(0).getRelativeX(e), ((TouchEvent<?>) event).getChangedTouches().get(0).getRelativeY(e));
		}
		else if (event instanceof TouchEvent<?>) {
			return new Point(((TouchEvent<?>) event).getTouches().get(0).getRelativeX(e), ((TouchEvent<?>) event).getTouches().get(0).getRelativeY(e));
		}
		else {
			throw new RuntimeException("Unknown Event Type: " + event);
		}
	}

	private static Point getPointAbsolute(HumanInputEvent<?> event) {
		if (event instanceof MouseEvent<?>) {
			return new Point(((MouseEvent<?>) event).getClientX(), ((MouseEvent<?>) event).getClientY());
		}
		else if (event instanceof TouchEndEvent) {
			return new Point(((TouchEvent<?>) event).getChangedTouches().get(0).getPageX(), ((TouchEvent<?>) event).getChangedTouches().get(0).getPageY());
		}
		else if (event instanceof TouchEvent<?>) {
			return new Point(((TouchEvent<?>) event).getTouches().get(0).getPageX(), ((TouchEvent<?>) event).getTouches().get(0).getPageY());
		}
		else {
			throw new RuntimeException("Unknown Event Type: " + event);
		}
	}
}
