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

/**
 * Lays out any number of children in a grid.
 * <p>
 * All the children are laid out in a grid, which has a fixed number of columns and rows.
 * <p>
 * In a grid, all children have the same width and the same height, regardless of their optimal size.
 */
public class FixedGrid extends Container {

	private final int columns;
	private final int rows;
	private final Widget[] children;

	/**
	 * Creates a grid specifying its number of columns and rows.
	 *
	 * @param columns
	 *            the number of columns
	 * @param rows
	 *            the number of rows
	 * @throws IllegalArgumentException
	 *             if one of the parameters is negative or zero
	 */
	public FixedGrid(int columns, int rows) {
		if (columns <= 0 || rows <= 0) {
			throw new IllegalArgumentException();
		}
		this.columns = columns;
		this.rows = rows;
		this.children = new Widget[columns * rows];

	}

	private int getIndex(int column, int row) {
		if (column >= this.columns || row >= this.rows) {
			throw new IndexOutOfBoundsException();
		}
		return column + row * this.columns;
	}

	/**
	 * Gets the number of columns.
	 *
	 * @return the number of columns.
	 */
	public int getColumns() {
		return this.columns;
	}

	/**
	 * Gets the number of rows.
	 *
	 * @return the number of rows.
	 */
	public int getRows() {
		return this.rows;
	}

	/**
	 * Gets the widget at the specified column and row.
	 *
	 * @param column
	 *            the column of the widget to return
	 * @param row
	 *            the row of the widget to return
	 * @return the widget at the specified column and row
	 * @throws IndexOutOfBoundsException
	 *             if the cell referenced by the given coordinates is out of range
	 */
	public @Nullable Widget getCellChild(int column, int row) {
		int index = getIndex(column, row);
		return this.children[index];
	}

	/**
	 * Adds a widget at the specified column and row.
	 *
	 * @param child
	 *            the child to add
	 * @param column
	 *            the column of the widget
	 * @param row
	 *            the row of the widget
	 * @throws IndexOutOfBoundsException
	 *             if the cell referenced by the given coordinates is out of range
	 */
	public void addChild(Widget child, int column, int row) {
		int index = getIndex(column, row);
		Widget previousWidget = this.children[index];
		if (previousWidget != null) {
			removeChild(previousWidget);
		}
		this.children[index] = child;
		addChild(child);
	}

	/**
	 * Removes the widget at the specified column and row.
	 *
	 * @param column
	 *            the column of the widget
	 * @param row
	 *            the row of the widget
	 * @throws IndexOutOfBoundsException
	 *             if the cell referenced by the given coordinates is out of range
	 */
	public void removeChild(int column, int row) {
		int index = getIndex(column, row);
		Widget previousWidget = this.children[index];
		if (previousWidget != null) {
			this.children[index] = null;
			removeChild(previousWidget);
		}
	}

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

	@Override
	protected void computeContentOptimalSize(Size size) {
		// handle case with no widget
		int childrenCount = getChildrenCount();
		if (childrenCount == 0) {
			size.setSize(0, 0);
			return;
		}

		// compute size hint for children
		int widthHint = size.getWidth();
		int heightHint = size.getHeight();
		int childWidthHint = (widthHint == Widget.NO_CONSTRAINT ? Widget.NO_CONSTRAINT : widthHint / this.columns);
		int childHeightHint = (heightHint == Widget.NO_CONSTRAINT ? Widget.NO_CONSTRAINT : heightHint / this.rows);

		// compute max cell size
		int maxCellWidth = 0;
		int maxCellHeight = 0;

		for (Widget widget : getChildren()) {
			assert (widget != null);

			// compute child optimal size
			computeChildOptimalSize(widget, childWidthHint, childHeightHint);

			// update max cell size
			maxCellWidth = Math.max(maxCellWidth, widget.getWidth());
			maxCellHeight = Math.max(maxCellHeight, widget.getHeight());
		}

		// set container optimal size
		size.setSize(maxCellWidth * this.columns, maxCellHeight * this.rows);
	}

	@Override
	protected void layOutChildren(int contentWidth, int contentHeight) {
		// handle case with no widget
		int childrenCount = getChildrenCount();
		if (childrenCount == 0) {
			return;
		}

		float columnWidthF = (float) contentWidth / this.columns;
		float rowHeightF = (float) contentHeight / this.rows;

		int index = 0;
		float xF = 0;
		float yF = 0;
		for (int r = 0; r < this.rows; r++) {
			int childY = Math.round(yF);
			float nextYF = yF + rowHeightF;
			int childHeight = Math.round(nextYF) - childY;
			for (int c = 0; c < this.columns; c++) {
				Widget child = this.children[index];
				float nextXF = xF + columnWidthF;
				int childX = Math.round(xF);
				int childWidth = Math.round(nextXF) - childX;
				if (child != null) {
					layOutChild(child, childX, childY, childWidth, childHeight);
				}
				xF = nextXF;
				index++;
			}
			yF = nextYF;
			xF = 0;
		}
	}

}
