/*
 * Copyright 2023-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.widget.container;

import ej.basictool.ArrayTools;
import ej.mwt.Container;
import ej.mwt.Widget;
import ej.mwt.util.Size;

/**
 * Lays out child elements in a flexible manner by arranging them along a main axis and a cross axis.
 * <p>
 * The flex layout manager allows you to create flexible and responsive layouts for containers. It arranges child
 * elements individually along the main axis, following the specified direction and justify. Additionally, you can
 * control the alignment of the items along the cross axis. Child elements are positioned based on the order they are
 * added to the layout.
 * <p>
 * Example of a flex layout with a row horizontal direction, centered along the main axis, and stretched to fill the
 * available height along the cross axis:
 *
 * <pre>
 * Flex flex = new Flex(Direction.ROW, Justify.CENTER, Align.STRETCH);
 * </pre>
 *
 * @see Flex#Flex(Direction, Justify, Align)
 */
public class Flex extends Container {

	/**
	 * Enum representing the possible directions for computing a flex layout.
	 */
	public enum Direction {
		/** The main axis runs from left to right, and the cross axis runs from top to bottom. */
		ROW,
		/** The main axis runs from right to left, and the cross axis runs from top to bottom. */
		ROW_REVERSE,
		/** The main axis runs from top to bottom, and the cross axis runs from left to right. */
		COLUMN,
		/** The main axis runs from bottom to top, and the cross axis runs from left to right. */
		COLUMN_REVERSE
	}

	/**
	 * Enum representing the possible values for justifying the widget's start position on the cross axis.
	 */
	public enum Justify {
		/** Lays out the elements at the start of the main axis container. */
		START,
		/** Lays out the elements at the center of the main axis container. */
		CENTER,
		/** Lays out the elements at the end of the main axis container. */
		END,
		/** Lays out the elements across the available space on the main axis. */
		SPACE_BETWEEN,
		/**
		 * Lays out the elements across the available space on the main axis, adding space before the first child and
		 * after the last one.
		 */
		SPACE_AROUND
	}

	/**
	 * Enum representing the possible values for aligning the row's start position on the cross axis.
	 */
	public enum Align {
		/** Stretches the widget cross size to fill all the remaining space. */
		STRETCH,
		/** Lays out the children based on the start of the container. */
		START,
		/** Lays out the children based on the center alignment on the cross axis. */
		CENTER,
		/** Lays out the children based on the end of the container. */
		END
	}

	private Direction direction;
	private Justify justify;
	private Align align;

	private FlexRow[] rows;

	/**
	 * Creates a flex container specifying its layout parameters.
	 *
	 * @param direction
	 *            the main axis direction for the layout (e.g., Direction.ROW or Flex.Direction.COLUMN).
	 * @param justify
	 *            the alignment of items along the main axis (e.g., Justify.START, Justify.CENTER or Justify.END).
	 * @param alignment
	 *            the alignment of items along the cross axis (e.g., Align.STRETCH (default), Align.CENTER, Align.END).
	 */
	public Flex(Direction direction, Justify justify, Align alignment) {
		this.direction = direction;
		this.justify = justify;
		this.align = alignment;
		this.rows = new FlexRow[0];
	}

	@Override
	protected void computeContentOptimalSize(Size size) {
		// Reset the rows before computing it again.
		this.rows = new FlexRow[0];
		int availableWidth = size.getWidth();
		int availableHeight = size.getHeight();

		int mainAxisSize = getMainSize(availableWidth, availableHeight);
		int crossAxisSize = getCrossSize(availableWidth, availableHeight);

		// update the widget's justify and alignment attributes based on size constraints.
		if (mainAxisSize == Widget.NO_CONSTRAINT) {
			setJustify(Justify.START);
		} else if (crossAxisSize == Widget.NO_CONSTRAINT) {
			setAlign(Align.START);
		}

		int offset = 0;
		while (offset < super.getChildrenCount()) {
			computeRowSize(super.getChildren(), mainAxisSize, crossAxisSize, offset);

			// update offset with last row end offset
			offset = this.rows[this.rows.length - 1].getEndOffset();
		}

		int totalMainSize = getRowTotalMainSize();
		int totalCrossSize = isStretch() ? crossAxisSize : getRowTotalCrossSize();

		// set container optimal size
		if (isRow()) {
			size.setSize(totalMainSize, totalCrossSize);
		} else {
			size.setSize(totalCrossSize, totalMainSize);
		}
	}

	/**
	 * Computes the size of the current row, handling main and cross-axis information, and potential overflow.
	 *
	 * @param widgets
	 *            the widgets to lay out.
	 * @param mainSize
	 *            content size on the main axis.
	 * @param crossSize
	 *            content size on the cross axis.
	 */
	private void computeRowSize(Widget[] widgets, int mainSize, int crossSize, int offset) {
		int mainAxisTotal = 0;
		int crossAxisMax = 0;

		while (offset < widgets.length) {
			Widget child = widgets[offset];
			assert (child != null);

			computeChildOptimalSize(child, mainSize, crossSize);
			int mainAxisChildLength = getMainSize(child.getWidth(), child.getHeight());
			int otherAxisChildLength = getCrossSize(child.getWidth(), child.getHeight());

			// check overflow
			if (mainSize != Widget.NO_CONSTRAINT && mainAxisTotal + mainAxisChildLength > mainSize) {
				break;
			}

			// add child to row
			mainAxisTotal += mainAxisChildLength;
			crossAxisMax = Math.max(otherAxisChildLength, crossAxisMax);

			offset++;
		}
		// Save the new row info
		this.rows = ArrayTools.add(this.rows, new FlexRow(offset, mainAxisTotal, crossAxisMax));
	}

	@Override
	protected void layOutChildren(int contentWidth, int contentHeight) {
		int[] rowCrossPosition = new int[2];
		rowCrossPosition[0] = 0; // row start
		rowCrossPosition[1] = 0; // row end

		int offset = 0;
		for (int rowIndex = 0; rowIndex < this.rows.length; rowIndex++) {
			layoutRow(super.getChildren(), this.rows, getMainSize(contentWidth, contentHeight),
					getCrossSize(contentWidth, contentHeight), rowCrossPosition, rowIndex, offset);

			// update offset and row start for next row iteration
			offset = this.rows[rowIndex].getEndOffset();
		}
	}

	private void layoutRow(Widget[] children, FlexRow[] rows, int mainSize, int crossSize, int[] rowCrossPosition,
			int rowIndex, int offset) {
		FlexRow row = rows[rowIndex];

		int endOffset = row.getEndOffset();
		int numberOfChildrenInRow = endOffset - offset;

		// calculate justify content
		int remainingSpace = mainSize - row.getMainRowSize();
		float justifyStart = computeJustifyStart(remainingSpace, numberOfChildrenInRow);

		int crossRowSize = calculateRowStretch(crossSize, row);
		rowCrossPosition[1] = rowCrossPosition[0] + crossRowSize; // current row end

		for (int i = offset; i < endOffset; i++) {
			Widget child = children[i];
			int childWidth = child.getWidth();
			int childHeight = child.getHeight();

			// calculate align content
			int alignStart = computeAlignment(getCrossSize(childWidth, childHeight), crossRowSize, rowCrossPosition,
					crossSize);

			layOutFlexChild(child, mainSize, justifyStart, alignStart, rowCrossPosition[1]);

			justifyStart += getMainSize(childWidth, childHeight);
			justifyStart += getSpaceBetweenWidgets(remainingSpace, numberOfChildrenInRow);
		}
		rowCrossPosition[0] = rowCrossPosition[1]; // next row start = current row end
	}

	/**
	 * Arranges a child widget based on the current flex layout parameters, considering alignment and direction.
	 *
	 * @param childWidget
	 *            the widget to arrange.
	 * @param mainContentSize
	 *            available size on the main axis.
	 * @param widgetStart
	 *            start position on the main axis for justification.
	 * @param rowStart
	 *            start position on the cross axis for alignment.
	 * @param rowEnd
	 *            available size on the cross axis.
	 */
	private void layOutFlexChild(Widget childWidget, int mainContentSize, double widgetStart, int rowStart,
			int rowEnd) {

		int childMainSize = getMainSize(childWidget.getWidth(), childWidget.getHeight());
		int childCrossSize = getCrossSize(childWidget.getWidth(), childWidget.getHeight());

		// Take the end of mainContentLength as a reference for laying out the children
		int widgetStartReverse = (int) (mainContentSize - widgetStart) - childMainSize;

		// let the widget take all the available space
		if (this.align == Align.STRETCH) {
			childCrossSize = rowEnd - rowStart;
		}

		// lay out each child depending on the current flex direction
		boolean isReverse = isReverse();
		int x;
		int y;
		int width;
		int height;
		if (isRow()) {
			x = isReverse ? widgetStartReverse : (int) widgetStart;
			y = rowStart;
			width = childMainSize;
			height = childCrossSize;
		} else {
			x = rowStart;
			y = isReverse ? widgetStartReverse : (int) widgetStart;
			width = childCrossSize;
			height = childMainSize;
		}
		layOutChild(childWidget, x, y, width, height);
	}

	/**
	 * Calculates the widget start position on the main axis based on justify content and remaining space.
	 *
	 * @param remainingSpace
	 *            remaining space on the main axis.
	 * @param numChildrenInRow
	 *            number of children in the current row.
	 * @return widget start position each child in the current row.
	 */
	private float computeJustifyStart(int remainingSpace, int numChildrenInRow) {
		int start;
		switch (this.justify) {
		case SPACE_AROUND:
			assert numChildrenInRow != 0;
			start = (remainingSpace / numChildrenInRow) / 2;
			break;
		case END:
			start = remainingSpace;
			break;
		case CENTER:
			start = remainingSpace / 2;
			break;
		case START:
		case SPACE_BETWEEN:
		default:
			start = 0;
		}

		return start;
	}

	/**
	 * Calculates the space between widgets based on the justify content main parameter for widget positioning.
	 *
	 * @param remainingSpace
	 *            remaining space on the main axis.
	 * @param numChildrenInRow
	 *            number of children in the current row.
	 * @return space between widgets for proper alignment.
	 */
	private float getSpaceBetweenWidgets(int remainingSpace, int numChildrenInRow) {

		float spaceBetweenWidgets;

		switch (this.justify) {
		case SPACE_BETWEEN:
			if (numChildrenInRow > 1) {
				spaceBetweenWidgets = (float) remainingSpace / (numChildrenInRow - 1);
			} else {
				spaceBetweenWidgets = remainingSpace;
			}
			break;
		case SPACE_AROUND:
			assert numChildrenInRow != 0;
			spaceBetweenWidgets = (float) remainingSpace / numChildrenInRow;
			break;
		case START:
		case CENTER:
		case END:
		default:
			spaceBetweenWidgets = 0;
		}
		return spaceBetweenWidgets;
	}

	/**
	 * Calculates the alignment position on the cross axis for a child based on alignment parameter.
	 *
	 * @param childCrossSize
	 *            child size on the cross axis.
	 * @param crossRowSize
	 *            the row size of current row.
	 * @param rowCrossPosition
	 *            the multiple row start position.
	 * @param crossContentSize
	 *            available size on the cross axis.
	 * @return alignment position for the child.
	 */
	private int computeAlignment(int childCrossSize, int crossRowSize, int[] rowCrossPosition, int crossContentSize) {

		int remainingSpace = crossContentSize - getRowTotalCrossSize();
		int rowRemainingSpace = crossRowSize - childCrossSize;

		int widgetCrossStart;
		switch (this.align) {
		case CENTER:
			widgetCrossStart = (remainingSpace / 2) + (rowRemainingSpace / 2);
			break;
		case END:
			widgetCrossStart = remainingSpace + rowRemainingSpace;
			break;
		case START:
		case STRETCH:
		default:
			widgetCrossStart = 0;
		}
		return rowCrossPosition[0] + widgetCrossStart; // add multiple row start position
	}

	private int getMainSize(int width, int height) {
		return isRow() ? width : height;
	}

	private int getCrossSize(int width, int height) {
		return isRow() ? height : width;
	}

	private int getRowTotalMainSize() {
		int total = 0;
		for (FlexRow row : this.rows) {
			total += row.getMainRowSize();
		}
		return total;
	}

	private int getRowTotalCrossSize() {
		int total = 0;
		for (FlexRow row : this.rows) {
			total += row.getCrossRowSize();
		}
		return total;
	}

	private int calculateRowStretch(int contentCrossSize, FlexRow currentRow) {
		return isStretch() ? contentCrossSize / this.rows.length : currentRow.getCrossRowSize();
	}

	/**
	 * Sets the direction of the flex.
	 *
	 * @param direction
	 *            the direction to set.
	 */
	public void setDirection(Direction direction) {
		this.direction = direction;
		resetRows();
	}

	/**
	 * Sets the justify of the flex.
	 *
	 * @param justify
	 *            the justify to set.
	 */
	public void setJustify(Justify justify) {
		this.justify = justify;
		resetRows();
	}

	/**
	 * Sets the align of the flex.
	 *
	 * @param align
	 *            the align to set.
	 */
	public void setAlign(Align align) {
		this.align = align;
		resetRows();
	}

	private void resetRows() {
		this.rows = new FlexRow[0];
	}

	/**
	 * Checks if the main axis is horizontal (ROW or ROW_REVERSE).
	 *
	 * @return <code>true</code> if the main axis is horizontal, <code>false</code> otherwise.
	 */
	public boolean isRow() {
		return this.direction == Direction.ROW || this.direction == Direction.ROW_REVERSE;
	}

	/**
	 * Checks if the direction is reversed (ROW_REVERSE or COLUMN_REVERSE).
	 *
	 * @return <code>true</code> if the direction is reversed, <code>false</code> otherwise.
	 */
	public boolean isReverse() {
		return this.direction == Direction.ROW_REVERSE || this.direction == Direction.COLUMN_REVERSE;
	}

	/**
	 * Checks if the current align is STRETCH.
	 *
	 * @return <code>true</code> if align is STRETCH, <code>false</code> otherwise.
	 */
	public boolean isStretch() {
		return this.align == Align.STRETCH;
	}

	@Override
	public void addChild(Widget child) {
		super.addChild(child);
		resetRows();
	}

	@Override
	public void removeChild(Widget childToRemove) {
		super.removeChild(childToRemove);
		resetRows();
	}

	@Override
	public void removeAllChildren() {
		super.removeAllChildren();
		resetRows();
	}

	private static class FlexRow {

		private final int endOffset;
		private final int mainRowSize;
		private final int crossRowSize;

		public FlexRow(int endOffset, int mainRowSize, int crossRowSize) {
			this.endOffset = endOffset;
			this.mainRowSize = mainRowSize;
			this.crossRowSize = crossRowSize;
		}

		public int getEndOffset() {
			return this.endOffset;
		}

		public int getMainRowSize() {
			return this.mainRowSize;
		}

		public int getCrossRowSize() {
			return this.crossRowSize;
		}

	}

}
