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

	private static final Widget[] EMPTY_WIDGET_ARRAY = new Widget[0];

	/* package */char contentX;
	/* package */char contentY;
	/* package */char contentWidth;
	/* package */char contentHeight;

	private Widget[] children;

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

	/**
	 * {@inheritDoc}
	 * <p>
	 * Notifies its children widgets that they are attached.
	 */
	@Override
	/* package */void setAttached() {
		super.setAttached();
		for (Widget child : this.children) {
			child.setAttached();
		}
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * Notifies its children widgets that they are detached.
	 */
	@Override
	/* package */void setDetached() {
		super.setDetached();
		for (Widget child : this.children) {
			child.setDetached();
		}
	}

	@Override
	/* package */void setShown() {
		super.setShown();
		setShownChildren();
	}

	@Override
	/* package */void setHidden() {
		super.setHidden();
		setHiddenChildren();
	}

	/**
	 * 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() {
		return this.children; // NOSONAR do not copy the array since it should be used as read-only.
	}

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

	@Override
	/* package */void layOut(int x, int y, int width, int height) {
		super.layOut(x, y, width, height);

		// set content bounds
		Style style = this.style;
		Rectangle rectangle = getSharedRectangle(0, 0, getWidth(), getHeight());
		OutlineHelper.applyOutlines(rectangle, style);
		this.contentX = (char) rectangle.getX();
		this.contentY = (char) rectangle.getY();
		this.contentWidth = (char) rectangle.getWidth();
		this.contentHeight = (char) rectangle.getHeight();

		// lay out children
		try {
			layOutChildren(this.contentWidth, this.contentHeight);
		} catch (Exception e) {
			ThreadUtils.handleUncaughtException(e);
		}
		if (Constants.getBoolean(Tracer.TRACE_ENABLED_CONSTANT_PROPERTY)) {
			Trace.TRACER.recordEventEnd(Trace.LAYOUT_EVENT, Trace.getWidgetId(this));
		}
	}

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

	private static String notAChildMessage(Container container, Widget child, String action) {
		return "This container (" + printClassname(container) + ") cannot " + action + " the given widget ("
				+ printClassname(child) + ") since it is not its child.";
	}
}
