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

import ej.annotation.Nullable;
import ej.basictool.ArrayTools;
import ej.basictool.ThreadUtils;
import ej.bon.Constants;
import ej.microui.display.GraphicsContext;
import ej.mwt.style.Style;
import ej.mwt.util.Alignment;
import ej.mwt.util.OutlineHelper;
import ej.mwt.util.Rectangle;
import ej.trace.Tracer;

/**
 * A container is a widget that can contain other {@link Widget} instances, following the composite pattern.
 * <p>
 * The children are stored in a list. The order of the list defines the front-to-back stacking order of the widgets
 * within the container. The first widget in the list is at the back of the stacking order.
 * <p>
 * A widget cannot be added two times in a hierarchy.
 */
public abstract class Container extends Widget {

    /**
     * Creates a container.
     */
    protected Container() {
        this(false);
    }

    /**
     * Creates a container specifying if its enabled or not.
     *
     * @param enabled
     *            <code>true</code> if this container is to be enabled, <code>false</code> otherwise.
     */
    protected Container(boolean enabled) {
        super(enabled);
        this.children = EMPTY_WIDGET_ARRAY;
    }

    /**
     * Adds the specified widget to the end of the list of children of this container.
     * <p>
     * The container needs to be laid out again.
     * <p>
     * Should be called in the display thread to avoid concurrency issues.
     *
     * @param child
     *            the widget to add.
     * @throws IllegalArgumentException
     *             if the specified widget is already attached.
     * @see #requestLayOut()
     * @see #isAttached()
     */
    protected void addChild(Widget child) {
        if (child.parent != null) {
            throw new IllegalArgumentException();
        }
        // Update children array before notifying that the child is attached.
        this.children = ArrayTools.add(this.children, child);
        child.setParent(this, isAttached());
    }

    /**
     * Removes the specified widget from the list of children of this container.
     * <p>
     * The container needs to be laid out again.
     * <p>
     * If the widget is not in the list of children of the container, nothing is done.
     * <p>
     * Should be called in the display thread to avoid concurrency issues.
     *
     * @param child
     *            the widget to remove.
     * @see #requestLayOut()
     */
    protected void removeChild(Widget child) {
        Widget[] children = this.children;
        this.children = ArrayTools.remove(children, child);
        if (this.children.length != children.length) {
            child.resetParent();
        }
    }

    /**
     * Inserts the specified widget at the specified index in the list of children of this container.
     * <p>
     * The container needs to be laid out again.
     * <p>
     * Should be called in the display thread to avoid concurrency issues.
     *
     * @param child
     *            the widget to add.
     * @param index
     *            the index at which the widget should be inserted.
     * @throws IllegalArgumentException
     *             if the specified widget is already attached.
     * @throws IndexOutOfBoundsException
     *             if the index is out of range (<code>index &lt; 0 || index &gt; getChildrenCount()</code>).
     * @see #requestLayOut()
     * @see #isAttached()
     */
    protected void insertChild(Widget child, int index) {
        if (child.parent != null) {
            throw new IllegalArgumentException();
        }
        // Update children array before notifying that the child is attached.
        this.children = ArrayTools.insert(this.children, index, child);
        child.setParent(this, isAttached());
    }

    /**
     * Replaces the child at the specified index in the list of children of this container by the specified widget.
     * <p>
     * If the specified widget is already the child at the specified index of the list of children, nothing is done.
     * <p>
     * The container needs to be laid out again.
     * <p>
     * Should be called in the display thread to avoid concurrency issues.
     *
     * @param index
     *            the index of the child to replace.
     * @param child
     *            the widget to add.
     * @throws IndexOutOfBoundsException
     *             if the index is out of range (<code>index &lt; 0 || index &gt;= getChildrenCount()</code>).
     * @throws IllegalArgumentException
     *             if the specified widget is already attached, unless it is already the child at the specified index.
     * @see #requestLayOut()
     * @see #isAttached()
     */
    protected void replaceChild(int index, Widget child) {
        Widget[] children = this.children;
        Widget oldChild = children[index];
        if (child != oldChild) {
            if (child.parent != null) {
                throw new IllegalArgumentException();
            }
            oldChild.resetParent();
            // Update children array before notifying that the child is attached.
            children[index] = child;
            child.setParent(this, isAttached());
        }
    }

    /**
     * Removes all the children of this container.
     * <p>
     * The container needs to be laid out again.
     * <p>
     * Should be called in the display thread to avoid concurrency issues.
     *
     * @see #requestLayOut()
     */
    protected void removeAllChildren() {
        Widget[] children = this.children;
        for (Widget child : children) {
            child.resetParent();
        }
        this.children = EMPTY_WIDGET_ARRAY;
    }

    /**
     * Changes the position of the specified widget in the list of children of this container.
     *
     * @param child
     *            the child to move.
     * @param index
     *            the index at which the widget should be moved.
     * @throws IllegalArgumentException
     *             if the given widget is not a child of this container.
     */
    protected void changeChildIndex(Widget child, int index) {
        Widget[] children = this.children;
        int currentIndex = ArrayTools.getIndex(children, child);
        if (currentIndex == -1) {
            throw new IllegalArgumentException();
        }
        if (currentIndex != index) {
            if (index < currentIndex) {
                System.arraycopy(children, index, children, index + 1, currentIndex - index);
            } else {
                System.arraycopy(children, currentIndex + 1, children, currentIndex, index - currentIndex);
            }
            children[index] = child;
        }
    }

    /**
     * Automatically declares children as shown when this container is shown.
     * <p>
     * A container may decide to keep some children as hidden by subclassing this method. It is then responsible of
     * drawing only its shown children in {@link #renderContent(GraphicsContext, int, int)}.
     *
     * @see #onShown()
     */
    protected void setShownChildren() {
        for (Widget child : this.children) {
            child.setShown();
        }
    }

    /**
     * Automatically declares children as hidden when this container is hidden.
     * <p>
     * It is not necessary to override this method, except for optimization. If this method is overridden, it should
     * always make sure that every child is hidden before returning.
     *
     * @see #onHidden()
     */
    protected void setHiddenChildren() {
        for (Widget child : this.children) {
            child.setHidden();
        }
    }

    /**
     * Sets a child as shown.
     * <p>
     * An assertion checks that the given widget is actually a child of this container.
     *
     * @param child
     *            the child to notify.
     * @see #onShown()
     */
    protected void setShownChild(Widget child) {
        assert child.parent == this : notAChildMessage(this, child, "show");
        child.setShown();
    }

    /**
     * Sets a child as hidden.
     * <p>
     * An assertion checks that the given widget is actually a child of this container.
     *
     * @param child
     *            the child to notify.
     * @see #onHidden()
     */
    protected void setHiddenChild(Widget child) {
        assert child.parent == this : notAChildMessage(this, child, "hide");
        child.setHidden();
    }

    /**
     * Returns the child widget that is at the specified position.
     * <p>
     * If {@link Widget#contains(int, int)} is <code>false</code> for this container, <code>null</code> is returned.
     * Otherwise, if there is a child for which {@link Widget#contains(int, int)} returns <code>true</code> then the
     * result of invoking {@link Widget#getWidgetAt(int, int)} on that widget is returned. Otherwise this container is
     * returned.
     * <p>
     * The position is relative to the position of this container's parent.
     *
     * @param x
     *            x coordinate
     * @param y
     *            y coordinate
     * @return the child at the position, <code>null</code> if no child is found in this container hierarchy.
     */
    @Override
    @Nullable
    public Widget getWidgetAt(int x, int y) {
        // equivalent to super.getWidgetAt(x, y) == null
        if (!contains(x, y)) {
            return null;
        }
        int relX = x - this.x - this.contentX;
        int relY = y - this.y - this.contentY;
        // browse children recursively
        Widget[] children = this.children;
        for (int i = children.length - 1; i >= 0; i--) {
            Widget at = children[i].getWidgetAt(relX, relY);
            if (at != null) {
                return at;
            }
        }
        return this;
    }

    /**
     * Gets the widget at the specified index in this container.
     *
     * @param index
     *            the index of the widget to return.
     * @return the widget at the specified index in this container.
     * @throws IndexOutOfBoundsException
     *             if the index is out of range (<code>index &lt; 0 || index &gt;= getChildrenCount()</code>).
     */
    public Widget getChild(int index) {
        Widget child = this.children[index];
        assert child != null;
        return child;
    }

    /**
     * Gets the index of the specified widget in the list of children of this container.
     *
     * @param child
     *            the child.
     * @return the index of the given child.
     * @throws IllegalArgumentException
     *             if the specified widget is not a child of this container.
     */
    public int getChildIndex(Widget child) {
        Widget[] children = this.children;
        int length = children.length;
        for (int i = 0; i < length; i++) {
            if (child == children[i]) {
                return i;
            }
        }
        throw new IllegalArgumentException();
    }

    /**
     * Gets the list of children in this container.
     * <p>
     * Beware, the returned array is the field, it should not be modified.
     *
     * @return the list of children.
     */
    protected Widget[] getChildren() {
        // NOSONAR do not copy the array since it should be used as read-only.
        return this.children;
    }

    /**
     * Gets the number of children in this container.
     *
     * @return the number of children.
     */
    public int getChildrenCount() {
        return this.children.length;
    }

    /**
     * Computes the optimal size of a child of this container.
     * <p>
     * An assertion checks that the given widget is actually a child of this container.
     *
     * @param child
     *            the child.
     * @param availableWidth
     *            the width available for this child or {@link Widget#NO_CONSTRAINT}.
     * @param availableHeight
     *            the height available for this child or {@link Widget#NO_CONSTRAINT}.
     * @see Desktop#requestLayOut()
     * @see Widget#requestLayOut()
     */
    protected void computeChildOptimalSize(Widget child, int availableWidth, int availableHeight) {
        assert child.parent == this : notAChildMessage(this, child, "compute the optimal size");
        child.computeOptimalSize(availableWidth, availableHeight);
    }

    /**
     * Lays out the children of this container.
     * <p>
     * The given size is the size of this container minus the border, margin and padding specified in the style.
     * <p>
     * When this method returns the children of this container have been lay out using
     * {@link #layOutChild(Widget, int, int, int, int)}.
     *
     * @param contentWidth
     *            the width available for the content.
     * @param contentHeight
     *            the height available for the content.
     */
    protected abstract void layOutChildren(int contentWidth, int contentHeight);

    /**
     * Lays out a child of this container.
     * <p>
     * The bounds of a widget can only be set by its parent during the lay out.
     * <p>
     * An assertion checks that the given widget is actually a child of this container.
     *
     * @param child
     *            the child to set the bounds to.
     * @param x
     *            the x coordinate.
     * @param y
     *            the y coordinate.
     * @param width
     *            the width.
     * @param height
     *            the height.
     * @see Desktop#requestLayOut()
     * @see Widget#requestLayOut()
     */
    protected void layOutChild(Widget child, int x, int y, int width, int height) {
        assert child.parent == this : notAChildMessage(this, child, "lay out");
        child.layOut(x, y, width, height);
    }

    /**
     * Lays out a child of this container in a similar fashion to CSS absolute position.
     * <p>
     * The child is positioned relatively to this container using given alignment with no size constraint.
     * <p>
     * Examples:
     *
     * <pre>
     * layOutAlignedChild(child1, left, top, Alignment.LEFT, Alignment.TOP, contentWidth, contentHeight);
     * layOutAlignedChild(child2, 0, top, Alignment.HCENTER, Alignment.TOP, contentWidth, contentHeight);
     * layOutAlignedChild(child3, right, top, Alignment.RIGHT, Alignment.TOP, contentWidth, contentHeight);
     * layOutAlignedChild(child4, left, 0, Alignment.LEFT, Alignment.VCENTER, contentWidth, contentHeight);
     * layOutAlignedChild(child5, 0, 0, Alignment.HCENTER, Alignment.VCENTER, contentWidth, contentHeight);
     * layOutAlignedChild(child6, right, 0, Alignment.RIGHT, Alignment.VCENTER, contentWidth, contentHeight);
     * layOutAlignedChild(child7, left, bottom, Alignment.LEFT, Alignment.BOTTOM, contentWidth, contentHeight);
     * layOutAlignedChild(child8, 0, bottom, Alignment.HCENTER, Alignment.BOTTOM, contentWidth, contentHeight);
     * layOutAlignedChild(child9, right, bottom, Alignment.RIGHT, Alignment.BOTTOM, contentWidth, contentHeight);
     * </pre>
     *
     * <img src="doc-files/layoutaligned.png" alt="Layout aligned children.">
     *
     * <p>
     * An assertion checks that the given widget is actually a child of this container.
     *
     * @param child
     *            the child to set the bounds to.
     * @param horizontalPosition
     *            the right position if {@code horizontalAlignment} is {@link Alignment#RIGHT}, the left position if
     *            {@code horizontalAlignment} is {@link Alignment#LEFT}, ignored otherwise.
     * @param verticalPosition
     *            the top position if {@code verticalAlignment} is {@link Alignment#TOP}, the bottom position if
     *            {@code verticalAlignment} is {@link Alignment#BOTTOM}, ignored otherwise.
     * @param horizontalAlignment
     *            the horizontal alignment.
     * @param verticalAlignment
     *            the vertical alignment.
     * @param contentWidth
     *            this container content width.
     * @param contentHeight
     *            this container content height.
     * @see Desktop#requestLayOut()
     * @see Widget#requestLayOut()
     * @see Alignment
     */
    protected void layOutAlignedChild(Widget child, int horizontalPosition, int verticalPosition, int horizontalAlignment, int verticalAlignment, int contentWidth, int contentHeight) {
        int width = child.getWidth();
        int height = child.getHeight();
        int x = Alignment.computeLeftX(width, horizontalPosition, contentWidth - (horizontalPosition << 1), horizontalAlignment);
        int y = Alignment.computeTopY(height, verticalPosition, contentHeight - (verticalPosition << 1), verticalAlignment);
        layOutChild(child, x, y, width, height);
    }

    /**
     * Gets the content x of this container. That means the sum of the different outlines on the left.
     * <p>
     * The content bounds of a container are computed when the bounds of the container are set. (Otherwise, the content
     * bounds will be zeros.)
     *
     * @return the content x.
     */
    public int getContentX() {
        return this.contentX;
    }

    /**
     * Gets the content y of this container. That means the sum of the different outlines on the top.
     * <p>
     * The content bounds of a container are computed when the bounds of the container are set. (Otherwise, the content
     * bounds will be zeros.)
     *
     * @return the content y.
     */
    public int getContentY() {
        return this.contentY;
    }

    /**
     * Gets the content width of this container. That means the width minus the sum of the different outlines
     * horizontally (left and right).
     * <p>
     * The content bounds of a container are computed when the bounds of the container are set. (Otherwise, the content
     * bounds will be zeros.)
     *
     * @return the content width.
     */
    public int getContentWidth() {
        return this.contentWidth;
    }

    /**
     * Gets the content height of this container. That means the height minus the sum of the different outlines
     * vertically (top and bottom).
     * <p>
     * The content bounds of a container are computed when the bounds of the container are set. (Otherwise, the content
     * bounds will be zeros.)
     *
     * @return the content height.
     */
    public int getContentHeight() {
        return this.contentHeight;
    }

    /**
     * {@inheritDoc}
     * <p>
     * Renders the children of this container.
     */
    @Override
    protected void renderContent(GraphicsContext g, int contentWidth, int contentHeight) {
        int translateX = g.getTranslationX();
        int translateY = g.getTranslationY();
        int x = g.getClipX();
        int y = g.getClipY();
        int width = g.getClipWidth();
        int height = g.getClipHeight();
        Widget[] children = this.children;
        int childrenLength = children.length;
        for (int i = 0; i < childrenLength; i++) {
            Widget child = children[i];
            assert child != null;
            renderChild(child, g);
            if (i < childrenLength - 1) {
                // Don't need to reset after the last widget.
                g.setTranslation(translateX, translateY);
                g.setClip(x, y, width, height);
            }
        }
    }

    /**
     * Renders a child of this container.
     * <p>
     * The given graphics context is translated and clipped to the parent content area. The child is then responsible of
     * translating to its location and clipping to its size.
     * <p>
     * If the child is not shown, nothing is done.
     * <p>
     * An assertion checks that the given widget is actually a child of this container.
     *
     * @param child
     *            the child to render.
     * @param g
     *            the graphics context where to render the content of the child.
     */
    protected void renderChild(Widget child, GraphicsContext g) {
        assert child.parent == this : notAChildMessage(this, child, "render");
        child.paint(g);
    }

    /**
     * {@inheritDoc}
     * <p>
     * The style of each child of this container is also updated.
     */
    @Override
    public void updateStyle() {
        super.updateStyle();
        for (Widget child : this.children) {
            child.updateStyle();
        }
        if (Constants.getBoolean(Tracer.TRACE_ENABLED_CONSTANT_PROPERTY)) {
            Trace.TRACER.recordEventEnd(Trace.UPDATE_STYLE_EVENT, Trace.getWidgetId(this));
        }
    }

    @Override
    public Rectangle getContentBounds() {
        return new Rectangle(this.contentX, this.contentY, this.contentWidth, this.contentHeight);
    }
}
