/*
 * Copyright 2009-2025 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;

import ej.annotation.Nullable;
import ej.bon.Constants;
import ej.microui.MicroUI;
import ej.microui.display.Display;
import ej.microui.display.Displayable;
import ej.microui.display.GraphicsContext;
import ej.mwt.animation.Animator;
import ej.mwt.event.EventDispatcher;
import ej.mwt.event.PointerEventDispatcher;
import ej.mwt.render.DefaultRenderPolicy;
import ej.mwt.render.RenderPolicy;
import ej.mwt.stylesheet.Stylesheet;
import ej.mwt.stylesheet.VoidStylesheet;
import ej.trace.Tracer;

/**
 * A desktop is the top-level object that can be displayed on a {@link Display}.
 * <p>
 * It contains a widget. This widget can be a container to have a more elaborate hierarchy of widgets.
 * <p>
 * A desktop may be shown or hidden, but at most one desktop is shown per {@link Display}.
 * <p>
 * A desktop provides an animator, which can be used by the widgets to start and stop animations. When the desktop is
 * hidden, every animation is automatically stopped.
 * <p>
 * A desktop provides a stylesheet, which is used by the widgets of the desktop in order to retrieve their style. By
 * default, the stylesheet is a {@link VoidStylesheet}, but this can be changed at any time.
 *
 * @see Display
 */
public class Desktop extends Displayable {

	private final Animator animator;

	@Nullable
	private EventDispatcher eventDispatcher;
	@Nullable
	private RenderPolicy renderPolicy;

	// widget shown on the desktop
	@Nullable
	private Widget widget;

	private Stylesheet stylesheet;

	private boolean attached;

	/**
	 * Creates a new desktop.
	 */
	public Desktop() {
		this.animator = new Animator();
		this.stylesheet = new VoidStylesheet();
		if (Constants.getBoolean(Tracer.TRACE_ENABLED_CONSTANT_PROPERTY)) {
			Trace.createNewDesktop(this);
		}
	}

	/**
	 * Creates the event dispatcher which is responsible for dispatching events to the widgets. By default, a
	 * {@link PointerEventDispatcher pointer event dispatcher} is created.
	 *
	 * @return the event dispatcher.
	 * @see PointerEventDispatcher
	 */
	protected EventDispatcher createEventDispatcher() {
		return new PointerEventDispatcher(this);
	}

	/**
	 * Gets the event dispatcher of this desktop.
	 *
	 * @return the event dispatcher of this desktop.
	 */
	@Nullable
	public EventDispatcher getEventDispatcher() {
		return this.eventDispatcher;
	}

	/**
	 * Creates the render policy which is responsible for rendering the widgets. By default, a
	 * {@link DefaultRenderPolicy default render policy} is created.
	 *
	 * @return the render policy.
	 * @see DefaultRenderPolicy
	 */
	protected RenderPolicy createRenderPolicy() {
		return new DefaultRenderPolicy(this);
	}

	/* package */void requestRender(Widget widget, int x, int y, int width, int height) {
		if (Constants.getBoolean(Animator.DEBUG_ANIMATOR_ENABLED_CONSTANT)) {
			this.animator.indicateRenderRequested();
		}
		if (Constants.getBoolean(Tracer.TRACE_ENABLED_CONSTANT_PROPERTY)) {
			Trace.TRACER.recordEvent(Trace.REQUEST_RENDER_EVENT, Trace.getWidgetId(widget), x, y, width, height);
		}

		RenderPolicy renderPolicy = this.renderPolicy;
		assert (renderPolicy != null);
		renderPolicy.requestRender(widget, x, y, width, height);
	}

	/**
	 * Attaches a widget to this desktop.
	 * <p>
	 * If there is already a widget on this desktop, the former is detached from the latter.
	 * <p>
	 * If the specified widget is <code>null</code>, the desktop does not hold a widget anymore.
	 * <p>
	 * Should be called in the display thread to avoid concurrency issues.
	 *
	 * @param widget
	 *            the widget.
	 * @throws IllegalArgumentException
	 *             if the specified widget is already attached.
	 */
	public void setWidget(@Nullable Widget widget) {
		Widget previousWidget = this.widget;
		if (previousWidget != null) {
			previousWidget.resetParent();
		}

		if (widget != null) {
			if (widget.parent != null) {
				throw new IllegalArgumentException();
			}
			widget.setParent(this, isShown());
		}

		this.widget = widget;
	}

	/**
	 * Gets the widget attached to this desktop.
	 *
	 * @return the widget attached with this desktop or <code>null</code>.
	 */
	@Nullable
	public Widget getWidget() {
		return this.widget;
	}

	/**
	 * Requests a lay out of all the hierarchy of this desktop.
	 * <p>
	 * The layout is done asynchronously, therefore this method returns immediately. To execute some code just after the
	 * layout is done, this code could be wrapped in a {@link Runnable} and executed asynchronously with
	 * {@link MicroUI#callSerially(Runnable)}.
	 * <p>
	 * Nothing is done if the desktop is not shown.
	 * <p>
	 * Notifies its child widget that it is shown.
	 */
	public void requestLayOut() {
		if (Constants.getBoolean(Tracer.TRACE_ENABLED_CONSTANT_PROPERTY)) {
			Trace.TRACER.recordEvent(Trace.REQUEST_DESKTOP_LAYOUT_EVENT, Trace.getDesktopId(this));
		}
		if (isShown()) {
			MicroUI.callSerially(new Runnable() {
				@Override
				public void run() {
					if (isShown()) {
						layOut();
						showWidget();
					}
				}
			});
			requestRender();
		}
	}

	@Override
	public void requestRender() {
		if (Constants.getBoolean(Animator.DEBUG_ANIMATOR_ENABLED_CONSTANT)) {
			this.animator.indicateRenderRequested();
		}
		if (Constants.getBoolean(Tracer.TRACE_ENABLED_CONSTANT_PROPERTY)) {
			Trace.TRACER.recordEvent(Trace.REQUEST_DESKTOP_RENDER_EVENT, Trace.getDesktopId(this));
		}

		RenderPolicy renderPolicy = this.renderPolicy;
		assert (renderPolicy != null);
		Display display = Display.getDisplay();
		int width = display.getWidth();
		int height = display.getHeight();
		Widget widget = this.widget;
		if (widget != null) {
			renderPolicy.requestRender(widget, 0, 0, width, height);
		}
	}

	/**
	 * Lays out all the hierarchy of this desktop.
	 * <p>
	 * The desktop must be attached before calling this method.
	 */
	/* package */void layOut() {
		assert this.attached : "This desktop cannot be laid out since it is not attached.";
		Widget widget = this.widget;
		if (widget != null) {
			Display display = Display.getDisplay();
			int width = display.getWidth();
			int height = display.getHeight();
			widget.computeOptimalSize(width, height);
			widget.layOut(0, 0, width, height);
		}
	}

	/**
	 * Checks whether the desktop is visible on the display.
	 * <p>
	 * Calling this method is equivalent to calling <code>Display.getDisplay().isShown(desktop);</code>.
	 *
	 * @return <code>true</code> if the desktop is currently visible, <code>false</code> otherwise.
	 */
	public boolean isShown() {
		return MicroUI.isStarted() && Display.getDisplay().isShown(this);
	}

	/**
	 * Shows the desktop on the display.
	 * <p>
	 * Calling this method is equivalent to calling <code>Display.getDisplay().requestShow(desktop);</code>.
	 * <p>
	 * This method returns immediately and the desktop is shown asynchronously in the MicroUI thread. To execute some
	 * code just after the show is done, this code could be wrapped in a {@link Runnable} and executed asynchronously
	 * with {@link MicroUI#callSerially(Runnable)}.
	 * 
	 * @throws SecurityException
	 *             if a security manager exists and does not allow the caller to get the display.
	 */
	public void requestShow() {
		if (Constants.getBoolean(Tracer.TRACE_ENABLED_CONSTANT_PROPERTY)) {
			Trace.TRACER.recordEvent(Trace.REQUEST_DESKTOP_SHOW_EVENT, Trace.getDesktopId(this));
		}
		Display.getDisplay().requestShow(this);
	}

	/**
	 * Hides the desktop from the display.
	 * <p>
	 * Calling this method is equivalent to calling <code>Display.getDisplay().requestHide(desktop);</code>.
	 * <p>
	 * This method returns immediately and the desktop is hidden asynchronously in the MicroUI thread. To execute some
	 * code just after the hide is done, this code could be wrapped in a {@link Runnable} and executed asynchronously
	 * with {@link MicroUI#callSerially(Runnable)}.
	 */
	public void requestHide() {
		Display.getDisplay().requestHide(this);
	}

	/**
	 * This method is called by the system as soon as the desktop is shown on the display.
	 * <ul>
	 * <li>The event dispatcher is created,</li>
	 * <li>the desktop is attached (along with its children),</li>
	 * <li>its children are shown.</li>
	 * </ul>
	 *
	 * @see #setAttached()
	 */
	@Override
	protected void onShown() {
		if (Constants.getBoolean(Tracer.TRACE_ENABLED_CONSTANT_PROPERTY)) {
			Trace.TRACER.recordEvent(Trace.ON_SHOWN_DESKTOP_EVENT, Trace.getDesktopId(this));
		}
		EventDispatcher eventDispatcher = createEventDispatcher();
		eventDispatcher.initialize();
		this.eventDispatcher = eventDispatcher;
		this.renderPolicy = createRenderPolicy();

		setAttached();
		showWidget();
	}

	/**
	 * This method is called by the system as soon as the desktop is hidden from the display.
	 * <ul>
	 * <li>The event dispatcher is disposed,</li>
	 * <li>the desktop is detached (along with its widget hierarchy).</li>
	 * </ul>
	 *
	 * @see #setDetached()
	 */
	@Override
	protected void onHidden() {
		if (Constants.getBoolean(Tracer.TRACE_ENABLED_CONSTANT_PROPERTY)) {
			Trace.TRACER.recordEvent(Trace.ON_HIDDEN_DESKTOP_EVENT, Trace.getDesktopId(this));
		}
		setDetached();

		EventDispatcher eventDispatcher = this.eventDispatcher;
		if (eventDispatcher != null) {
			eventDispatcher.dispose();
			this.eventDispatcher = null;
		}

		this.animator.stopAllAnimations();
	}

	private void showWidget() {
		Widget widget = this.widget;
		if (widget != null) {
			widget.setShown();
		}
	}

	/**
	 * Sets this desktop as attached.
	 * <p>
	 * Every widget in its hierarchy is attached and a layout is performed.
	 *
	 * @see #isAttached()
	 * @see Widget#onAttached()
	 */
	public void setAttached() {
		if (!this.attached) {
			this.attached = true;

			Widget widget = this.widget;
			if (widget != null) {
				widget.setAttached();
				widget.updateStyle();
			}

			layOut();
		}
	}

	/**
	 * Sets this desktop as detached.
	 * <p>
	 * Every widget in its hierarchy is detached.
	 *
	 * @see #isAttached()
	 * @see Widget#onDetached()
	 */
	public void setDetached() {
		this.attached = false;
		Widget widget = this.widget;
		if (widget != null) {
			widget.setDetached();
		}
	}

	/**
	 * Gets whether this desktop is attached or not.
	 * <p>
	 * When the desktop is attached, that means that it is ready to be displayed (shown on a display or drawn in an
	 * image).
	 *
	 * @return <code>true</code> if this desktop is attached, <code>false</code> otherwise.
	 */
	public boolean isAttached() {
		return this.attached;
	}

	/**
	 * Handles an event by delegating it to the event dispatcher.
	 *
	 * @param event
	 *            the event to handle.
	 * @return <code>true</code> if the event is consumed, <code>false</code> otherwise.
	 */
	@Override
	public boolean handleEvent(int event) {
		EventDispatcher eventDispatcher = this.eventDispatcher;
		if (eventDispatcher != null) {
			return eventDispatcher.dispatchEvent(event);
		} else {
			return false;
		}
	}

	/**
	 * Renders a widget.
	 * <p>
	 * Beware that even an hidden child can be rendered by calling this method.
	 * <p>
	 * An assertion checks that the given widget is actually a child of this desktop.
	 *
	 * @param g
	 *            the graphics context to use to draw the widget.
	 * @param widget
	 *            the widget to render.
	 */
	public void renderWidget(GraphicsContext g, Widget widget) {
		assert containsWidget(widget) : "This desktop cannot render the given widget (" + Widget.printClassname(widget)
				+ ") since it is not its child.";
		widget.paint(g);
	}

	/**
	 * The desktop is rendered using the given graphics context.
	 * <p>
	 * Renders its child widget entirely.
	 *
	 * @param g
	 *            the graphics context of the display.
	 */
	@Override
	protected void render(GraphicsContext g) {
		RenderPolicy renderPolicy = this.renderPolicy;
		assert (renderPolicy != null);
		renderPolicy.renderDesktop();
	}

	/**
	 * Returns the child widget at the specified position.
	 * <p>
	 * If this desktop does not <code>contains(x, y)</code>, <code>null</code> is returned. The position is considered
	 * here as a relative position to the desktop.
	 *
	 * @param x
	 *            x coordinate.
	 * @param y
	 *            y coordinate.
	 * @return the widget at the position, <code>null</code> if no widget is found in this desktop hierarchy.
	 * @see Widget#getWidgetAt(int, int)
	 */
	@Nullable
	public Widget getWidgetAt(int x, int y) {
		Widget widget = this.widget;
		if (widget != null) {
			return widget.getWidgetAt(x, y);
		}
		return null;
	}

	/**
	 * Returns whether or not this desktop contains the given widget.
	 *
	 * @param widget
	 *            the widget to check.
	 * @return <code>true</code> if this desktop contains the given widget, <code>false</code> otherwise.
	 * @see Widget#containsWidget(Widget)
	 */
	public boolean containsWidget(Widget widget) {
		return (this.widget != null && this.widget.containsWidget(widget));
	}

	/**
	 * Gets the animator instance.
	 *
	 * @return the animator instance.
	 */
	public Animator getAnimator() {
		return this.animator;
	}

	/**
	 * Sets the stylesheet instance.
	 *
	 * @param stylesheet
	 *            the stylesheet instance.
	 */
	public void setStylesheet(Stylesheet stylesheet) {
		this.stylesheet = stylesheet;
	}

	/**
	 * Gets the stylesheet instance.
	 *
	 * @return the stylesheet instance.
	 */
	public Stylesheet getStylesheet() {
		return this.stylesheet;
	}
}
