/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
/*
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 *
 * Copyright 2013 Red Hat, Inc.
 */

/**
 * SECTION:nmt-newt-grid
 * @short_description: Grid container
 *
 * #NmtNewtGrid is the most general-purpose container widget in NmtNewt.
 *
 * An #NmtNewtGrid consists of a number of rows and columns. There is
 * no pre-established maximum row or columns. Rather, rows and columns
 * exist if and only if there are widgets in them.
 *
 * The width of each column is the width of the widest widget in that
 * column, and the height of each row is the height of the tallest
 * widget in that row. Empty rows and empty columns take up no space,
 * so a grid with a single widget at 0,0 would look exactly the same
 * if the widget was at 5,10 instead.
 *
 * If a widget's cell ends up being larger than the widget's requested
 * size, then by default the widget will be centered in its cell.
 * However, this can be modified by changing its #NmtNewtGridFlags.
 * FIXME: the FILL/ANCHOR flags can be implemented in #NmtNewtWidget
 * and so should move there. Less clear about the EXPAND flags, which
 * must be implemented by the container...
 */

#include "nm-default.h"

#include <string.h>

#include "nmt-newt-grid.h"

G_DEFINE_TYPE (NmtNewtGrid, nmt_newt_grid, NMT_TYPE_NEWT_CONTAINER)

#define NMT_NEWT_GRID_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), NMT_TYPE_NEWT_GRID, NmtNewtGridPrivate))

typedef struct {
	NmtNewtWidget *widget;
	int x, y;
	NmtNewtGridFlags flags;
	int req_height, req_width;
} NmtNewtGridChild;

typedef struct {
	GArray *children;
	int max_x, max_y;
	int *row_heights, *col_widths;
	gboolean *expand_rows, *expand_cols;
	int n_expand_rows, n_expand_cols;
	int req_height, req_width;
} NmtNewtGridPrivate;

/**
 * nmt_newt_grid_new:
 *
 * Creates a new #NmtNewtGrid
 *
 * Returns: a new #NmtNewtGrid
 */
NmtNewtWidget *
nmt_newt_grid_new (void)
{
	return g_object_new (NMT_TYPE_NEWT_GRID, NULL);
}

static void
nmt_newt_grid_init (NmtNewtGrid *grid)
{
	NmtNewtGridPrivate *priv = NMT_NEWT_GRID_GET_PRIVATE (grid);

	priv->children = g_array_new (FALSE, FALSE, sizeof (NmtNewtGridChild));
}

static void
nmt_newt_grid_finalize (GObject *object)
{
	NmtNewtGridPrivate *priv = NMT_NEWT_GRID_GET_PRIVATE (object);

	g_array_unref (priv->children);
	g_clear_pointer (&priv->row_heights, g_free);
	g_clear_pointer (&priv->col_widths, g_free);
	g_clear_pointer (&priv->expand_rows, g_free);
	g_clear_pointer (&priv->expand_cols, g_free);

	G_OBJECT_CLASS (nmt_newt_grid_parent_class)->finalize (object);
}

static int
child_sort_func (gconstpointer a,
                 gconstpointer b)
{
	NmtNewtGridChild *child_a = (NmtNewtGridChild *)a;
	NmtNewtGridChild *child_b = (NmtNewtGridChild *)b;

	if (child_a->y != child_b->y)
		return child_a->y - child_b->y;
	else
		return child_a->x - child_b->x;
}

static newtComponent *
nmt_newt_grid_get_components (NmtNewtWidget *widget)
{
	NmtNewtGridPrivate *priv = NMT_NEWT_GRID_GET_PRIVATE (widget);
	NmtNewtGridChild *children;
	GPtrArray *cos;
	newtComponent *child_cos;
	int i, c;

	g_array_sort (priv->children, child_sort_func);
	children = (NmtNewtGridChild *)priv->children->data;

	cos = g_ptr_array_new ();

	for (i = 0; i < priv->children->len; i++) {
		if (!nmt_newt_widget_get_visible (children[i].widget))
			continue;

		child_cos = nmt_newt_widget_get_components (children[i].widget);
		for (c = 0; child_cos[c]; c++)
			g_ptr_array_add (cos, child_cos[c]);
		g_free (child_cos);
	}
	g_ptr_array_add (cos, NULL);

	return (newtComponent *) g_ptr_array_free (cos, FALSE);
}

static void
nmt_newt_grid_size_request (NmtNewtWidget *widget,
                            int           *width,
                            int           *height)
{
	NmtNewtGrid *grid = NMT_NEWT_GRID (widget);
	NmtNewtGridPrivate *priv = NMT_NEWT_GRID_GET_PRIVATE (grid);
	NmtNewtGridChild *children = (NmtNewtGridChild *)priv->children->data;
	int row, col, i;

	g_free (priv->row_heights);
	g_free (priv->col_widths);
	g_free (priv->expand_rows);
	g_free (priv->expand_cols);

	priv->row_heights = g_new0 (int, priv->max_y + 1);
	priv->col_widths = g_new0 (int, priv->max_x + 1);
	priv->expand_rows = g_new0 (gboolean, priv->max_y + 1);
	priv->expand_cols = g_new0 (gboolean, priv->max_x + 1);
	priv->n_expand_rows = priv->n_expand_cols = 0;

	for (row = 0; row < priv->max_y + 1; row++) {
		for (col = 0; col < priv->max_x + 1; col++) {
			for (i = 0; i < priv->children->len; i++) {
				if (children[i].x != col || children[i].y != row)
					continue;
				if (!nmt_newt_widget_get_visible (children[i].widget))
					continue;

				nmt_newt_widget_size_request (children[i].widget,
				                              &children[i].req_width,
				                              &children[i].req_height);
				if (children[i].req_height > priv->row_heights[row])
					priv->row_heights[row] = children[i].req_height;
				if (children[i].req_width > priv->col_widths[col])
					priv->col_widths[col] = children[i].req_width;

				if (   (children[i].flags & NMT_NEWT_GRID_EXPAND_X)
				    && !priv->expand_cols[children[i].x]) {
					priv->expand_cols[children[i].x] = TRUE;
					priv->n_expand_cols++;
				}
				if (   (children[i].flags & NMT_NEWT_GRID_EXPAND_Y)
				    && !priv->expand_rows[children[i].y]) {
					priv->expand_rows[children[i].y] = TRUE;
					priv->n_expand_rows++;
				}
			}
		}
	}

	priv->req_height = priv->req_width = 0;
	for (row = 0; row < priv->max_y + 1; row++)
		priv->req_height += priv->row_heights[row];
	for (col = 0; col < priv->max_x + 1; col++)
		priv->req_width += priv->col_widths[col];

	*height = priv->req_height;
	*width = priv->req_width;
}

static void
nmt_newt_grid_size_allocate (NmtNewtWidget *widget,
                             int            x,
                             int            y,
                             int            width,
                             int            height)
{
	NmtNewtGridPrivate *priv = NMT_NEWT_GRID_GET_PRIVATE (widget);
	NmtNewtGridChild *children = (NmtNewtGridChild *)priv->children->data, *child;
	int i, row, col;
	int child_x, child_y, child_width, child_height;
	int extra, extra_all, extra_some;

	extra = width - priv->req_width;
	if (extra > 0 && priv->n_expand_cols) {
		extra_all = extra / priv->n_expand_cols;
		extra_some = extra % priv->n_expand_cols;

		for (col = 0; col < priv->max_x + 1; col++) {
			if (!priv->expand_cols[col])
				continue;
			priv->col_widths[col] += extra_all;
			if (extra_some) {
				priv->col_widths[col]++;
				extra_some--;
			}
		}
	}

	extra = height - priv->req_height;
	if (extra > 0 && priv->n_expand_rows) {
		extra_all = extra / priv->n_expand_rows;
		extra_some = extra % priv->n_expand_rows;

		for (row = 0; row < priv->max_y + 1; row++) {
			if (!priv->expand_rows[row])
				continue;
			priv->row_heights[row] += extra_all;
			if (extra_some) {
				priv->row_heights[row]++;
				extra_some--;
			}
		}
	}

	for (i = 0; i < priv->children->len; i++) {
		child = &children[i];
		if (!nmt_newt_widget_get_visible (child->widget))
			continue;

		child_x = x;
		for (col = 0; col < child->x; col++)
			child_x += priv->col_widths[col];

		if ((child->flags & NMT_NEWT_GRID_FILL_X) == NMT_NEWT_GRID_FILL_X) {
			child_width = priv->col_widths[child->x];
		} else {
			child_width = child->req_width;
			if (child->flags & NMT_NEWT_GRID_ANCHOR_RIGHT)
				child_x += priv->col_widths[child->x] - child->req_width;
			else if (!(child->flags & NMT_NEWT_GRID_ANCHOR_LEFT))
				child_x += (priv->col_widths[child->x] - child->req_width) / 2;
		}

		child_y = y;
		for (row = 0; row < child->y; row++)
			child_y += priv->row_heights[row];

		if ((child->flags & NMT_NEWT_GRID_FILL_Y) == NMT_NEWT_GRID_FILL_Y) {
			child_height = priv->row_heights[child->y];
		} else {
			child_height = child->req_height;
			if (child->flags & NMT_NEWT_GRID_ANCHOR_BOTTOM)
				child_y += priv->row_heights[child->y] - child->req_height;
			else if (!(child->flags & NMT_NEWT_GRID_ANCHOR_TOP))
				child_y += (priv->row_heights[child->y] - child->req_height) / 2;
		}

		nmt_newt_widget_size_allocate (child->widget,
		                               child_x, child_y,
		                               child_width, child_height);
	}
}

static void
nmt_newt_grid_find_size (NmtNewtGrid *grid)
{
	NmtNewtGridPrivate *priv = NMT_NEWT_GRID_GET_PRIVATE (grid);
	NmtNewtGridChild *children = (NmtNewtGridChild *)priv->children->data;
	int i;

	priv->max_x = priv->max_y = 0;
	for (i = 0; i < priv->children->len; i++) {
		if (children[i].x > priv->max_x)
			priv->max_x = children[i].x;
		if (children[i].y > priv->max_y)
			priv->max_y = children[i].y;
	}
}

/**
 * nmt_newt_grid_add:
 * @grid: an #NmtNewtGrid
 * @widget: the widget to add
 * @x: x coordinate
 * @y: y coordinate
 *
 * Adds @widget to @grid at @x, @y. See the discussion above for more
 * details of exactly how this works.
 */
void
nmt_newt_grid_add (NmtNewtGrid   *grid,
                   NmtNewtWidget *widget,
                   int            x,
                   int            y)
{
	NmtNewtGridPrivate *priv = NMT_NEWT_GRID_GET_PRIVATE (grid);
	NmtNewtGridChild child;

	NMT_NEWT_CONTAINER_CLASS (nmt_newt_grid_parent_class)->add (NMT_NEWT_CONTAINER (grid), widget);

	memset (&child, 0, sizeof (child));
	child.widget = widget;
	child.x = x;
	child.y = y;
	child.flags = NMT_NEWT_GRID_FILL_X | NMT_NEWT_GRID_FILL_Y;
	g_array_append_val (priv->children, child);

	if (x > priv->max_x)
		priv->max_x = x;
	if (y > priv->max_y)
		priv->max_y = y;
}

static int
find_child (NmtNewtGrid   *grid,
            NmtNewtWidget *widget)
{
	NmtNewtGridPrivate *priv = NMT_NEWT_GRID_GET_PRIVATE (grid);
	NmtNewtGridChild *children = (NmtNewtGridChild *)priv->children->data;
	int i;

	for (i = 0; i < priv->children->len; i++) {
		if (children[i].widget == widget)
			return i;
	}

	return -1;
}

static void
nmt_newt_grid_remove (NmtNewtContainer *container,
                      NmtNewtWidget    *widget)
{
	NmtNewtGrid *grid = NMT_NEWT_GRID (container);
	NmtNewtGridPrivate *priv = NMT_NEWT_GRID_GET_PRIVATE (grid);
	int i;

	i = find_child (grid, widget);
	if (i != -1) {
		g_array_remove_index (priv->children, i);
		nmt_newt_grid_find_size (grid);
	}

	NMT_NEWT_CONTAINER_CLASS (nmt_newt_grid_parent_class)->remove (container, widget);
}

/**
 * nmt_newt_grid_move:
 * @grid: an #NmtNewtGrid
 * @widget: a child of @grid
 * @x: x coordinate
 * @y: y coordinate
 *
 * Moves @widget to the given new coordinates.
 */
void
nmt_newt_grid_move (NmtNewtGrid   *grid,
                    NmtNewtWidget *widget,
                    int            x,
                    int            y)
{
	NmtNewtGridPrivate *priv = NMT_NEWT_GRID_GET_PRIVATE (grid);
	NmtNewtGridChild *children = (NmtNewtGridChild *)priv->children->data;
	int i;

	i = find_child (grid, widget);
	if (i != -1 && (children[i].x != x || children[i].y != y)) {
		children[i].x = x;
		children[i].y = y;
		nmt_newt_grid_find_size (grid);
		nmt_newt_widget_needs_rebuild (NMT_NEWT_WIDGET (grid));
	}
}

/**
 * NmtNewtGridFlags:
 * @NMT_NEWT_GRID_EXPAND_X: The widget's cell should expand
 *   horizontally if the grid as a whole is given more width than
 *   it requested.
 * @NMT_NEWT_GRID_EXPAND_Y: The widget's cell should expand
 *   vertically if the grid as a whole is given more height than
 *   it requested.
 * @NMT_NEWT_GRID_ANCHOR_LEFT: If the widget's cell is wider than
 *   the widget requested, the widget should be anchored to the
 *   left of its cell rather than being centered.
 * @NMT_NEWT_GRID_ANCHOR_RIGHT: If the widget's cell is wider than
 *   the widget requested, the widget should be anchored to the
 *   right of its cell rather than being centered.
 * @NMT_NEWT_GRID_FILL_X: If the widget's cell is wider than
 *   the widget requested, the widget should be allocated the
 *   full width of the cell; this is equivalent to specifying
 *   both %NMT_NEWT_GRID_ANCHOR_LEFT and %NMT_NEWT_GRID_ANCHOR_RIGHT.
 * @NMT_NEWT_GRID_ANCHOR_TOP: If the widget's cell is taller than
 *   the widget requested, the widget should be anchored to the
 *   top of its cell rather than being centered.
 * @NMT_NEWT_GRID_ANCHOR_BOTTOM: If the widget's cell is taller than
 *   the widget requested, the widget should be anchored to the
 *   bottom of its cell rather than being centered.
 * @NMT_NEWT_GRID_FILL_Y: If the widget's cell is taller than
 *   the widget requested, the widget should be allocated the
 *   full height of the cell; this is equivalent to specifying
 *   both %NMT_NEWT_GRID_ANCHOR_TOP and %NMT_NEWT_GRID_ANCHOR_BOTTOM.
 *
 * Flags describing how a widget is placed within its grid cell.
 */

/**
 * nmt_newt_grid_set_flags:
 * @grid: an #NmtNewtGrid
 * @widget: a child of @grid
 * @flags: #NmtNewtGridFlags for @widget
 *
 * Sets the #NmtNewtGridFlags on @widget
 */
void
nmt_newt_grid_set_flags (NmtNewtGrid      *grid,
                         NmtNewtWidget    *widget,
                         NmtNewtGridFlags  flags)
{
	NmtNewtGridPrivate *priv = NMT_NEWT_GRID_GET_PRIVATE (grid);
	NmtNewtGridChild *children = (NmtNewtGridChild *)priv->children->data;
	int i;

	i = find_child (grid, widget);
	if (i != -1)
		children[i].flags = flags;
}

static void
nmt_newt_grid_class_init (NmtNewtGridClass *grid_class)
{
	GObjectClass *object_class = G_OBJECT_CLASS (grid_class);
	NmtNewtWidgetClass *widget_class = NMT_NEWT_WIDGET_CLASS (grid_class);
	NmtNewtContainerClass *container_class = NMT_NEWT_CONTAINER_CLASS (grid_class);

	g_type_class_add_private (grid_class, sizeof (NmtNewtGridPrivate));

	/* virtual methods */
	object_class->finalize = nmt_newt_grid_finalize;

	widget_class->get_components = nmt_newt_grid_get_components;
	widget_class->size_request   = nmt_newt_grid_size_request;
	widget_class->size_allocate  = nmt_newt_grid_size_allocate;

	container_class->remove = nmt_newt_grid_remove;
}
