/*
 * 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.basictool.ArrayTools;
import ej.basictool.BitFieldHelper;
import ej.basictool.ThreadUtils;
import ej.bon.Constants;
import ej.microui.MicroUI;
import ej.microui.display.Font;
import ej.microui.display.GraphicsContext;
import ej.microui.display.Painter;
import ej.mwt.style.DefaultStyle;
import ej.mwt.style.Style;
import ej.mwt.style.background.Background;
import ej.mwt.style.dimension.Dimension;
import ej.mwt.style.outline.Outline;
import ej.mwt.util.OutlineHelper;
import ej.mwt.util.Rectangle;
import ej.mwt.util.Size;
import ej.trace.Tracer;

/**
 * Widget is the superclass of all the user interface objects.
 * <p>
 * There are a number of important concepts involving widgets:
 * <ul>
 * <li>Lay out.
 * <p>
 * Lay out is the process of positioning and setting the size of the widgets on a desktop.
 * <p>
 * This is performed by evaluating the optimal size of each widget and container in the hierarchy then, considering the
 * layout of the containers and the optimal size of their children, setting the position and actual size of all widgets.
 * <p>
 * The optimal size is the minimal size that allow to show correctly the content of a widget. For instance the optimal
 * size of a label that displays a string will be the size of the string with the font defined in the style.
 * <p>
 * The optimal size is computed by {@link #computeContentOptimalSize(Size)} method. On this size are applied the boxes
 * and dimension defined in the style, and the size of the widget is set. This size is then used by the parent container
 * to lay out (set position and size) the widget along with its siblings.
 * <p>
 * Whenever the state of a widget changes in a way that may affect its optimal size and the lay out of the desktop of
 * which it is a part then the hierarchy of the widget must be ask to perform a new lay out. This can be achieved by
 * invoking {@link #requestLayOut()} on one of the parents of the widget or the desktop depending on the lay out
 * modification.
 * <p>
 * An application will normally invoke {@link #requestLayOut()} after making a set of changes to widgets.
 * <li>Rendering.
 * <p>
 * Any widget can be asked to render itself by invoking {@link #requestRender()}. If a widget has children it will ask
 * them to render. If the widget is transparent it will cause the relevant area of its parent to be rendered. Note that
 * a render request does not trigger a new lay out, and the scope of the rendering that results from a call to
 * {@link #requestRender()} will never exceed the widget itself, its children (recursively), and, if it is transparent,
 * its parent (recursively if the parent is also transparent).</li>
 * </ul>
 * <p>
 * For heap optimization the position and size are stored as a <code>short</code> and therefore are limited between
 * <code>-32768</code> and <code>32767</code>.
 */
public abstract class Widget {

    /**
     * A width or height hint equal to <code>NO_CONSTRAINT</code> means that there is no constraint on this dimension.
     *
     * @see Widget#computeContentOptimalSize(Size)
     */
    public static final int NO_CONSTRAINT = 0;

    // Widget position and size.
    /**
     * The cached style.
     */
    /**
     * Creates a widget.
     * <p>
     * Once created, the widget is disabled. It may be enabled later by calling {@link #setEnabled(boolean)}. Enabled
     * widgets can handle events by overriding {@link #handleEvent(int)}.
     */
    protected Widget() {
        this(false);
    }

    /**
     * Creates a widget specifying if its enabled or not.
     * <p>
     * Enabled widgets can handle events by overriding {@link #handleEvent(int)}.
     *
     * @param enabled
     *            <code>true</code> if this widget is to be enabled, <code>false</code> otherwise.
     */
    protected Widget(boolean enabled) {
        // Detached and hidden by default.
        this.flags = (byte) BitFieldHelper.setBooleanProperty(0, enabled, ENABLED_MASK);
        this.classSelectors = EMPTY_INT_ARRAY;
        this.style = LAZY_DEFAULT_STYLE;
        if (Constants.getBoolean(Tracer.TRACE_ENABLED_CONSTANT_PROPERTY)) {
            Trace.createNewWidget(this);
        }
    }

    /**
     * Gets the x coordinate of this widget, relative to its parent.
     *
     * @return the x coordinate.
     */
    public int getX() {
        return this.x;
    }

    /**
     * Gets the y coordinate of this widget, relative to its parent.
     *
     * @return the y coordinate.
     */
    public int getY() {
        return this.y;
    }

    /**
     * Gets the width of this widget.
     *
     * @return the width.
     */
    public int getWidth() {
        return this.width;
    }

    /**
     * Gets the height of this widget.
     *
     * @return the height.
     */
    public int getHeight() {
        return this.height;
    }

    /**
     * Sets the position of this widget.
     *
     * @param x
     *            the x coordinate.
     * @param y
     *            the y coordinate.
     */
    public void setPosition(int x, int y) {
        this.x = (short) x;
        this.y = (short) y;
    }

    /**
     * Gets the content bounds of this widget (the bounds minus the outlines).
     *
     * @return the content bounds of this widget.
     */
    public Rectangle getContentBounds() {
        Style style = this.style;
        Rectangle contentBounds = new Rectangle(0, 0, this.width, this.height);
        OutlineHelper.applyOutlines(contentBounds, style);
        return contentBounds;
    }

    /**
     * This method is called as soon as the widget bounds are set.
     */
    protected void onLaidOut() {
        // Do nothing by default.
    }

    /**
     * This method is called as soon as:
     * <ul>
     * <li>the widget is attached to a desktop that is attached,</li>
     * <li>the desktop of the widget is attached.</li>
     * </ul>
     * <p>
     * After this call, the widget is ready to be rendered.
     * <p>
     * For example, the widget can allocate some resources useful to render it.
     *
     * @see Desktop#setAttached()
     */
    protected void onAttached() {
        // Do nothing by default.
    }

    /**
     * This method is called as soon as:
     * <ul>
     * <li>the widget is detached from a desktop that is attached,</li>
     * <li>the desktop of the widget is detached.</li>
     * </ul>
     * <p>
     * After this call, the resources allocated to render the widget must be disposed.
     *
     * @see Desktop#setDetached()
     */
    protected void onDetached() {
        // Do nothing by default.
    }

    /**
     * Gets whether this widget is attached or not.
     * <p>
     * A widget is considered as attached if it belongs to the hierarchy of an attached desktop.
     *
     * @return <code>true</code> if this widget is attached, <code>false</code> otherwise.
     */
    public boolean isAttached() {
        return BitFieldHelper.getBooleanProperty(this.flags, ATTACHED_MASK);
    }

    /**
     * This method is called as soon as the widget is visible on the display.
     * <p>
     * For example, this method can be used to start a task that refreshes the widget periodically.
     */
    protected void onShown() {
        // Do nothing by default.
    }

    /**
     * This method is called as soon as the widget is no more visible on the display.
     * <p>
     * After this call, all that has been allocated or started in {@link #onShown()} must be disposed or stopped.
     */
    protected void onHidden() {
        // Do nothing by default.
    }

    /**
     * Gets whether this widget is shown or not.
     * <p>
     * This information is set by the parent of the widget and used to know if the widget can be drawn.
     *
     * @return <code>true</code> if this widget is shown, <code>false</code> otherwise.
     * @see Container#setShownChildren()
     */
    public boolean isShown() {
        return BitFieldHelper.getBooleanProperty(this.flags, SHOWN_MASK);
    }

    /**
     * Gets the absolute x coordinate of the widget. That is, the x coordinate relative to the origin of the display.
     *
     * @return the absolute x coordinate of the widget.
     */
    public int getAbsoluteX() {
        int absoluteX = this.x;
        Container parent = getParent();
        while (parent != null) {
            absoluteX += parent.x + parent.contentX;
            parent = parent.getParent();
        }
        return absoluteX;
    }

    /**
     * Gets the absolute y coordinate of the widget. That is, the y coordinate relative to the origin of the display.
     *
     * @return the absolute y coordinate of the widget.
     */
    public int getAbsoluteY() {
        int absoluteY = this.y;
        Container parent = getParent();
        while (parent != null) {
            absoluteY += parent.y + parent.contentY;
            parent = parent.getParent();
        }
        return absoluteY;
    }

    /**
     * Tells whether or not this widget is transparent.
     * <p>
     * By default, a widget is transparent. A widget is considered as transparent if it does not draw every pixel of its
     * bounds with maximal opacity when it is rendered. If a widget is transparent, its parent (recursively if also
     * transparent) has to be rendered before the widget.
     *
     * @return <code>true</code> if this widget is transparent, <code>false</code> otherwise.
     * @see #contains(int, int)
     */
    public boolean isTransparent() {
        Style style = this.style;
        Rectangle bounds = getSharedRectangle(this.x, this.y, this.width, this.height);
        style.getMargin().apply(bounds);
        return style.getBackground().isTransparent(bounds.getWidth(), bounds.getHeight());
    }

    /**
     * Returns whether or not this widget contains the given widget in its hierarchy.
     * <p>
     * A widget contains an other widget if one of the children of the former contains the latter or if they reference
     * the same widget.
     *
     * @param widget
     *            the widget to check.
     * @return <code>true</code> if this widget contains the given widget, <code>false</code> otherwise.
     */
    public boolean containsWidget(Widget widget) {
        Widget ancestor = widget;
        do {
            if (ancestor == this) {
                return true;
            }
            ancestor = ancestor.getParent();
        } while (ancestor != null);
        return false;
    }

    /**
     * Gets whether a position (x,y) is in the widget's bounds.
     * <p>
     * The given position is considered here as a relative position to parent.
     * <p>
     * By default, the widget's bounds include the content, the padding & the border but not the margins.
     * <p>
     * Subclasses can override this method if the widget is not reactive to pointer events in its entire bounds. As long
     * as this method is used to dispatch pointer events, its implementation should be as fast as possible (for example
     * by simplifying the shape of the sensitive area).
     * <p>
     * If this method is overridden, it may be relevant to override the {@link #isTransparent()} method as well.
     *
     * @param x
     *            x coordinate.
     * @param y
     *            y coordinate.
     * @return {@code true} if the {@code (x,y)} position is in widget bounds, {@code false} otherwise.
     * @see ej.mwt.event.PointerEventDispatcher
     */
    public boolean contains(int x, int y) {
        Rectangle bounds = getSharedRectangle(this.x, this.y, this.width, this.height);
        Style style = this.style;
        style.getMargin().apply(bounds);
        int boundsX = bounds.getX();
        int boundsY = bounds.getY();
        return x >= boundsX && x < boundsX + bounds.getWidth() && y >= boundsY && y < boundsY + bounds.getHeight();
    }

    /**
     * Gets the widget at the specified position.
     * <p>
     * If this widget does not <code>contains(x, y)</code>, <code>null</code> is returned, else this widget is returned.
     * The position is considered here as a relative position to parent.
     *
     * @param x
     *            x coordinate.
     * @param y
     *            y coordinate.
     * @return this widget if it <code>contains(x, y)</code>, <code>null</code> otherwise.
     */
    @Nullable
    public Widget getWidgetAt(int x, int y) {
        if (contains(x, y)) {
            return this;
        }
        return null;
    }

    /**
     * Gets whether or not this widget is enabled.
     *
     * @return <code>true</code> if this widget is enabled, <code>false</code> otherwise.
     */
    public boolean isEnabled() {
        return BitFieldHelper.getBooleanProperty(this.flags, ENABLED_MASK);
    }

    /**
     * Sets this widget to be enabled or not.
     *
     * @param enabled
     *            <code>true</code> if this widget is to be enabled, <code>false</code> otherwise.
     */
    public void setEnabled(boolean enabled) {
        this.flags = (byte) BitFieldHelper.setBooleanProperty(this.flags, enabled, ENABLED_MASK);
    }

    /**
     * Requests a lay out of all the widgets in the sub hierarchy of this widget.
     * <p>
     * This method returns immediately and the layout of the widget is performed asynchronously in the MicroUI thread.
     * 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>
     * This method can only be called if this widget has been added to a desktop.
     * <p>
     * Nothing is done if the widget is not attached or if the widget has an empty size (width and height are equal to
     * <code>0</code>).
     * <p>
     * If the widget was not already shown, it is shown as soon as its bounds are set.
     * <p>
     * The style of all the widgets in the hierarchy is set (or updated) during this phase.
     * <p>
     * After the widget is laid out, it will be rendered.
     */
    public void requestLayOut() {
        if (!isAttached() || (this.width == 0 && this.height == 0)) {
            return;
        }
        if (Constants.getBoolean(Tracer.TRACE_ENABLED_CONSTANT_PROPERTY)) {
            Trace.TRACER.recordEvent(Trace.REQUEST_LAYOUT_EVENT, Trace.getWidgetId(this));
        }
        MicroUI.callSerially(new Runnable() {

            @Override
            public void run() {
                if (!isAttached()) {
                    return;
                }
                Widget widget = Widget.this;
                int width = widget.width;
                int height = widget.height;
                computeOptimalSize(width, height);
                layOut(widget.x, widget.y, width, height);
                setShown();
            }
        });
        // inline requestRender() without the isShown() check
        getDesktop().requestRender(this, 0, 0, this.width, this.height);
    }

    /**
     * Computes the optimal size of the widget.
     * <p>
     * This method does not consider the border, margin, padding and dimension specified in the style.
     * <p>
     * The given size is the available size for this widget in its parent. A width or a height equal to
     * <code>Widget#NO_CONSTRAINT</code> means that there is no constraint on this dimension.
     * <p>
     * The given size is modified to set the optimal size.
     *
     * @param size
     *            the size available for the content.
     */
    protected abstract void computeContentOptimalSize(Size size);

    /**
     * Requests a render of this entire widget on the display.
     * <p>
     * This method returns immediately and the rendering of the widget is performed asynchronously in the MicroUI
     * thread. To execute some code just after the render is done, this code could be wrapped in a {@link Runnable} and
     * executed asynchronously with {@link MicroUI#callSerially(Runnable)}.
     * <p>
     * If the widget is not shown, nothing is done.
     *
     * @see ej.mwt.render.RenderPolicy
     */
    public void requestRender() {
        requestRender(0, 0, this.width, this.height);
    }

    /**
     * Requests a render of a zone of this widget on the display.
     * <p>
     * This method returns immediately and the rendering of the widget is performed asynchronously in the MicroUI
     * thread. To execute some code just after the render is done, this code could be wrapped in a {@link Runnable} and
     * executed asynchronously with {@link MicroUI#callSerially(Runnable)}.
     * <p>
     * If the widget is not shown, nothing is done.
     * <p>
     * If the given area exceeds the bounds of the widget, only the intersection of the widget and the area will be
     * rendered.
     *
     * @param x
     *            the relative x coordinate of the area to render.
     * @param y
     *            the relative y coordinate of the area to render.
     * @param width
     *            the width of the area to render.
     * @param height
     *            the height of the area to render.
     * @see ej.mwt.render.RenderPolicy
     */
    public void requestRender(int x, int y, int width, int height) {
        if (isShown()) {
            getDesktop().requestRender(this, x, y, width, height);
        }
    }

    /**
     * Handles the given event. Does nothing by default and returns <code>false</code> (does not consume event).
     * <p>
     * Called by the desktop event manager.
     *
     * @param event
     *            the event to handle.
     * @return <code>true</code> if the widget has consumed the event, <code>false</code> otherwise.
     */
    public boolean handleEvent(int event) {
        return false;
    }

    /**
     * Renders the widget on the given graphics context.
     * <p>
     * The given graphics context is translated to the origin of the widget and clipped to the area to draw.
     * <p>
     * First, the different outlines defined in the style are applied, then, the content is rendered.
     *
     * @param g
     *            the graphics context to use to draw the widget.
     * @see OutlineHelper#applyOutlinesAndBackground(GraphicsContext, Size, Style)
     * @see #renderContent(GraphicsContext, int, int)
     */
    public void render(GraphicsContext g) {
        Style style = this.style;
        Size contentSize = new Size(this.width, this.height);
        OutlineHelper.applyOutlinesAndBackground(g, contentSize, style);
        try {
            renderContent(g, contentSize.getWidth(), contentSize.getHeight());
        } catch (Exception e) {
            ThreadUtils.handleUncaughtException(e);
        }
    }

    /**
     * Renders the content of the widget without the border, margin and padding specified in the style.
     * <p>
     * The given graphics context is translated and clipped according to the given bounds (the border, margin and
     * padding are applied on this graphics context before).
     *
     * @param g
     *            the graphics context where to render the content of the widget.
     * @param contentWidth
     *            the width of the content area.
     * @param contentHeight
     *            the height of the content area.
     */
    protected abstract void renderContent(GraphicsContext g, int contentWidth, int contentHeight);

    /**
     * Gets the parent of this widget or <code>null</code> if the widget is not in a hierarchy or if it is the root of
     * its hierarchy.
     *
     * @return the parent of this widget or <code>null</code>.
     */
    @Nullable
    public Container getParent() {
        Object parent = this.parent;
        if (parent instanceof Container) {
            return (Container) parent;
        } else {
            return null;
        }
    }

    /**
     * Gets the desktop to which this widget has been added.
     * <p>
     * This method can only be called if this widget has been added to a desktop.
     *
     * @return the desktop to which this widgets has been added.
     */
    public Desktop getDesktop() {
        Object parent = this.parent;
        assert (parent != null) : "This widget (" + printClassname(this) + ") is not attached to any desktop.";
        if (parent instanceof Desktop) {
            return (Desktop) parent;
        } else {
            return ((Container) parent).getDesktop();
        }
    }

    /**
     * Gets the current style of the widget.
     * <p>
     * This method should not be called before this widget is laid out.
     *
     * @return the current style of the widget.
     * @see ej.mwt.stylesheet.Stylesheet#getStyle(Widget)
     */
    public Style getStyle() {
        return this.style;
    }

    /**
     * Sets the style of the widget.
     *
     * @param newStyle
     *            the style.
     */
    public void setStyle(Style newStyle) {
        this.style = newStyle;
    }

    /**
     * Updates the style of this widget.
     * <p>
     * If the widget is not in a desktop, nothing is done.
     */
    public void updateStyle() {
        if (Constants.getBoolean(Tracer.TRACE_ENABLED_CONSTANT_PROPERTY)) {
            Trace.TRACER.recordEvent(Trace.UPDATE_STYLE_EVENT, Trace.getWidgetId(this));
        }
        Desktop desktop = getDesktopOrNull();
        if (desktop != null) {
            Style newStyle = desktop.getStylesheet().getStyle(this);
            setStyle(newStyle);
        }
        if (Constants.getBoolean(Tracer.TRACE_ENABLED_CONSTANT_PROPERTY)) {
            if (!(this instanceof Container)) {
                // NOSONAR do not merge ifs to let the constant optimisation.
                Trace.TRACER.recordEventEnd(Trace.UPDATE_STYLE_EVENT, Trace.getWidgetId(this));
            }
        }
    }

    /**
     * Gets whether or not the widget has the given class selector.
     *
     * @param classSelector
     *            the class selector to check.
     * @return <code>true</code> if the widget has the given class selector, <code>false</code> otherwise.
     */
    public boolean hasClassSelector(int classSelector) {
        return ArrayTools.getIndex(this.classSelectors, classSelector) != -1;
    }

    /**
     * Adds a class selector.
     *
     * @param classSelector
     *            the class selector to add.
     * @throws NullPointerException
     *             if the given class selector is <code>null</code>.
     */
    public void addClassSelector(int classSelector) {
        this.classSelectors = ArrayTools.add(this.classSelectors, classSelector);
    }

    /**
     * Removes a class selector.
     *
     * @param classSelector
     *            the class selector to remove.
     */
    public void removeClassSelector(int classSelector) {
        this.classSelectors = ArrayTools.remove(this.classSelectors, classSelector);
    }

    /**
     * Sets the class selectors.
     * <p>
     * If there is already some class selectors, they are removed.
     *
     * @param classSelectors
     *            the class selectors list to split.
     */
    public void setClassSelectors(int[] classSelectors) {
        this.classSelectors = classSelectors.clone();
    }

    /**
     * Removes all the class selectors.
     */
    public void removeAllClassSelectors() {
        this.classSelectors = EMPTY_INT_ARRAY;
    }

    /**
     * Gets whether or not the widget is in the given state.
     *
     * @param state
     *            the state to check.
     * @return <code>true</code> if the widget is in the given state, <code>false</code> otherwise.
     */
    public boolean isInState(int state) {
        return false;
    }
    /**
     * Lays out this widget.
     * <p>
     * The dimension in the style is applied to modify the given bounds before applying to the actual bounds.
     *
     * @param x
     *            the x coordinate.
     * @param y
     *            the y coordinate.
     * @param width
     *            the width.
     * @param height
     *            the height.
     */
    /**
     * Sets the given container or desktop as parent of this widget.
     * <p>
     * Does not check that the parent field is set or not.
     */
    /**
     * Resets widget's parent.
     */
    /**
     * @throws IllegalArgumentException
     *             if the widget is being attached whereas it is already attached.
     */
    /**
     * Computes the optimal size of this widget including the outlines defined in the style.
     * <p>
     * After this call the style is set and the optimal size will have been established.
     * <p>
     * The parameters define the maximum size available for this widget, or {@link Widget#NO_CONSTRAINT} if there is no
     * constraint.
     *
     * @param availableWidth
     *            the width available for this widget or {@link Widget#NO_CONSTRAINT}.
     * @param availableHeight
     *            the height available for this widget or {@link Widget#NO_CONSTRAINT}.
     */
    /**
     * Gets a unique instance of rectangle. It is shared to avoid creating new objects for small operations in the
     * MicroUI thread.
     *
     * @param x
     *            the x coordinate.
     * @param y
     *            the y coordinate.
     * @param width
     *            the width.
     * @param height
     *            the height.
     * @return the shared rectangle.
     */
}
