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.element.interfaces.GridElement;
import com.baselet.gwt.client.element.WebStorage;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.NativeEvent;
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.MouseWheelEvent;
import com.google.gwt.event.dom.client.MouseWheelHandler;
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 firstDrag, final boolean isMiddleMouseButton);

		void onMouseMove(Point point);

		void onMouseDragEnd(GridElement elementToDrag, Point point);

		void onMouseDownScheduleDeferred(GridElement elementToDrag, boolean controlKeyDown);

		void onDoubleClick(GridElement gridElementOnPosition);

		void onShowMenu(Point p);

		void onMouseWheelZoom(MouseWheelEvent event);

		Rectangle getVisibleBounds();

		int getAbsoluteLeft();

		int getAbsoluteTop();

		void setFocus(boolean b);

		GridElement getGridElementOnPosition(Point p);

		Element getElement();

		int getGridSize();

	}

	private static enum DragStatus {
		FIRST, CONTINUOUS, NO
	}

	public 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>();
		private boolean isMiddleMouseButton;

		public EventHandlingTarget getActivePanel() {
			return activePanel;
		}

		public void setActivePanel(EventHandlingTarget activePanel) {
			this.activePanel = activePanel;
			WebStorage.updateTargetPanel(activePanel);
		}

		/**
		 * 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)
	}

	private static DragCache storageInstance;

	public static DragCache getStorageInstance() {
		return storageInstance;
	}

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

		// Initializing active panel to be diagram panel
		for (EventHandlingTarget panel : panels) {
			if (panel instanceof DrawPanelDiagram) {
				storage.activePanel = panel;
				WebStorage.updateTargetPanel(panel);
			}
		}

		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)
					handleStart(panels, storage, handlerTarget, 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) {
				handleStart(panels, storage, handlerTarget, 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);
				}
			}
		}));

		storage.nonTouchHandlers.add(handlerTarget.addMouseWheelHandler(new MouseWheelHandler() {
			@Override
			public void onMouseWheel(MouseWheelEvent event) {
				if (storage.activePanel != null) {
					storage.activePanel.onMouseWheelZoom(event);
				}
				else {
					event.preventDefault();
					event.stopPropagation();
				}
			}
		}));

		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) {
		storage.activePanel = getPanelWhichContainsPoint(panels, getPointAbsolute(event));
		if (storage.activePanel == null) {
			return;
		}

		WebStorage.updateTargetPanel(storage.activePanel);

		Point p = getPoint(storage.activePanel, event);
		// 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);
		int usedButton = event.getNativeEvent().getButton();
		if (usedButton != NativeEvent.BUTTON_RIGHT) {
			storage.dragging = DragStatus.FIRST;
		}
		storage.isMiddleMouseButton = usedButton == NativeEvent.BUTTON_MIDDLE;
		if (!storage.isMiddleMouseButton) {
			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();

			if (panel instanceof DrawPanelPalette) {
				DrawPanelPalette currentPanel = ((DrawPanelPalette) panel);
				// If preview is displayed we need to use the correct grid size for calculating the diffs
				if (((DrawPanelDiagram) currentPanel.otherDrawFocusPanel).currentlyDisplayingPreview()) {
					diffX -= diffX % ((DrawPanelPalette) panel).otherDrawFocusPanel.getGridSize();
					diffY -= diffY % ((DrawPanelPalette) panel).otherDrawFocusPanel.getGridSize();
				}
				else {
					diffX -= diffX % panel.getGridSize();
					diffY -= diffY % panel.getGridSize();
				}
			}
			else {
				diffX -= diffX % panel.getGridSize();
				diffY -= diffY % panel.getGridSize();
			}
			if (diffX != 0 || diffY != 0) {
				panel.onMouseMoveDraggingScheduleDeferred(storage.moveStart, diffX, diffY, storage.elementToDrag, event.isShiftKeyDown(), event.isControlKeyDown(), storage.dragging == DragStatus.FIRST, storage.isMiddleMouseButton);
				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();
			// For some reason, MouseUpEvent does not always fire for same position as MouseDownEvent, therefore making
			// check of point inside panel more narrow
			visibleBounds.x += 1;
			visibleBounds.width -= 2;
			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);
		}
	}
}
