/*
 * 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 {

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

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

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

    /**
     * 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;
    }
    /**
     * Lays out all the hierarchy of this desktop.
     * <p>
     * The desktop must be attached before calling this method.
     */
}
