/*
 * Copyright 2009-2021 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.mwt.Container;
import ej.mwt.Widget;
import ej.mwt.util.Size;

/**
 * Lays out any number of children horizontally or vertically, using multiple rows if necessary.
 * <p>
 * As many children as possible will be laid out on the first row. If the next remaining child does not fit on the row,
 * a new row is created in order to lay out the remaining children. This process is repeated until all children are laid
 * out. To know how many children can be laid out in a single row, the optimal size of the children is used.
 * <p>
 * In a horizontal flow, all the widgets of the same row will have the same height. In a vertical flow, all the widgets
 * of the same row will have the same width.
 * <p>
 * Horizontal:
 * <p>
 * <img src="doc-files/flow_horizontal.png" alt="Horizontal flow.">
 * <p>
 * Vertical:
 * <p>
 * <img src="doc-files/flow_vertical.png" alt="Vertical flow.">
 */
public class Flow extends Container {

	private boolean orientation;

	/**
	 * Creates a flow specifying its orientation.
	 *
	 * @param orientation
	 *            the orientation of the flow (see {@link LayoutOrientation}).
	 */
	public Flow(boolean orientation) {
		this.orientation = orientation;
	}

	/**
	 * Sets the orientation of this flow.
	 *
	 * @param orientation
	 *            the orientation to set (see {@link LayoutOrientation}).
	 */
	public void setOrientation(boolean orientation) {
		this.orientation = orientation;
	}

	/**
	 * Gets the orientation of this flow.
	 *
	 * @return the orientation of this flow (see {@link LayoutOrientation}).
	 */
	public boolean getOrientation() {
		return this.orientation;
	}

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

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

	@Override
	public void insertChild(Widget child, int index) {
		super.insertChild(child, index);
	}

	@Override
	public void replaceChild(int index, Widget child) {
		super.replaceChild(index, child);
	}

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

	@Override
	protected void computeContentOptimalSize(Size size) {
		int widthHint = size.getWidth();
		int heightHint = size.getHeight();

		Widget[] children = getChildren();
		for (Widget child : children) {
			assert (child != null);

			// compute child optimal size
			computeChildOptimalSize(child, widthHint, heightHint);
		}

		// get total size
		boolean isHorizontal = (this.orientation == LayoutOrientation.HORIZONTAL);
		int mainSizeHint = (isHorizontal ? widthHint : heightHint);
		int[] totalSize = new int[2];
		computeTotalSize(children, isHorizontal, mainSizeHint, totalSize);

		// set container optimal size
		if (isHorizontal) {
			size.setSize(totalSize[0], totalSize[1]);
		} else {
			size.setSize(totalSize[1], totalSize[0]);
		}
	}

	@Override
	protected void layOutChildren(int contentWidth, int contentHeight) {
		boolean isHorizontal = (this.orientation == LayoutOrientation.HORIZONTAL);
		int mainContentLength = (isHorizontal ? contentWidth : contentHeight);
		int otherContentLength = (isHorizontal ? contentHeight : contentWidth);
		Widget[] children = getChildren();

		// compute total size
		int[] size = new int[2];
		computeTotalSize(children, isHorizontal, mainContentLength, size);
		int otherTotalLength = size[1];

		int rowPosition = 0;
		int childrenOffset = 0;
		while (childrenOffset < children.length) {
			// compute row size
			int newChildrenOffset = computeRowSize(children, childrenOffset, isHorizontal, mainContentLength, size);
			int mainRowLength = size[0];
			int otherRowLength = size[1];

			// compute row start and end
			int rowStart = rowPosition * otherContentLength / otherTotalLength;
			int rowEnd = (rowPosition + otherRowLength) * otherContentLength / otherTotalLength;

			// lay out each widget of this row
			int widgetPosition = 0;
			for (int i = childrenOffset; i < newChildrenOffset; i++) {
				Widget child = children[i];
				assert (child != null);
				int mainWidgetLength = (isHorizontal ? child.getWidth() : child.getHeight());

				// compute widget start and end
				int widgetStart = widgetPosition * mainContentLength / mainRowLength;
				int widgetEnd = (widgetPosition + mainWidgetLength) * mainContentLength / mainRowLength;

				// lay out child
				if (isHorizontal) {
					layOutChild(child, widgetStart, rowStart, widgetEnd - widgetStart, rowEnd - rowStart);
				} else {
					layOutChild(child, rowStart, widgetStart, rowEnd - rowStart, widgetEnd - widgetStart);
				}

				widgetPosition += mainWidgetLength;
			}

			rowPosition += otherRowLength;

			childrenOffset = newChildrenOffset;
		}
	}

	/**
	 * Returns the total size of the rows, considering the maximum size of a row. The given size array is expected to be
	 * at least 2 of length. The first element is set to the maximum row size (of main axis), and the second element is
	 * set to the total row size (of other axis).
	 *
	 * @param widgets
	 *            the widgets to lay out.
	 * @param isHorizontal
	 *            whether the flow is horizontal or vertical.
	 * @param maxRowSize
	 *            the maximum size of a row, or Widget#NO_CONSTRAINT for no constraint.
	 * @param size
	 *            the size array to fill.
	 */
	private static void computeTotalSize(Widget[] widgets, boolean isHorizontal, int maxRowSize, int[] size) {
		int mainMax = 0;
		int otherTotal = 0;

		int widgetsOffset = 0;
		while (widgetsOffset < widgets.length) {
			// get row size
			widgetsOffset = computeRowSize(widgets, widgetsOffset, isHorizontal, maxRowSize, size);

			// add to total size
			mainMax = Math.max(size[0], mainMax);
			otherTotal += size[1];
		}

		// set total size
		size[0] = mainMax;
		size[1] = otherTotal;
	}

	/**
	 * Returns the size of a rows, considering the maximum size of a row. The given size array is expected to be at
	 * least 2 of length. The first element is set to the total widget size (of main axis), and the second element is
	 * set to the max widget size (of other axis). If the first given widget is bigger than the maximum row size, it is
	 * fitted on the row anyway.
	 *
	 * @param widgets
	 *            the widgets to lay out.
	 * @param widgetsOffset
	 *            the offset to start at.
	 * @param isHorizontal
	 *            whether the flow is horizontal or vertical.
	 * @param maxRowSize
	 *            the maximum size of a row, or Widget#NO_CONSTRAINT for no constraint.
	 * @param size
	 *            the size array to fill.
	 */
	private static int computeRowSize(Widget[] widgets, int widgetsOffset, boolean isHorizontal, int maxRowSize,
			int[] size) {
		int mainTotal = 0;
		int otherMax = 0;

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

			// get child optimal size
			int mainChildLength;
			int otherChildLength;
			if (isHorizontal) {
				mainChildLength = child.getWidth();
				otherChildLength = child.getHeight();
			} else {
				mainChildLength = child.getHeight();
				otherChildLength = child.getWidth();
			}

			// check overflow
			if (maxRowSize != Widget.NO_CONSTRAINT && mainTotal + mainChildLength > maxRowSize
					&& childrenIndex != widgetsOffset) {
				break;
			}

			// add child to row
			mainTotal += mainChildLength;
			otherMax = Math.max(otherChildLength, otherMax);

			childrenIndex++;
		}

		// set row size
		size[0] = mainTotal;
		size[1] = otherMax;

		// return new children offset
		return childrenIndex;
	}
}
