/*
 * gtkcols.c - implementation of the `Columns' GTK layout container.
 */

#include "gtkcols.h"

static void columns_init(Columns *cols);
static void columns_class_init(ColumnsClass *klass);
static void columns_map(GtkWidget *widget);
static void columns_unmap(GtkWidget *widget);
static void columns_draw(GtkWidget *widget, GdkRectangle *area);
static gint columns_expose(GtkWidget *widget, GdkEventExpose *event);
static void columns_base_add(GtkContainer *container, GtkWidget *widget);
static void columns_remove(GtkContainer *container, GtkWidget *widget);
static void columns_forall(GtkContainer *container, gboolean include_internals,
                           GtkCallback callback, gpointer callback_data);
static gint columns_focus(GtkContainer *container, GtkDirectionType dir);
static GtkType columns_child_type(GtkContainer *container);
static void columns_size_request(GtkWidget *widget, GtkRequisition *req);
static void columns_size_allocate(GtkWidget *widget, GtkAllocation *alloc);

static GtkContainerClass *parent_class = NULL;

GtkType columns_get_type(void)
{
    static GtkType columns_type = 0;

    if (!columns_type) {
        static const GtkTypeInfo columns_info = {
            "Columns",
            sizeof(Columns),
            sizeof(ColumnsClass),
            (GtkClassInitFunc) columns_class_init,
            (GtkObjectInitFunc) columns_init,
            /* reserved_1 */ NULL,
            /* reserved_2 */ NULL,
            (GtkClassInitFunc) NULL,
        };

        columns_type = gtk_type_unique(GTK_TYPE_CONTAINER, &columns_info);
    }

    return columns_type;
}

static gint (*columns_inherited_focus)(GtkContainer *container,
				       GtkDirectionType direction);

static void columns_class_init(ColumnsClass *klass)
{
    GtkObjectClass *object_class;
    GtkWidgetClass *widget_class;
    GtkContainerClass *container_class;

    object_class = (GtkObjectClass *)klass;
    widget_class = (GtkWidgetClass *)klass;
    container_class = (GtkContainerClass *)klass;

    parent_class = gtk_type_class(GTK_TYPE_CONTAINER);

    /*
     * FIXME: do we have to do all this faffing with set_arg,
     * get_arg and child_arg_type? Ick.
     */

    widget_class->map = columns_map;
    widget_class->unmap = columns_unmap;
    widget_class->draw = columns_draw;
    widget_class->expose_event = columns_expose;
    widget_class->size_request = columns_size_request;
    widget_class->size_allocate = columns_size_allocate;

    container_class->add = columns_base_add;
    container_class->remove = columns_remove;
    container_class->forall = columns_forall;
    container_class->child_type = columns_child_type;
    /* Save the previous value of this method. */
    if (!columns_inherited_focus)
	columns_inherited_focus = container_class->focus;
    container_class->focus = columns_focus;
}

static void columns_init(Columns *cols)
{
    GTK_WIDGET_SET_FLAGS(cols, GTK_NO_WINDOW);

    cols->children = NULL;
    cols->spacing = 0;
}

/*
 * These appear to be thoroughly tedious functions; the only reason
 * we have to reimplement them at all is because we defined our own
 * format for our GList of children...
 */
static void columns_map(GtkWidget *widget)
{
    Columns *cols;
    ColumnsChild *child;
    GList *children;

    g_return_if_fail(widget != NULL);
    g_return_if_fail(IS_COLUMNS(widget));

    cols = COLUMNS(widget);
    GTK_WIDGET_SET_FLAGS(cols, GTK_MAPPED);

    for (children = cols->children;
         children && (child = children->data);
         children = children->next) {
        if (child->widget &&
	    GTK_WIDGET_VISIBLE(child->widget) &&
            !GTK_WIDGET_MAPPED(child->widget))
            gtk_widget_map(child->widget);
    }
}
static void columns_unmap(GtkWidget *widget)
{
    Columns *cols;
    ColumnsChild *child;
    GList *children;

    g_return_if_fail(widget != NULL);
    g_return_if_fail(IS_COLUMNS(widget));

    cols = COLUMNS(widget);
    GTK_WIDGET_UNSET_FLAGS(cols, GTK_MAPPED);

    for (children = cols->children;
         children && (child = children->data);
         children = children->next) {
        if (child->widget &&
	    GTK_WIDGET_VISIBLE(child->widget) &&
            GTK_WIDGET_MAPPED(child->widget))
            gtk_widget_unmap(child->widget);
    }
}
static void columns_draw(GtkWidget *widget, GdkRectangle *area)
{
    Columns *cols;
    ColumnsChild *child;
    GList *children;
    GdkRectangle child_area;

    g_return_if_fail(widget != NULL);
    g_return_if_fail(IS_COLUMNS(widget));

    if (GTK_WIDGET_DRAWABLE(widget)) {
        cols = COLUMNS(widget);

        for (children = cols->children;
             children && (child = children->data);
             children = children->next) {
            if (child->widget &&
		GTK_WIDGET_DRAWABLE(child->widget) &&
                gtk_widget_intersect(child->widget, area, &child_area))
                gtk_widget_draw(child->widget, &child_area);
        }
    }
}
static gint columns_expose(GtkWidget *widget, GdkEventExpose *event)
{
    Columns *cols;
    ColumnsChild *child;
    GList *children;
    GdkEventExpose child_event;

    g_return_val_if_fail(widget != NULL, FALSE);
    g_return_val_if_fail(IS_COLUMNS(widget), FALSE);
    g_return_val_if_fail(event != NULL, FALSE);

    if (GTK_WIDGET_DRAWABLE(widget)) {
        cols = COLUMNS(widget);
        child_event = *event;

        for (children = cols->children;
             children && (child = children->data);
             children = children->next) {
            if (child->widget &&
		GTK_WIDGET_DRAWABLE(child->widget) &&
                GTK_WIDGET_NO_WINDOW(child->widget) &&
                gtk_widget_intersect(child->widget, &event->area,
                                     &child_event.area))
                gtk_widget_event(child->widget, (GdkEvent *)&child_event);
        }
    }
    return FALSE;
}

static void columns_base_add(GtkContainer *container, GtkWidget *widget)
{
    Columns *cols;

    g_return_if_fail(container != NULL);
    g_return_if_fail(IS_COLUMNS(container));
    g_return_if_fail(widget != NULL);

    cols = COLUMNS(container);

    /*
     * Default is to add a new widget spanning all columns.
     */
    columns_add(cols, widget, 0, 0);   /* 0 means ncols */
}

static void columns_remove(GtkContainer *container, GtkWidget *widget)
{
    Columns *cols;
    ColumnsChild *child;
    GtkWidget *childw;
    GList *children;
    gboolean was_visible;

    g_return_if_fail(container != NULL);
    g_return_if_fail(IS_COLUMNS(container));
    g_return_if_fail(widget != NULL);

    cols = COLUMNS(container);

    for (children = cols->children;
         children && (child = children->data);
         children = children->next) {
        if (child->widget != widget)
            continue;

        was_visible = GTK_WIDGET_VISIBLE(widget);
        gtk_widget_unparent(widget);
        cols->children = g_list_remove_link(cols->children, children);
        g_list_free(children);
        g_free(child);
        if (was_visible)
            gtk_widget_queue_resize(GTK_WIDGET(container));
        break;
    }

    for (children = cols->taborder;
         children && (childw = children->data);
         children = children->next) {
        if (childw != widget)
            continue;

        cols->taborder = g_list_remove_link(cols->taborder, children);
        g_list_free(children);
        break;
    }
}

static void columns_forall(GtkContainer *container, gboolean include_internals,
                           GtkCallback callback, gpointer callback_data)
{
    Columns *cols;
    ColumnsChild *child;
    GList *children, *next;

    g_return_if_fail(container != NULL);
    g_return_if_fail(IS_COLUMNS(container));
    g_return_if_fail(callback != NULL);

    cols = COLUMNS(container);

    for (children = cols->children;
         children && (child = children->data);
         children = next) {
        /*
         * We can't wait until after the callback to assign
         * `children = children->next', because the callback might
         * be gtk_widget_destroy, which would remove the link
         * `children' from the list! So instead we must get our
         * hands on the value of the `next' pointer _before_ the
         * callback.
         */
        next = children->next;
	if (child->widget)
	    callback(child->widget, callback_data);
    }
}

static GtkType columns_child_type(GtkContainer *container)
{
    return GTK_TYPE_WIDGET;
}

GtkWidget *columns_new(gint spacing)
{
    Columns *cols;

    cols = gtk_type_new(columns_get_type());
    cols->spacing = spacing;

    return GTK_WIDGET(cols);
}

void columns_set_cols(Columns *cols, gint ncols, const gint *percentages)
{
    ColumnsChild *childdata;
    gint i;

    g_return_if_fail(cols != NULL);
    g_return_if_fail(IS_COLUMNS(cols));
    g_return_if_fail(ncols > 0);
    g_return_if_fail(percentages != NULL);

    childdata = g_new(ColumnsChild, 1);
    childdata->widget = NULL;
    childdata->ncols = ncols;
    childdata->percentages = g_new(gint, ncols);
    childdata->force_left = FALSE;
    for (i = 0; i < ncols; i++)
        childdata->percentages[i] = percentages[i];

    cols->children = g_list_append(cols->children, childdata);
}

void columns_add(Columns *cols, GtkWidget *child,
                 gint colstart, gint colspan)
{
    ColumnsChild *childdata;

    g_return_if_fail(cols != NULL);
    g_return_if_fail(IS_COLUMNS(cols));
    g_return_if_fail(child != NULL);
    g_return_if_fail(child->parent == NULL);

    childdata = g_new(ColumnsChild, 1);
    childdata->widget = child;
    childdata->colstart = colstart;
    childdata->colspan = colspan;
    childdata->force_left = FALSE;

    cols->children = g_list_append(cols->children, childdata);
    cols->taborder = g_list_append(cols->taborder, child);

    gtk_widget_set_parent(child, GTK_WIDGET(cols));

    if (GTK_WIDGET_REALIZED(cols))
        gtk_widget_realize(child);

    if (GTK_WIDGET_VISIBLE(cols) && GTK_WIDGET_VISIBLE(child)) {
        if (GTK_WIDGET_MAPPED(cols))
            gtk_widget_map(child);
        gtk_widget_queue_resize(child);
    }
}

void columns_force_left_align(Columns *cols, GtkWidget *widget)
{
    ColumnsChild *child;
    GList *children;

    g_return_if_fail(cols != NULL);
    g_return_if_fail(IS_COLUMNS(cols));
    g_return_if_fail(widget != NULL);

    for (children = cols->children;
         children && (child = children->data);
         children = children->next) {
        if (child->widget != widget)
            continue;

	child->force_left = TRUE;
        if (GTK_WIDGET_VISIBLE(widget))
            gtk_widget_queue_resize(GTK_WIDGET(cols));
        break;
    }
}

void columns_taborder_last(Columns *cols, GtkWidget *widget)
{
    GtkWidget *childw;
    GList *children;

    g_return_if_fail(cols != NULL);
    g_return_if_fail(IS_COLUMNS(cols));
    g_return_if_fail(widget != NULL);

    for (children = cols->taborder;
         children && (childw = children->data);
         children = children->next) {
        if (childw != widget)
            continue;

        cols->taborder = g_list_remove_link(cols->taborder, children);
        g_list_free(children);
	cols->taborder = g_list_append(cols->taborder, widget);
        break;
    }
}

/*
 * Override GtkContainer's focus movement so the user can
 * explicitly specify the tab order.
 */
static gint columns_focus(GtkContainer *container, GtkDirectionType dir)
{
    Columns *cols;
    GList *pos;
    GtkWidget *focuschild;

    g_return_val_if_fail(container != NULL, FALSE);
    g_return_val_if_fail(IS_COLUMNS(container), FALSE);

    cols = COLUMNS(container);

    if (!GTK_WIDGET_DRAWABLE(cols) ||
	!GTK_WIDGET_IS_SENSITIVE(cols))
	return FALSE;

    if (!GTK_WIDGET_CAN_FOCUS(container) &&
	(dir == GTK_DIR_TAB_FORWARD || dir == GTK_DIR_TAB_BACKWARD)) {

	focuschild = container->focus_child;
	gtk_container_set_focus_child(container, NULL);

	if (dir == GTK_DIR_TAB_FORWARD)
	    pos = cols->taborder;
	else
	    pos = g_list_last(cols->taborder);

	while (pos) {
	    GtkWidget *child = pos->data;

	    if (focuschild) {
		if (focuschild == child) {
		    focuschild = NULL; /* now we can start looking in here */
		    if (GTK_WIDGET_DRAWABLE(child) &&
			GTK_IS_CONTAINER(child) &&
			!GTK_WIDGET_HAS_FOCUS(child)) {
			if (gtk_container_focus(GTK_CONTAINER(child), dir))
			    return TRUE;
		    }
		}
	    } else if (GTK_WIDGET_DRAWABLE(child)) {
		if (GTK_IS_CONTAINER(child)) {
		    if (gtk_container_focus(GTK_CONTAINER(child), dir))
			return TRUE;
		} else if (GTK_WIDGET_CAN_FOCUS(child)) {
		    gtk_widget_grab_focus(child);
		    return TRUE;
		}
	    }

	    if (dir == GTK_DIR_TAB_FORWARD)
		pos = pos->next;
	    else
		pos = pos->prev;
	}

	return FALSE;
    } else
	return columns_inherited_focus(container, dir);
}

/*
 * Now here comes the interesting bit. The actual layout part is
 * done in the following two functions:
 * 
 * columns_size_request() examines the list of widgets held in the
 * Columns, and returns a requisition stating the absolute minimum
 * size it can bear to be.
 * 
 * columns_size_allocate() is given an allocation telling it what
 * size the whole container is going to be, and it calls
 * gtk_widget_size_allocate() on all of its (visible) children to
 * set their size and position relative to the top left of the
 * container.
 */

static void columns_size_request(GtkWidget *widget, GtkRequisition *req)
{
    Columns *cols;
    ColumnsChild *child;
    GList *children;
    gint i, ncols, colspan, *colypos;
    const gint *percentages;
    static const gint onecol[] = { 100 };

    g_return_if_fail(widget != NULL);
    g_return_if_fail(IS_COLUMNS(widget));
    g_return_if_fail(req != NULL);

    cols = COLUMNS(widget);

    req->width = 0;
    req->height = cols->spacing;

    ncols = 1;
    colypos = g_new(gint, 1);
    colypos[0] = 0;
    percentages = onecol;

    for (children = cols->children;
         children && (child = children->data);
         children = children->next) {
        GtkRequisition creq;

	if (!child->widget) {
	    /* Column reconfiguration. */
	    for (i = 1; i < ncols; i++) {
		if (colypos[0] < colypos[i])
		    colypos[0] = colypos[i];
	    }
	    ncols = child->ncols;
	    percentages = child->percentages;
	    colypos = g_renew(gint, colypos, ncols);
	    for (i = 1; i < ncols; i++)
		colypos[i] = colypos[0];
	    continue;
	}

        /* Only take visible widgets into account. */
        if (!GTK_WIDGET_VISIBLE(child->widget))
            continue;

        gtk_widget_size_request(child->widget, &creq);
	colspan = child->colspan ? child->colspan : ncols-child->colstart;

        /*
         * To compute width: we know that creq.width plus
         * cols->spacing needs to equal a certain percentage of the
         * full width of the container. So we work this value out,
         * figure out how wide the container will need to be to
         * make that percentage of it equal to that width, and
         * ensure our returned width is at least that much. Very
         * simple really.
         */
        {
            int percent, thiswid, fullwid;

            percent = 0;
            for (i = 0; i < colspan; i++)
                percent += percentages[child->colstart+i];

            thiswid = creq.width + cols->spacing;
            /*
             * Since creq is the _minimum_ size the child needs, we
             * must ensure that it gets _at least_ that size.
             * Hence, when scaling thiswid up to fullwid, we must
             * round up, which means adding percent-1 before
             * dividing by percent.
             */
            fullwid = (thiswid * 100 + percent - 1) / percent;

            /*
             * The above calculation assumes every widget gets
             * cols->spacing on the right. So we subtract
             * cols->spacing here to account for the extra load of
             * spacing on the right.
             */
            if (req->width < fullwid - cols->spacing)
                req->width = fullwid - cols->spacing;
        }

        /*
         * To compute height: the widget's top will be positioned
         * at the largest y value so far reached in any of the
         * columns it crosses. Then it will go down by creq.height
         * plus padding; and the point it reaches at the bottom is
         * the new y value in all those columns, and minus the
         * padding it is also a lower bound on our own size
         * request.
         */
        {
            int topy, boty;

            topy = 0;
            for (i = 0; i < colspan; i++) {
                if (topy < colypos[child->colstart+i])
                    topy = colypos[child->colstart+i];
            }
            boty = topy + creq.height + cols->spacing;
            for (i = 0; i < colspan; i++) {
                colypos[child->colstart+i] = boty;
            }

            if (req->height < boty - cols->spacing)
                req->height = boty - cols->spacing;
        }
    }

    req->width += 2*GTK_CONTAINER(cols)->border_width;
    req->height += 2*GTK_CONTAINER(cols)->border_width;

    g_free(colypos);
}

static void columns_size_allocate(GtkWidget *widget, GtkAllocation *alloc)
{
    Columns *cols;
    ColumnsChild *child;
    GList *children;
    gint i, ncols, colspan, border, *colxpos, *colypos;
    const gint *percentages;
    static const gint onecol[] = { 100 };

    g_return_if_fail(widget != NULL);
    g_return_if_fail(IS_COLUMNS(widget));
    g_return_if_fail(alloc != NULL);

    cols = COLUMNS(widget);
    widget->allocation = *alloc;
    border = GTK_CONTAINER(cols)->border_width;

    ncols = 1;
    percentages = onecol;
    /* colxpos gives the starting x position of each column.
     * We supply n+1 of them, so that we can find the RH edge easily.
     * All ending x positions are expected to be adjusted afterwards by
     * subtracting the spacing. */
    colxpos = g_new(gint, 2);
    colxpos[0] = 0;
    colxpos[1] = alloc->width - 2*border + cols->spacing;
    /* As in size_request, colypos is the lowest y reached in each column. */
    colypos = g_new(gint, 1);
    colypos[0] = 0;

    for (children = cols->children;
         children && (child = children->data);
         children = children->next) {
        GtkRequisition creq;
        GtkAllocation call;

	if (!child->widget) {
	    gint percent;

	    /* Column reconfiguration. */
	    for (i = 1; i < ncols; i++) {
		if (colypos[0] < colypos[i])
		    colypos[0] = colypos[i];
	    }
	    ncols = child->ncols;
	    percentages = child->percentages;
	    colypos = g_renew(gint, colypos, ncols);
	    for (i = 1; i < ncols; i++)
		colypos[i] = colypos[0];
	    colxpos = g_renew(gint, colxpos, ncols + 1);
	    colxpos[0] = 0;
	    percent = 0;
	    for (i = 0; i < ncols; i++) {
		percent += percentages[i];
		colxpos[i+1] = (((alloc->width - 2*border) + cols->spacing)
				* percent / 100);
	    }
	    continue;
	}

        /* Only take visible widgets into account. */
        if (!GTK_WIDGET_VISIBLE(child->widget))
            continue;

        gtk_widget_get_child_requisition(child->widget, &creq);
	colspan = child->colspan ? child->colspan : ncols-child->colstart;

        /*
         * Starting x position is cols[colstart].
         * Ending x position is cols[colstart+colspan] - spacing.
	 * 
	 * Unless we're forcing left, in which case the width is
	 * exactly the requisition width.
         */
        call.x = alloc->x + border + colxpos[child->colstart];
	if (child->force_left)
	    call.width = creq.width;
	else
	    call.width = (colxpos[child->colstart+colspan] -
			  colxpos[child->colstart] - cols->spacing);

        /*
         * To compute height: the widget's top will be positioned
         * at the largest y value so far reached in any of the
         * columns it crosses. Then it will go down by creq.height
         * plus padding; and the point it reaches at the bottom is
         * the new y value in all those columns.
         */
        {
            int topy, boty;

            topy = 0;
            for (i = 0; i < colspan; i++) {
                if (topy < colypos[child->colstart+i])
                    topy = colypos[child->colstart+i];
            }
            call.y = alloc->y + border + topy;
            call.height = creq.height;
            boty = topy + creq.height + cols->spacing;
            for (i = 0; i < colspan; i++) {
                colypos[child->colstart+i] = boty;
            }
        }

        gtk_widget_size_allocate(child->widget, &call);
    }

    g_free(colxpos);
    g_free(colypos);    
}
