/*
 * Copyright 2015-2024 MicroEJ Corp. All rights reserved.
 * This library is provided in source code for use, modification and test, subject to license terms.
 * Any modification of the source code will break MicroEJ Corp. warranties on the whole library.
 */
package ej.mwt.event;

import ej.annotation.Nullable;
import ej.microui.event.Event;
import ej.microui.event.generator.Buttons;
import ej.microui.event.generator.Pointer;
import ej.mwt.Desktop;
import ej.mwt.Widget;

/**
 * Dispatches the pointer events received on a desktop to its children.
 * <p>
 * Pointer events are grouped in sessions. A session starts when the pointer is pressed, and ends when the pointer is
 * released.
 * <p>
 * Each event of the session is sent to the leaf widget that was under the pointer during the press event (see
 * {@link Desktop#getWidgetAt(int, int)}), then sent to all its parent hierarchy. Events are sent only to enabled
 * widgets (see {@link Widget#isEnabled()}).
 * <p>
 * Once a widget has consumed an event, it will be the only one to receive the next events during the session. All the
 * other widgets (ancestors or offsprings) receive an {@link #EXITED} event.
 * <p>
 * When the pointer exits the bounds of a widget in the hierarchy, this widget receives an {@link #EXITED} event and
 * won't receive any event from the session except if it has consumed an event.
 */
public class PointerEventDispatcher extends EventDispatcher {

	/**
	 * The "exited" action.
	 */
	public static final int EXITED = 0x00;

	@Nullable
	private DesktopEventGenerator eventGenerator;
	private int exitEvent;

	@Nullable
	private Widget pressedHierarchyLeaf;
	@Nullable
	private Widget consumerWidget;

	/**
	 * Creates a desktop event dispatcher.
	 *
	 * @param desktop
	 *            the desktop to dispatch in.
	 */
	public PointerEventDispatcher(Desktop desktop) {
		super(desktop);
	}

	@Override
	public void initialize() {
		super.initialize();
		DesktopEventGenerator eventGenerator = createEventGenerator();
		eventGenerator.addToSystemPool();
		this.exitEvent = eventGenerator.buildEvent(EXITED);
		this.eventGenerator = eventGenerator;
	}

	@Override
	public void dispose() {
		super.dispose();
		DesktopEventGenerator eventGenerator = this.eventGenerator;
		if (eventGenerator != null) {
			eventGenerator.removeFromSystemPool();
			this.eventGenerator = null;
			this.exitEvent = 0;
		}

		this.pressedHierarchyLeaf = null;
		this.consumerWidget = null;
	}

	/**
	 * Returns the leaf widget of the hierarchy which is subscribed to the ongoing pointer session.
	 * <p>
	 * When a pointer session is started, all the enabled widgets under the pointer become subscribed to this session.
	 * <p>
	 * The reference to this widget is initialized when the pointer is pressed ; it is updated when the pointer is
	 * dragged outside of the bounds of the widget or when a pointer event is consumed ; and it is reset when the
	 * pointer is released or when the pointer is dragged outside of the bounds of each subscribed widgets.
	 *
	 * @return the leaf widget of the subscribed hierarchy, or <code>null</code> if there is none.
	 */
	@Nullable
	public Widget getPressedHierarchyLeaf() {
		return this.pressedHierarchyLeaf;
	}

	/**
	 * Returns the widget which has consumed the ongoing pointer session.
	 * <p>
	 * When a pointer event is been consumed during a pointer session, it becomes the only widget to be subscribed to
	 * this session.
	 * <p>
	 * The reference to this widget is initialized when a pointer event is consumed ; and it is reset when the pointer
	 * is released or when the pointer is dragged outside of the bounds of the widget.
	 *
	 * @return the consumer widget, or <code>null</code> if there is none.
	 */
	@Nullable
	public Widget getConsumerWidget() {
		return this.consumerWidget;
	}

	/**
	 * Creates the event generator which is responsible for generating exit events to the widgets. By default, a
	 * {@link DesktopEventGenerator desktop event generator} is created.
	 *
	 * @return the event generator.
	 * @see DesktopEventGenerator
	 */
	protected DesktopEventGenerator createEventGenerator() {
		return new DesktopEventGenerator();
	}

	/**
	 * Gets the event generator of this dispatcher.
	 *
	 * @return the event generator of this dispatcher.
	 */
	@Nullable
	protected DesktopEventGenerator getEventGenerator() {
		return this.eventGenerator;
	}

	@Override
	public boolean dispatchEvent(int event) {
		// dispatch events (separate pointer and other events)
		int type = Event.getType(event);
		if (type == Pointer.EVENT_TYPE) {
			pointerEvent(event);
			return true;
		}
		return false;
	}

	/**
	 * Handles a pointer button event.
	 */
	private void pointerEvent(int event) {
		Pointer pointer = (Pointer) Event.getGenerator(event);
		int action = Buttons.getAction(event);

		switch (action) {
		case Buttons.PRESSED:
			pressed(event, pointer);
			break;
		case Pointer.DRAGGED:
			dragged(event, pointer);
			break;
		case Buttons.RELEASED:
			released(event, pointer);
			break;
		default:
			// ignore those events
		}
	}

	private void pressed(int event, Pointer pointer) {
		updatePointerPress(pointer);
		dispatchEvent(event, true, false);
	}

	private void dragged(int event, Pointer pointer) {
		updatePointerDrag(pointer);
		dispatchEvent(event, true, true);
	}

	private void released(int event, Pointer pointer) {
		updatePointerDrag(pointer);
		dispatchEvent(event, false, true);

		this.consumerWidget = null;
		this.pressedHierarchyLeaf = null;
	}

	private void updatePointerPress(Pointer pointer) {
		int x = pointer.getX();
		int y = pointer.getY();

		// search widget under the pointer
		Widget widget = getDesktop().getWidgetAt(x, y);

		// search the first enabled widget in the hierarchy
		while (widget != null && !widget.isEnabled()) {
			widget = widget.getParent();
		}

		// set pressed hierarchy leaf
		this.pressedHierarchyLeaf = widget;
		this.consumerWidget = null;
	}

	private void updatePointerDrag(Pointer pointer) {
		int x = pointer.getX();
		int y = pointer.getY();

		Widget oldPressedHierarchyLeaf = this.pressedHierarchyLeaf;
		if (oldPressedHierarchyLeaf == null) { // there is no point going further
			return;
		}

		// check that the pressed hierarchy is still attached to the desktop
		Desktop desktop = getDesktop();
		if (!desktop.containsWidget(oldPressedHierarchyLeaf)) {
			this.consumerWidget = null;
			this.pressedHierarchyLeaf = null;
			return;
		}

		// search leaf widget under the pointer
		Widget widget = desktop.getWidgetAt(x, y);

		// search the first widget in the hierarchy which intersects with the pressed hierarchy
		while (widget != null && (!widget.isEnabled() || !widget.containsWidget(oldPressedHierarchyLeaf))) {
			widget = widget.getParent();
		}

		// check whether the widget under the pointer is still the same
		if (widget == oldPressedHierarchyLeaf) {
			return;
		}

		Widget consumerWidget = this.consumerWidget;
		if (consumerWidget != null) { // press has been consumed
			// send exit to consumer widget (because the pointer is no longer over the consumer widget)
			sendEventToWidget(consumerWidget, this.exitEvent);
			this.pressedHierarchyLeaf = null;
		} else { // press has not been consumed
			// send exit to all ancestors of the old leaf except the ancestors of the new leaf
			sendEventToLimitedWidgetHierarchy(oldPressedHierarchyLeaf, widget, this.exitEvent);
			this.pressedHierarchyLeaf = widget;
		}
	}

	/**
	 * Dispatches the given event.
	 * <p>
	 * If there is already a consumer widget, the event is sent to this widget.
	 * <p>
	 * If there is no consumer widget, the event is sent to the pressed hierarchy. If a widget consumes the event, it
	 * becomes the consumed widget and exit events are sent to all the other widgets of the hierarchy.
	 *
	 * @param event
	 *            the event to dispatch.
	 * @param exitOffsprings
	 *            whether an exit event should be sent to the offsprings of the widget which consumes the event.
	 * @param exitAncestors
	 *            whether an exit event should be sent to the ancestors of the widget which consumes the event.
	 */
	private void dispatchEvent(int event, boolean exitOffsprings, boolean exitAncestors) {
		Widget consumerWidget = this.consumerWidget;
		if (consumerWidget != null) {
			sendEventToWidget(consumerWidget, event);
		} else {
			Widget pressedHierarchyLeaf = this.pressedHierarchyLeaf;
			if (pressedHierarchyLeaf != null) {
				consumerWidget = sendEventToWidgetHierarchy(pressedHierarchyLeaf, event);
				// Event generator can be null if the consumer widget has hidden the desktop (thus disposed this event
				// dispatcher).
				if (consumerWidget != null && isInitialized()) {
					dispatchExitEvent(exitOffsprings, exitAncestors, pressedHierarchyLeaf, consumerWidget);
					this.consumerWidget = consumerWidget;
					this.pressedHierarchyLeaf = consumerWidget;
				}
			}
		}
	}

	private void dispatchExitEvent(final boolean exitOffsprings, final boolean exitAncestors,
			final Widget pressedHierarchyLeaf, final Widget consumerWidget) {
		int exitEvent = this.exitEvent;
		if (exitOffsprings) {
			sendEventToLimitedWidgetHierarchy(pressedHierarchyLeaf, consumerWidget, exitEvent);
		}
		if (exitAncestors) {
			sendEventToLimitedWidgetHierarchy(consumerWidget.getParent(), null, exitEvent);
		}
	}

	/**
	 * Sends the given event to the widgets in the given old hierarchy unless they are also in the given new hierarchy.
	 * <p>
	 * Events are sent to all widgets even if one of them consumes the event.
	 *
	 * @param oldHierarchyLeaf
	 *            the leaf of the hierarchy to which the event should be sent.
	 * @param newHierarchyLeaf
	 *            the leaf of the hierarchy to which the event should NOT be sent (may be null).
	 * @param event
	 *            the event to send.
	 */
	private void sendEventToLimitedWidgetHierarchy(@Nullable Widget oldHierarchyLeaf, @Nullable Widget newHierarchyLeaf,
			int event) {
		Widget widget = oldHierarchyLeaf;
		// stop loop if the event dispatcher has been disposed during an event handling
		while (widget != null && isInitialized()) {
			if (widget == newHierarchyLeaf) {
				return;
			}

			// send event, ignore whether it is consumed
			sendEventToWidget(widget, event);

			// go to next ancestor
			widget = widget.getParent();
		}
	}

	/**
	 * Returns whether or not the given event is an exited event.
	 *
	 * @param event
	 *            the event to check.
	 * @return true if the given event is an exited event, false otherwise.
	 */
	public static boolean isExited(int event) {
		return (Event.getType(event) == DesktopEventGenerator.EVENT_TYPE
				&& DesktopEventGenerator.getAction(event) == EXITED);
	}
}
