/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
 * Copyright (C) 2004      Imendio AB
 * Copyright (C) 2001-2002 CodeFactory AB
 * Copyright (C) 2001-2002 Richard Hult <richard@imendio.com>
 * Copyright (C) 2001-2002 Mikael Hallendal <micke@imendio.com>
 * Copyright (C) 2004      Alvaro del Castillo <acs@barrapunto.com>
 *
 * 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, write to the
 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#include <config.h>
#include <string.h>
#include <time.h>
#include <math.h>
#include <gtk/gtk.h>
#include <gdk/gdkkeysyms.h>
#include <glib/gi18n.h>
#include <libplanner/mrp-task.h>
#include <libplanner/mrp-relation.h>
#include "planner-marshal.h"
#include "planner-conf.h"
#include "planner-gantt-chart.h"
#include "planner-gantt-header.h"
#include "planner-gantt-background.h"
#include "planner-gantt-model.h"
#include "planner-gantt-row.h"
#include "planner-relation-arrow.h"
#include "planner-scale-utils.h"

/* Padding to the left and right of the contents of the gantt chart. */
#define PADDING 100.0

#define ZOOM_IN_LIMIT 12
#define ZOOM_OUT_LIMIT 0

#define DEFAULT_ZOOM_LEVEL 7
#define SCALE(n) (font_width_factor*pow(2,(n)-19))
#define ZOOM(x) (log((x)/font_width_factor)/log(2)+19)

/* Font width factor. */
static gdouble font_width_factor = 1.0;

#define CRITICAL_PATH_KEY  "highlight-critical-path"
#define NOSTDDAYS_PATH_KEY "display-nonstandard-days"
#define GUIDELINES_PATH_KEY "show-guidelines"


typedef struct _TreeNode TreeNode;
typedef void (*TreeFunc) (TreeNode *node, gpointer data);

struct _TreeNode {
	MrpTask          *task;
	GnomeCanvasItem  *item;
	TreeNode         *parent;
	TreeNode        **children;
	guint             num_children;
	guint             expanded : 1;
};

typedef struct {
	gulong   id;
	gpointer instance;
} ConnectData;

struct _PlannerGanttChartPriv {
	GtkWidget       *header;
	GnomeCanvas     *canvas;

	GtkAdjustment   *hadjustment;
	GtkAdjustment   *vadjustment;
	guint            hscroll_policy : 1;
	guint            vscroll_policy : 1;

	GtkTreeModel    *model;
	TreeNode        *tree;
	PlannerTaskTree *view;

	GHashTable      *relation_hash;

	GnomeCanvasItem *background;

	gdouble          zoom;

	gint             row_height;

	/* Cached height. */
	gdouble          height;

	mrptime          project_start;
	mrptime          last_time;

	gboolean         height_changed;
	guint            reflow_idle_id;

	/* Critical path. */
	gboolean         highlight_critical;

	/* Nonstandard days visualization */
	gboolean         nonstandard_days;

	/* GuideLines . */
	gboolean         guidelines;

	/* Keep a list of signal connection ids, so we can remove them
	 * easily.
	 */
	GList           *signal_ids;
};

/* Properties */
enum {
	PROP_0,
	PROP_HEADER_HEIGHT,
	PROP_ROW_HEIGHT,
	PROP_MODEL,
	/* Scrollable interface */
	PROP_HADJUSTMENT,
	PROP_VADJUSTMENT,
	PROP_HSCROLL_POLICY,
	PROP_VSCROLL_POLICY
};

enum {
	STATUS_UPDATED,
	RESOURCE_CLICKED,
	LAST_SIGNAL
};

static void        gantt_chart_class_init               (PlannerGanttChartClass  *klass);
static void        gantt_chart_init                     (PlannerGanttChart       *chart);
static void        gantt_chart_finalize                 (GObject            *object);
static void        gantt_chart_set_property             (GObject            *object,
							 guint               prop_id,
							 const GValue       *value,
							 GParamSpec         *pspec);
static void        gantt_chart_get_property             (GObject            *object,
							 guint               prop_id,
							 GValue             *value,
							 GParamSpec         *pspec);
static void        gantt_chart_destroy                  (GtkWidget          *widget);
static void        gantt_chart_style_updated            (GtkWidget          *widget);
static void        gantt_chart_realize                  (GtkWidget          *widget);
static void        gantt_chart_unrealize                (GtkWidget          *widget);
static void        gantt_chart_map                      (GtkWidget          *widget);
static void        gantt_chart_size_allocate            (GtkWidget          *widget,
							 GtkAllocation      *allocation);
static void        gantt_chart_set_hadjustment          (PlannerGanttChart  *chart,
							 GtkAdjustment      *hadj);
static void        gantt_chart_set_vadjustment          (PlannerGanttChart  *chart,
							 GtkAdjustment      *vadj);
static void        gantt_chart_row_changed              (GtkTreeModel       *model,
							 GtkTreePath        *path,
							 GtkTreeIter        *iter,
							 gpointer            data);
static void        gantt_chart_row_inserted             (GtkTreeModel       *model,
							 GtkTreePath        *path,
							 GtkTreeIter        *iter,
							 gpointer            data);
static void        gantt_chart_remove_children          (PlannerGanttChart  *chart,
							 TreeNode           *node);
static void        gantt_chart_row_deleted              (GtkTreeModel       *model,
							 GtkTreePath        *path,
							 gpointer            data);
static void        gantt_chart_rows_reordered           (GtkTreeModel       *model,
							 GtkTreePath        *parent,
							 GtkTreeIter        *iter,
							 gint               *new_order,
							 gpointer            data);
static void        gantt_chart_relation_added           (MrpTask            *task,
							 MrpRelation        *relation,
							 PlannerGanttChart  *chart);
static void        gantt_chart_relation_removed         (MrpTask            *task,
							 MrpRelation        *relation,
							 PlannerGanttChart  *chart);
static void        gantt_chart_task_removed             (MrpTask            *task,
							 PlannerGanttChart  *chart);
static void        gantt_chart_build_tree               (PlannerGanttChart  *chart);
static void        gantt_chart_reflow                   (PlannerGanttChart  *chart,
							 gboolean            height_changed);
static TreeNode *  gantt_chart_insert_task              (PlannerGanttChart  *chart,
							 GtkTreePath        *path,
							 MrpTask            *task);
static PlannerRelationArrow *
gantt_chart_add_relation                                (PlannerGanttChart  *chart,
							 TreeNode           *task,
							 TreeNode           *predecessor,
							 MrpRelationType     type);
static void        gantt_chart_set_scroll_region        (PlannerGanttChart  *chart,
							 gdouble             x1,
							 gdouble             y1,
							 gdouble             x2,
							 gdouble             y2);
static void        gantt_chart_set_zoom                 (PlannerGanttChart  *chart,
							 gdouble             level);
static gint        gantt_chart_get_width                (PlannerGanttChart  *chart);
static PlannerGanttRow *gantt_chart_get_row_from_task   (PlannerGanttChart  *chart,
							 MrpTask            *task);
static void        gantt_chart_header_date_hint_changed_cb (GtkWidget         *header,
							    const gchar       *hint,
							    PlannerGanttChart *chart);
static TreeNode *  gantt_chart_tree_node_new            (void);
static void        gantt_chart_tree_node_insert_path    (TreeNode           *node,
							 GtkTreePath        *path,
							 TreeNode           *new_node);
static TreeNode *  gantt_chart_tree_node_at_path        (TreeNode           *root,
							 GtkTreePath        *path);
static void        gantt_chart_tree_node_remove         (PlannerGanttChart  *chart,
							 TreeNode           *node);
static void        gantt_chart_tree_traverse            (TreeNode           *node,
							 TreeFunc            func,
							 gpointer            data);

static guint        signals[LAST_SIGNAL];
static GtkBoxClass *parent_class = NULL;


GType
planner_gantt_chart_get_type (void)
{
	static GType type = 0;

	if (!type) {
		static const GTypeInfo info = {
			sizeof (PlannerGanttChartClass),
			NULL,		/* base_init */
			NULL,		/* base_finalize */
			(GClassInitFunc) gantt_chart_class_init,
			NULL,		/* class_finalize */
			NULL,		/* class_data */
			sizeof (PlannerGanttChart),
			0,              /* n_preallocs */
			(GInstanceInitFunc) gantt_chart_init
		};

		static const GInterfaceInfo scrollable_info = {
			(GInterfaceInitFunc) NULL,
			NULL,
			NULL
		};

		type = g_type_register_static (GTK_TYPE_BOX, "PlannerGanttChart",
					       &info, 0);

		g_type_add_interface_static (type, GTK_TYPE_SCROLLABLE, &scrollable_info);
	}

	return type;
}

static void
gantt_chart_class_init (PlannerGanttChartClass *class)
{
	GObjectClass      *o_class;
	GtkWidgetClass    *widget_class;

	parent_class = g_type_class_peek_parent (class);

	o_class         = (GObjectClass *) class;
	widget_class    = (GtkWidgetClass *) class;

	o_class->set_property = gantt_chart_set_property;
	o_class->get_property = gantt_chart_get_property;
	o_class->finalize     = gantt_chart_finalize;

	widget_class->destroy       = gantt_chart_destroy;
	widget_class->style_updated = gantt_chart_style_updated;
	widget_class->realize       = gantt_chart_realize;
	widget_class->map           = gantt_chart_map;
	widget_class->unrealize     = gantt_chart_unrealize;
	widget_class->size_allocate = gantt_chart_size_allocate;

	signals[STATUS_UPDATED] =
		g_signal_new ("status-updated",
			      G_TYPE_FROM_CLASS (class),
			      G_SIGNAL_RUN_LAST,
			      0,
			      NULL, NULL,
			      planner_marshal_VOID__POINTER,
			      G_TYPE_NONE, 1, G_TYPE_POINTER);

	signals[RESOURCE_CLICKED] =
		g_signal_new ("resource-clicked",
			      G_TYPE_FROM_CLASS (class),
			      G_SIGNAL_RUN_LAST,
			      0,
			      NULL, NULL,
			      planner_marshal_VOID__OBJECT,
			      G_TYPE_NONE, 1,
			      MRP_TYPE_RESOURCE);

	/* GtkScrollable interface properties */
	g_object_class_override_property (o_class, PROP_HADJUSTMENT,    "hadjustment");
	g_object_class_override_property (o_class, PROP_VADJUSTMENT,    "vadjustment");
	g_object_class_override_property (o_class, PROP_HSCROLL_POLICY, "hscroll-policy");
	g_object_class_override_property (o_class, PROP_VSCROLL_POLICY, "vscroll-policy");

	/* Properties. */
	g_object_class_install_property (o_class,
					 PROP_MODEL,
					 g_param_spec_object ("model",
							      NULL,
							      NULL,
							      GTK_TYPE_TREE_MODEL,
							      G_PARAM_READWRITE));

	g_object_class_install_property (o_class,
					 PROP_HEADER_HEIGHT,
					 g_param_spec_int ("header-height",
							   NULL,
							   NULL,
							   0, G_MAXINT, 0,
							   G_PARAM_READWRITE));

	g_object_class_install_property (o_class,
					 PROP_ROW_HEIGHT,
					 g_param_spec_int ("row-height",
							   NULL,
							   NULL,
							   0, G_MAXINT, 0,
							   G_PARAM_READWRITE));
}

static void
gantt_chart_init (PlannerGanttChart *chart)
{
	PlannerGanttChartPriv *priv;

	gtk_widget_set_redraw_on_allocate (GTK_WIDGET (chart), FALSE);

	priv = g_new0 (PlannerGanttChartPriv, 1);
	chart->priv = priv;

	priv->tree = gantt_chart_tree_node_new ();

	priv->zoom = DEFAULT_ZOOM_LEVEL;

	priv->height_changed = FALSE;
	priv->reflow_idle_id = 0;

	gtk_box_set_homogeneous (GTK_BOX (chart), FALSE);
	gtk_box_set_spacing (GTK_BOX (chart), 0);

	priv->header = g_object_new (PLANNER_TYPE_GANTT_HEADER,
				     "scale", SCALE (priv->zoom),
				     "zoom", priv->zoom,
				     NULL);

	g_signal_connect (priv->header,
			  "date_hint_changed",
			  G_CALLBACK (gantt_chart_header_date_hint_changed_cb),
			  chart);

	gtk_box_pack_start (GTK_BOX (chart),
			    GTK_WIDGET (priv->header),
			    FALSE,	/* expand */
			    TRUE,	/* fill */
			    0);		/* padding */

	priv->canvas = GNOME_CANVAS (gnome_canvas_new ());
	/* TODO: Consider porting these over to gtk3
	priv->canvas->close_enough = 5;
	gnome_canvas_set_center_scroll_region (priv->canvas, FALSE);
	 */

	/* Easiest way to get access to the chart from the canvas items. */
	g_object_set_data (G_OBJECT (priv->canvas), "chart", chart);

	gtk_box_pack_start (GTK_BOX (chart),
			    GTK_WIDGET (priv->canvas),
			    TRUE,
			    TRUE,
			    0);

	priv->row_height = -1;
	priv->height = -1;
	priv->project_start = MRP_TIME_INVALID;
	priv->last_time = MRP_TIME_INVALID;

	priv->background = gnome_canvas_item_new (gnome_canvas_root (priv->canvas),
						  PLANNER_TYPE_GANTT_BACKGROUND,
						  "scale", SCALE (priv->zoom),
						  "zoom", priv->zoom,
						  NULL);

	priv->relation_hash = g_hash_table_new (NULL, NULL);

	priv->highlight_critical = planner_conf_get_bool (CRITICAL_PATH_KEY,
							  "gantt");
	priv->nonstandard_days   = planner_conf_get_bool (NOSTDDAYS_PATH_KEY,
							  "gantt");
	priv->guidelines = planner_conf_get_bool (GUIDELINES_PATH_KEY, "gantt");

	g_object_set (G_OBJECT(chart->priv->background),
			"show_guidelines",
			priv->guidelines,
			NULL);

	/* TODO: Is this an OK way to share the adjustments? Probably could just set them in set_property instead for simplicity? */
	g_object_bind_property (chart, "hadjustment",
				priv->header, "hadjustment",
				G_BINDING_DEFAULT);
	g_object_bind_property (chart, "hadjustment",
				priv->canvas, "hadjustment",
				G_BINDING_DEFAULT);
	g_object_bind_property (chart, "vadjustment",
				priv->canvas, "vadjustment",
				G_BINDING_DEFAULT);
}

static void
gantt_chart_set_property (GObject      *object,
			  guint         prop_id,
			  const GValue *value,
			  GParamSpec   *pspec)
{
	PlannerGanttChart *chart;

	chart = PLANNER_GANTT_CHART (object);

	switch (prop_id) {
	case PROP_MODEL:
		planner_gantt_chart_set_model (chart, g_value_get_object (value));
		break;
	case PROP_HEADER_HEIGHT:
		g_object_set (chart->priv->header,
			      "height", g_value_get_int (value),
			      NULL);
		break;
	case PROP_ROW_HEIGHT:
		chart->priv->row_height = g_value_get_int (value);
		g_object_set (G_OBJECT(chart->priv->background),
			      "row_height",chart->priv->row_height,
			      NULL);
		gantt_chart_reflow (chart, TRUE);
		break;
	case PROP_HADJUSTMENT:
		gantt_chart_set_hadjustment (chart, g_value_get_object (value));
		break;
	case PROP_VADJUSTMENT:
		gantt_chart_set_vadjustment (chart, g_value_get_object (value));
		break;
	case PROP_HSCROLL_POLICY:
		chart->priv->hscroll_policy = g_value_get_enum (value);
		gtk_widget_queue_resize (GTK_WIDGET (chart));
		break;
	case PROP_VSCROLL_POLICY:
		chart->priv->hscroll_policy = g_value_get_enum (value);
		gtk_widget_queue_resize (GTK_WIDGET (chart));
		break;

	default:
		break;
	}
}

static void
gantt_chart_get_property (GObject    *object,
			  guint       prop_id,
			  GValue     *value,
			  GParamSpec *pspec)
{
	PlannerGanttChart *chart;

	chart = PLANNER_GANTT_CHART (object);

	switch (prop_id) {
	case PROP_MODEL:
		g_value_set_object (value, G_OBJECT (chart->priv->model));
		break;
	case PROP_HADJUSTMENT:
		g_value_set_object (value, chart->priv->hadjustment);
		break;
	case PROP_VADJUSTMENT:
		g_value_set_object (value, chart->priv->vadjustment);
		break;
	case PROP_HSCROLL_POLICY:
		g_value_set_enum (value, chart->priv->hscroll_policy);
		break;
	case PROP_VSCROLL_POLICY:
		g_value_set_enum (value, chart->priv->vscroll_policy);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

static void
gantt_chart_finalize (GObject *object)
{
	PlannerGanttChart *chart = PLANNER_GANTT_CHART (object);

	g_hash_table_destroy (chart->priv->relation_hash);

	g_free (chart->priv);

	if (G_OBJECT_CLASS (parent_class)->finalize) {
		(* G_OBJECT_CLASS (parent_class)->finalize) (object);
	}
}

static void
gantt_chart_destroy (GtkWidget *widget)
{
	PlannerGanttChart *chart = PLANNER_GANTT_CHART (widget);

	planner_gantt_chart_set_model (chart, NULL);

	/* FIXME: free more stuff. */
	if (chart->priv->tree != NULL) {
		gantt_chart_remove_children (chart, chart->priv->tree);
		chart->priv->tree = NULL;
	}

	if (chart->priv->model != NULL) {
		g_object_unref (chart->priv->model);
		chart->priv->model = NULL;
	}

	if (GTK_WIDGET_CLASS (parent_class)->destroy) {
		(* GTK_WIDGET_CLASS (parent_class)->destroy) (widget);
	}
}

static void
gantt_chart_style_updated (GtkWidget *widget)
{
	PlannerGanttChart     *chart;
	PangoContext     *context;
	PangoFontMetrics *metrics;

	g_return_if_fail (PLANNER_IS_GANTT_CHART (widget));

	if (GTK_WIDGET_CLASS (parent_class)->style_updated) {
		GTK_WIDGET_CLASS (parent_class)->style_updated (widget);
	}

	chart = PLANNER_GANTT_CHART (widget);

	context = gtk_widget_get_pango_context (GTK_WIDGET (chart->priv->canvas));

	metrics = pango_context_get_metrics (context, NULL, NULL);

	font_width_factor = 0.2 * pango_font_metrics_get_approximate_char_width (metrics) / PANGO_SCALE;

	pango_font_metrics_unref(metrics);

	/* Re-layout with the new factor. */
	gantt_chart_set_zoom (PLANNER_GANTT_CHART (widget), chart->priv->zoom);
}

static void
gantt_chart_realize (GtkWidget *widget)
{
	PlannerGanttChart     *chart;
	PlannerGanttChartPriv *priv;
	GtkCssProvider        *css_provider;
	GtkStyleContext       *style_context;
	GtkWidget             *canvas;
	GError                *error = NULL;

	chart = PLANNER_GANTT_CHART (widget);
	priv = chart->priv;

	canvas = GTK_WIDGET (priv->canvas);

	if (GTK_WIDGET_CLASS (parent_class)->realize) {
		(* GTK_WIDGET_CLASS (parent_class)->realize) (widget);
	}

	/* Set the background to white. */
	css_provider = gtk_css_provider_new ();
	gtk_css_provider_load_from_data (css_provider,
					 "@define-color theme_bg_color white;",
					 -1, &error);
	style_context = gtk_widget_get_style_context (canvas);
	if (error == NULL) {
		gtk_style_context_add_provider (style_context,
						GTK_STYLE_PROVIDER (css_provider),
						GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
	} else {
		g_warning ("%s: %s", G_STRFUNC, error->message);
		g_clear_error (&error);
	}
	g_object_unref (css_provider);

	gantt_chart_set_zoom (chart, priv->zoom);
}

static void
gantt_chart_unrealize (GtkWidget *widget)
{
	if (GTK_WIDGET_CLASS (parent_class)->unrealize) {
		(* GTK_WIDGET_CLASS (parent_class)->unrealize) (widget);
	}
}

/* We reflow when we are mapped. This updates the chart when changes are made
 * for example in the task view and this view is hidden.
 */
static void
gantt_chart_map (GtkWidget *widget)
{
	PlannerGanttChart *chart;

	chart = PLANNER_GANTT_CHART (widget);

	if (GTK_WIDGET_CLASS (parent_class)->map) {
		(* GTK_WIDGET_CLASS (parent_class)->map) (widget);
	}

	chart->priv->height_changed = TRUE;
	planner_gantt_chart_reflow_now (chart);
}

static void
gantt_chart_size_allocate (GtkWidget     *widget,
			   GtkAllocation *allocation)
{
	PlannerGanttChart *chart;

	GTK_WIDGET_CLASS (parent_class)->size_allocate (widget, allocation);

	chart = PLANNER_GANTT_CHART (widget);

	/* Force reflow (if we are mapped), since it looks smoother with less
	 * jumping around.
	 */
	if (gtk_widget_get_mapped (GTK_WIDGET (chart))) {
		planner_gantt_chart_reflow_now (chart);
	}
}

static void
gantt_chart_set_hadjustment (PlannerGanttChart *chart,
                             GtkAdjustment     *hadj)
{
	if (hadj && chart->priv->hadjustment == hadj)
		return;

	if (hadj)
		g_return_if_fail (GTK_IS_ADJUSTMENT (hadj));
	else
		hadj = GTK_ADJUSTMENT (gtk_adjustment_new (0.0, 0.0, 0.0, 0.0, 0.0, 0.0));

	if (chart->priv->hadjustment != NULL) {
		g_signal_handlers_disconnect_matched (chart->priv->hadjustment,
						      G_SIGNAL_MATCH_DATA,
						      0, 0, NULL, NULL,
						      chart);
		g_object_unref (chart->priv->hadjustment);
	}

	/* TODO: value-changed handler? If not adding, disconnecting above is probably unnecessary too */
	chart->priv->hadjustment = g_object_ref_sink (hadj);
	/* TODO: Do we need to set the initial hadj values? */
	/* TODO: Share the hadjustment with header and canvas? */

	g_object_notify (G_OBJECT (chart), "hadjustment");
}

static void
gantt_chart_set_vadjustment (PlannerGanttChart *chart,
                             GtkAdjustment     *vadj)
{
	if (vadj && chart->priv->vadjustment == vadj)
		return;

	if (vadj)
		g_return_if_fail (GTK_IS_ADJUSTMENT (vadj));
	else
		vadj = GTK_ADJUSTMENT (gtk_adjustment_new (0.0, 0.0, 0.0, 0.0, 0.0, 0.0));

	if (chart->priv->vadjustment != NULL) {
		g_signal_handlers_disconnect_matched (chart->priv->vadjustment,
						      G_SIGNAL_MATCH_DATA,
						      0, 0, NULL, NULL,
						      chart);
		g_object_unref (chart->priv->vadjustment);
	}

	/* TODO: value-changed handler? If not adding, disconnecting above is probably unnecessary too */
	chart->priv->vadjustment = g_object_ref_sink (vadj);
	/* TODO: Do we need to set the initial vadj values? */
	/* TODO: Share the vadjustment with canvas? */

	g_object_notify (G_OBJECT (chart), "vadjustment");
}

static void
gantt_chart_row_changed (GtkTreeModel *model,
			 GtkTreePath  *path,
			 GtkTreeIter  *iter,
			 gpointer      data)
{
#if 0
	gboolean free_path = FALSE;

	g_return_if_fail (path != NULL || iter != NULL);

	if (path == NULL) {
		path = gtk_tree_model_get_path (model, iter);
		free_path = TRUE;
	}
	else if (iter == NULL) {
		gtk_tree_model_get_iter (model, iter, path);
	}

	if (free_path) {
		gtk_tree_path_free (path);
	}
#endif
}

static void
gantt_chart_row_inserted (GtkTreeModel *model,
			  GtkTreePath  *path,
			  GtkTreeIter  *iter,
			  gpointer      data)
{
	PlannerGanttChart     *chart;
	gboolean               free_path = FALSE;
	MrpTask               *task;

	chart = data;

	g_return_if_fail (path != NULL || iter != NULL);

	if (path == NULL) {
		path = gtk_tree_model_get_path (model, iter);
		free_path = TRUE;
	}
	else if (iter == NULL) {
		gtk_tree_model_get_iter (model, iter, path);
	}

	task = planner_gantt_model_get_task (PLANNER_GANTT_MODEL (model), iter);

	gantt_chart_insert_task (chart, path, task);

	gantt_chart_reflow (chart, TRUE);

	if (free_path) {
		gtk_tree_path_free (path);
	}
}

static void
gantt_chart_row_has_child_toggled (GtkTreeModel *model,
				   GtkTreePath  *path,
				   GtkTreeIter  *iter,
				   gpointer      data)
{
	PlannerGanttChart     *chart;
	PlannerGanttChartPriv *priv;
	gboolean               free_path = FALSE;
	TreeNode              *node;

	chart = data;
	priv = chart->priv;

	g_return_if_fail (path != NULL || iter != NULL);

	if (path == NULL) {
		path = gtk_tree_model_get_path (model, iter);
		free_path = TRUE;
	}

	node = gantt_chart_tree_node_at_path (priv->tree, path);
	gnome_canvas_item_request_update (node->item);

	if (free_path) {
		gtk_tree_path_free (path);
	}
}

static void
gantt_chart_remove_children (PlannerGanttChart *chart,
			     TreeNode          *node)
{
	gint i;

	for (i = 0; i < node->num_children; i++) {
		gantt_chart_remove_children (chart, node->children[i]);
	}

	if (node->item) {
		g_object_run_dispose (G_OBJECT (node->item));
		node->item = NULL;
	}
	node->task = NULL;

	g_free (node->children);
	node->children = NULL;
	g_free (node);
}

static void
gantt_chart_row_deleted (GtkTreeModel *model,
			 GtkTreePath  *path,
			 gpointer      data)
{
	PlannerGanttChart *chart = data;
	TreeNode          *node;

	g_return_if_fail (path != NULL);

	node = gantt_chart_tree_node_at_path (chart->priv->tree, path);

	gantt_chart_tree_node_remove (chart, node);
	gantt_chart_remove_children (chart, node);

	gantt_chart_reflow (chart, TRUE);
}

static void
gantt_chart_rows_reordered (GtkTreeModel *model,
			    GtkTreePath  *parent,
			    GtkTreeIter  *iter,
			    gint         *new_order,
			    gpointer      data)
{
#if 0
	gint       len;

	len = gtk_tree_model_iter_n_children (model, iter);

	if (len < 2) {
		return;
	}

	/* FIXME: impl. */
#endif
}

static void
gantt_chart_build_tree_do (PlannerGanttChart *chart,
			   GtkTreeIter       *iter,
			   GHashTable        *hash)
{
	PlannerGanttChartPriv *priv;
	GtkTreeIter            child;
	GtkTreePath           *path;
	MrpTask               *task;
	TreeNode              *node;

	priv = chart->priv;

	do {
		task = planner_gantt_model_get_task (PLANNER_GANTT_MODEL (priv->model), iter);

		path = gtk_tree_model_get_path (priv->model, iter);

		node = gantt_chart_insert_task (chart, path, task);
		g_hash_table_insert (hash, task, node);

		gtk_tree_path_free (path);

		if (gtk_tree_model_iter_children (priv->model, &child, iter)) {
			gantt_chart_build_tree_do (chart, &child, hash);
		}
	} while (gtk_tree_model_iter_next (priv->model, iter));
}

static void
gantt_chart_build_relations (PlannerGanttChart *chart,
			     GtkTreeIter       *iter,
			     GHashTable        *hash)
{
	PlannerGanttChartPriv *priv;
	GtkTreeIter            child;
	MrpTask               *task;
	MrpRelation           *relation;
	MrpTask               *predecessor;
	TreeNode              *task_node;
	TreeNode              *predecessor_node;
	GList                 *relations, *l;
	PlannerRelationArrow  *arrow;
	MrpRelationType	       rel_type;

	priv = chart->priv;

	do {
		task = planner_gantt_model_get_task (
			PLANNER_GANTT_MODEL (priv->model), iter);

		relations = mrp_task_get_predecessor_relations (task);

		for (l = relations; l; l = l->next) {
			relation = l->data;

			predecessor = mrp_relation_get_predecessor (relation);

			task_node = g_hash_table_lookup (hash, task);
			predecessor_node = g_hash_table_lookup (hash, predecessor);

			rel_type = mrp_relation_get_relation_type(relation);

			arrow = gantt_chart_add_relation (chart,
							  task_node,
							  predecessor_node,
							  rel_type);

			g_hash_table_insert (priv->relation_hash, relation, arrow);
		}

		if (gtk_tree_model_iter_children (priv->model, &child, iter)) {
			gantt_chart_build_relations (chart, &child, hash);
		}

	} while (gtk_tree_model_iter_next (priv->model, iter));
}

static void
gantt_chart_build_tree (PlannerGanttChart *chart)
{
	GtkTreeIter  iter;
	GtkTreePath *path;
	GHashTable  *hash;

	path = gtk_tree_path_new_first ();
	if (!gtk_tree_model_get_iter (chart->priv->model, &iter, path)) {
		gtk_tree_path_free (path);
		return;
	}

	hash = g_hash_table_new (NULL, NULL);

	gantt_chart_build_tree_do (chart, &iter, hash);

	gtk_tree_model_get_iter (chart->priv->model, &iter, path);
	gantt_chart_build_relations (chart, &iter, hash);

	gtk_tree_path_free (path);
	g_hash_table_destroy (hash);

	/* FIXME: free paths used as keys. */
}

static gboolean
node_is_visible (TreeNode *node)
{
	g_return_val_if_fail (node->parent != NULL, FALSE);

	while (node->parent) {
		if (!node->parent->expanded) {
			return FALSE;
		}
		node = node->parent;
	}

	return TRUE;
}

static gdouble
gantt_chart_reflow_do (PlannerGanttChart *chart, TreeNode *root, gdouble start_y)
{
	gdouble   row_y;
	TreeNode *node;
	guint     i;
	gint      row_height;

	if (root->children == NULL) {
		return 0;
	}

	node = root->children[0];
	row_y = start_y;

	row_height = chart->priv->row_height;
	if (row_height == -1) {
		row_height = 23;
	}

	for (i = 0; i < root->num_children; i++) {
		node = root->children[i];

		if (node_is_visible (node)) {
			g_object_set (node->item,
				      "y", row_y,
				      "height", (double) row_height,
				      NULL);

			row_y += row_height;

			if (node->children != NULL) {
				row_y += gantt_chart_reflow_do (chart, node, row_y);
			}
		}
	}

	return row_y - start_y;
}

static gboolean
gantt_chart_reflow_idle (PlannerGanttChart *chart)
{
	PlannerGanttChartPriv *priv;
	mrptime                t1, t2;
	gdouble                x1, y1, x2, y2;
	gdouble                width, height;
	gdouble                bx1, bx2;
	GtkAllocation          allocation;

	priv = chart->priv;

	if (priv->height_changed || priv->height == -1) {
		height = gantt_chart_reflow_do (chart, priv->tree, 0);
		priv->height = height;
	} else {
		height = priv->height;
	}

	gtk_widget_get_allocation (GTK_WIDGET (priv->canvas), &allocation);

	t1 = priv->project_start;
	t2 = priv->last_time;

	x1 = t1 * SCALE (priv->zoom) - PADDING;
	x2 = t2 * SCALE (priv->zoom) + PADDING;

	y2 = height;
	y1 = 0;

	width = MAX (x2 - x1, allocation.width - 1.0);
	height = MAX (y2 - y1, allocation.height - 1.0);

	/* Also make sure that everything actually fits horizontally.
	 * Note: this is the only thing that we use ::get_bounds for,
	 * so make sure to not implement that for anything that shouldn't
	 * expand the scroll region.
	 */
	gnome_canvas_item_get_bounds (priv->canvas->root,
				      &bx1, NULL,
				      &bx2, NULL);

	/* Put some padding after the right-most coordinate. */
	bx2 += PADDING;

	width = MAX (width, bx2 - bx1);

	x2 = x1 + width;

	gantt_chart_set_scroll_region (chart,
				       x1,
				       y1,
				       x2,
				       y1 + height);

	if (x1 > -1 && x2 > -1) {
		g_object_set (priv->header,
			      "x1", x1,
			      "x2", x2,
			      NULL);
	}

	priv->height_changed = FALSE;
	priv->reflow_idle_id = 0;

	return FALSE;
}

void
planner_gantt_chart_reflow_now (PlannerGanttChart *chart)
{
	if (! gtk_widget_get_mapped (GTK_WIDGET (chart))) {
		return;
	}

	gantt_chart_reflow_idle (chart);
}

static void
gantt_chart_reflow (PlannerGanttChart *chart, gboolean height_changed)
{
	if (! gtk_widget_get_mapped (GTK_WIDGET (chart))) {
		return;
	}

	chart->priv->height_changed |= height_changed;

	if (chart->priv->reflow_idle_id != 0) {
		return;
	}

	chart->priv->reflow_idle_id = g_idle_add ((GSourceFunc) gantt_chart_reflow_idle, chart);
}

static TreeNode *
gantt_chart_insert_task (PlannerGanttChart *chart,
			 GtkTreePath  *path,
			 MrpTask      *task)
{
	PlannerGanttChartPriv *priv;
	GnomeCanvasItem       *item;
	TreeNode              *tree_node;

	priv = chart->priv;

	item = gnome_canvas_item_new (gnome_canvas_root (priv->canvas),
				      PLANNER_TYPE_GANTT_ROW,
				      "task", task,
				      "scale", SCALE (priv->zoom),
				      "zoom", priv->zoom,
				      NULL);

	tree_node = gantt_chart_tree_node_new ();
	tree_node->item = item;
	tree_node->task = task;

	gantt_chart_tree_node_insert_path (priv->tree, path, tree_node);

	g_signal_connect (task,
			  "relation-added",
			  G_CALLBACK (gantt_chart_relation_added),
			  chart);

	g_signal_connect (task,
			  "relation-removed",
			  G_CALLBACK (gantt_chart_relation_removed),
			  chart);

	g_signal_connect (task,
			  "removed",
			  G_CALLBACK (gantt_chart_task_removed),
			  chart);

	return tree_node;
}

static PlannerRelationArrow *
gantt_chart_add_relation (PlannerGanttChart *chart,
			  TreeNode          *task,
			  TreeNode          *predecessor,
			  MrpRelationType    type)
{
	return planner_relation_arrow_new (PLANNER_GANTT_ROW (task->item),
					   PLANNER_GANTT_ROW (predecessor->item),
					   type);
}

static void
show_hide_descendants (TreeNode *node, gboolean show)
{
	gint i;

	for (i = 0; i < node->num_children; i++) {
		planner_gantt_row_set_visible (PLANNER_GANTT_ROW (node->children[i]->item),
					  show);

		if (!show || (show && node->children[i]->expanded)) {
			show_hide_descendants (node->children[i], show);
		}
	}
}

static void
collapse_descendants (TreeNode *node)
{
	gint i;

	for (i = 0; i < node->num_children; i++) {
		node->children[i]->expanded = FALSE;
		collapse_descendants (node->children[i]);
	}
}

GtkWidget *
planner_gantt_chart_new (void)
{

	return planner_gantt_chart_new_with_model (NULL);
}

GtkWidget *
planner_gantt_chart_new_with_model (GtkTreeModel *model)
{
	PlannerGanttChart *chart;

	chart = PLANNER_GANTT_CHART (g_object_new (PLANNER_TYPE_GANTT_CHART,
						   "orientation", GTK_ORIENTATION_VERTICAL,
						   NULL));

	if (model) {
		planner_gantt_chart_set_model (chart, model);
	}

	return GTK_WIDGET (chart);
}

void
planner_gantt_chart_expand_row (PlannerGanttChart *chart, GtkTreePath *path)
{
	TreeNode *node;

	g_return_if_fail (PLANNER_IS_GANTT_CHART (chart));

	node = gantt_chart_tree_node_at_path (chart->priv->tree, path);

	if (node) {
		node->expanded = TRUE;
		show_hide_descendants (node, TRUE);
		gantt_chart_reflow (chart, TRUE);
	}
}

void
planner_gantt_chart_collapse_row (PlannerGanttChart *chart, GtkTreePath *path)
{
	TreeNode *node;

	g_return_if_fail (PLANNER_IS_GANTT_CHART (chart));

	node = gantt_chart_tree_node_at_path (chart->priv->tree, path);

	if (node) {
		node->expanded = FALSE;
		collapse_descendants (node);
		show_hide_descendants (node, FALSE);
		gantt_chart_reflow (chart, TRUE);
	}
}

static void
gantt_chart_project_start_changed (MrpProject   *project,
				   GParamSpec   *spec,
				   PlannerGanttChart *chart)
{
	mrptime t;

	t = mrp_project_get_project_start (project);
	chart->priv->project_start = t;

	g_object_set (chart->priv->background,
		      "project-start", t,
		      NULL);

	planner_gantt_chart_reflow_now (chart);
}

static void
gantt_chart_root_finish_changed (MrpTask           *root,
				 GParamSpec        *spec,
				 PlannerGanttChart *chart)
{
	chart->priv->last_time = mrp_task_get_finish (root);
	gantt_chart_reflow (chart, FALSE);
}

static void
gantt_chart_relation_added (MrpTask           *task,
			    MrpRelation       *relation,
			    PlannerGanttChart *chart)
{
	GtkTreePath          *task_path;
	GtkTreePath          *predecessor_path;
	TreeNode             *task_node;
	TreeNode             *predecessor_node;
	PlannerRelationArrow *arrow;
	MrpTask              *predecessor;
	MrpRelationType	      rel_type;

	predecessor = mrp_relation_get_predecessor (relation);

	if (g_getenv ("PLANNER_DEBUG_UNDO_TASK")) {
		g_message ("Adding a new relation arrow (%p): %s (%p) -> %s (%p)",
			   relation,
			   mrp_task_get_name (predecessor), predecessor,
			   mrp_task_get_name (task), task);
	}

	if (task == predecessor) {
		/* We are only interested in the successor task. */
		return;
	}

	task_path = planner_gantt_model_get_path_from_task (
		PLANNER_GANTT_MODEL (chart->priv->model), task);
	predecessor_path = planner_gantt_model_get_path_from_task (
		PLANNER_GANTT_MODEL (chart->priv->model),
		predecessor);

	task_node = gantt_chart_tree_node_at_path (chart->priv->tree,
						   task_path);
	predecessor_node = gantt_chart_tree_node_at_path (chart->priv->tree,
							  predecessor_path);

	rel_type = mrp_relation_get_relation_type (relation);

	arrow = gantt_chart_add_relation (chart,
					  task_node,
					  predecessor_node,
					  rel_type);

	g_hash_table_insert (chart->priv->relation_hash, relation, arrow);
}

static void
gantt_chart_relation_removed (MrpTask           *task,
			      MrpRelation       *relation,
			      PlannerGanttChart *chart)
{
	GnomeCanvasItem *arrow;
	MrpTask         *predecessor;

	predecessor = mrp_relation_get_predecessor (relation);

	if (task == predecessor) {
		/* We are only interested in the successor task. */
		return;
	}

	arrow = g_hash_table_lookup (chart->priv->relation_hash, relation);
	if (arrow != NULL) {
		g_hash_table_remove (chart->priv->relation_hash, relation);

		g_object_run_dispose (G_OBJECT (arrow));
		gantt_chart_reflow (chart, FALSE);
	}
}

static void
gantt_chart_task_removed (MrpTask            *task,
			  PlannerGanttChart  *chart)
{
	GList *l, *relations;

	if (g_getenv ("PLANNER_DEBUG_UNDO_TASK")) {
		g_message ("Task removed: %s", mrp_task_get_name (task));
		g_message ("Cleaning signals for task: %s", mrp_task_get_name (task));
	}

	relations = mrp_task_get_predecessor_relations (task);
	for (l = relations; l; l = l->next) {
		gantt_chart_relation_removed (task, l->data, chart);
	}

	relations = mrp_task_get_successor_relations (task);
	for (l = relations; l; l = l->next) {
		gantt_chart_relation_removed (task, l->data, chart);
	}

	g_signal_handlers_disconnect_by_func (task, gantt_chart_relation_added, chart);
	g_signal_handlers_disconnect_by_func (task, gantt_chart_relation_removed, chart);
	g_signal_handlers_disconnect_by_func (task, gantt_chart_task_removed, chart);
}

static gboolean
gantt_chart_task_moved_task_traverse_func (MrpTask *task, PlannerGanttChart *chart)
{
	GList                 *relations;
	GList                 *l;
	MrpRelation           *relation;
	PlannerGanttChartPriv *priv;
	PlannerRelationArrow  *arrow;
	PlannerGanttRow       *row;

	priv = chart->priv;

	row = gantt_chart_get_row_from_task (chart, task);

	relations = mrp_task_get_predecessor_relations (task);
	for (l = relations; l; l = l->next) {
		relation = l->data;

		arrow = g_hash_table_lookup (priv->relation_hash, relation);
		if (arrow) {
			planner_relation_arrow_set_successor (arrow, row);
		}
	}

	relations = mrp_task_get_successor_relations (task);
	for (l = relations; l; l = l->next) {
		relation = l->data;

		arrow = g_hash_table_lookup (priv->relation_hash, relation);
		if (arrow) {
			planner_relation_arrow_set_predecessor (arrow, row);
		}
	}

	return FALSE;
}

static void
gantt_chart_task_moved (MrpProject        *project,
			MrpTask           *task,
			PlannerGanttChart *chart)
{
	/* Note: Seems like we don't need this? */
	/*gantt_chart_reflow_now (chart);*/

	/* When a task has been indented or otherwise moved, it gets re-inserted
	 * in the new place. The relation arrow must be connected to the new
	 * canvas gantt row items. So we check if any of the relations has an
	 * arrow item associated with it, and reconnects if so.
	 */
	mrp_project_task_traverse (project,
				   task,
				   (MrpTaskTraverseFunc) gantt_chart_task_moved_task_traverse_func,
				   chart);
}

static void
gantt_chart_add_signal (PlannerGanttChart *chart, gpointer instance, gulong id)
{
	ConnectData *data;

	data = g_new0 (ConnectData, 1);

	data->instance = instance;
	data->id = id;

	chart->priv->signal_ids = g_list_prepend (chart->priv->signal_ids,
						  data);
}

static void
gantt_chart_disconnect_signals (PlannerGanttChart *chart)
{
	GList       *l;
	ConnectData *data;

	for (l = chart->priv->signal_ids; l; l = l->next) {
		data = l->data;

		g_signal_handler_disconnect (data->instance,
					     data->id);
		g_free (data);
	}

	g_list_free (chart->priv->signal_ids);
	chart->priv->signal_ids = NULL;
}

PlannerTaskTree *
planner_gantt_chart_get_view (PlannerGanttChart *chart)
{
	g_return_val_if_fail (PLANNER_IS_GANTT_CHART (chart), NULL);

	return chart->priv->view;
}

void
planner_gantt_chart_set_view (PlannerGanttChart *chart, PlannerTaskTree *view)
{
	g_return_if_fail (PLANNER_IS_TASK_TREE (view));

	chart->priv->view = view;
}

GtkTreeModel *
planner_gantt_chart_get_model (PlannerGanttChart *chart)
{
	g_return_val_if_fail (PLANNER_IS_GANTT_CHART (chart), NULL);

	return chart->priv->model;
}

void
planner_gantt_chart_set_model (PlannerGanttChart *chart,
			       GtkTreeModel      *model)
{
	PlannerGanttChartPriv *priv;
	MrpTask               *root;
	MrpProject            *project;
	gulong                 id;

	g_return_if_fail (PLANNER_IS_GANTT_CHART (chart));

	priv = chart->priv;

	if (model == priv->model) {
		return;
	}

	if (priv->model) {
		gantt_chart_disconnect_signals (chart);
		g_object_unref (priv->model);
	}

	priv->model = model;

	if (model) {
		g_object_ref (model);

		gantt_chart_build_tree (chart);

		project = planner_gantt_model_get_project (PLANNER_GANTT_MODEL (model));
		root = mrp_project_get_root_task (project);

		g_object_set (priv->background, "project", project, NULL);

		id = g_signal_connect (project,
				       "notify::project-start",
				       G_CALLBACK (gantt_chart_project_start_changed),
				       chart);
		gantt_chart_add_signal (chart, project, id);

		g_signal_connect (root,
				  "notify::finish",
				  G_CALLBACK (gantt_chart_root_finish_changed),
				  chart);

		/* Connect with _after so that we get our event after the model
		 * is done.
		 */
		id = g_signal_connect_after (project,
					     "task-moved",
					     G_CALLBACK (gantt_chart_task_moved),
					     chart);
		gantt_chart_add_signal (chart, project, id);

		id = g_signal_connect (model,
				       "row-changed",
				       G_CALLBACK (gantt_chart_row_changed),
				       chart);
		gantt_chart_add_signal (chart, model, id);

		id = g_signal_connect (model,
				       "row-inserted",
				       G_CALLBACK (gantt_chart_row_inserted),
				       chart);
		gantt_chart_add_signal (chart, model, id);

		id = g_signal_connect (model,
				       "row-deleted",
				       G_CALLBACK (gantt_chart_row_deleted),
				       chart);
		gantt_chart_add_signal (chart, model, id);

		id = g_signal_connect (model,
				       "rows-reordered",
				       G_CALLBACK (gantt_chart_rows_reordered),
				       chart);
		gantt_chart_add_signal (chart, model, id);

		id = g_signal_connect (model,
				       "row-has-child-toggled",
				       G_CALLBACK (gantt_chart_row_has_child_toggled),
				       chart);
		gantt_chart_add_signal (chart, model, id);

		priv->project_start = mrp_project_get_project_start (project);

		g_object_set (priv->background,
			      "project-start", priv->project_start,
			      NULL);

		priv->last_time = mrp_task_get_finish (root);

		/* Force a reflow initially to avoid visible reflow on
		 * start-up .
		 */
		priv->height_changed = TRUE;
		planner_gantt_chart_reflow_now (chart);
	}

	g_object_notify (G_OBJECT (chart), "model");
}

/* TreeNode */

static TreeNode *
gantt_chart_tree_node_new (void)
{
	TreeNode *node;

	node = g_new0 (TreeNode, 1);
	node->expanded = TRUE;

	return node;
}

static void
gantt_chart_tree_node_insert_path (TreeNode *node, GtkTreePath *path, TreeNode *new_node)
{
	gint *indices, i, depth;

	depth = gtk_tree_path_get_depth (path);
	indices = gtk_tree_path_get_indices (path);

	for (i = 0; i < depth - 1; i++) {
		node = node->children[indices[i]];
	}

	node->num_children++;
	node->children = g_realloc (node->children, sizeof (gpointer) * node->num_children);

	if (node->num_children - 1 == indices[i]) {
		/* Don't need to move if the new node is at the end. */
	} else {
		memmove (node->children + indices[i] + 1,
			 node->children + indices[i],
			 sizeof (gpointer) * (node->num_children - indices[i] - 1));
	}

	node->children[indices[i]] = new_node;

	new_node->parent = node;
}

static void
gantt_chart_tree_node_remove (PlannerGanttChart *chart, TreeNode *node)
{
	TreeNode *parent;
	gint     i, pos;

	parent = node->parent;

	pos = -1;
	for (i = 0; i < parent->num_children; i++) {
		if (parent->children[i] == node) {
			pos = i;
			break;
		}
	}

	g_assert (pos != -1);

	memmove (parent->children + pos,
		 parent->children + pos + 1,
		 sizeof (gpointer) * (parent->num_children - pos - 1));

	parent->num_children--;
	parent->children = g_realloc (parent->children, sizeof (gpointer) * parent->num_children);

	if (g_getenv ("PLANNER_DEBUG_UNDO_TASK")) {
		g_message ("Cleaning signals for: %s", mrp_task_get_name (node->task));
	}

	g_signal_handlers_disconnect_by_func (node->task, gantt_chart_relation_added, chart);
	g_signal_handlers_disconnect_by_func (node->task, gantt_chart_relation_removed, chart);
	g_signal_handlers_disconnect_by_func (node->task, gantt_chart_task_removed, chart);

	node->parent = NULL;
}

static TreeNode *
gantt_chart_tree_node_at_path (TreeNode *node, GtkTreePath *path)
{
	gint *indices, i, depth;

	depth = gtk_tree_path_get_depth (path);
	indices = gtk_tree_path_get_indices (path);

	for (i = 0; i < depth; i++) {
		/* FIXME: workaround for a bug in the gantt view, the
		 * row_expanded (and collapsed) should not be handled before we
		 * have inserted the rows.
		 */
		if (node->num_children <= indices[i]) {
			return NULL;
		}

		node = node->children[indices[i]];
	}

	return node;
}

void
planner_gantt_chart_scroll_to (PlannerGanttChart *chart, time_t t)
{
	/*gint x1, x2;*/

	g_return_if_fail (PLANNER_IS_GANTT_CHART (chart));

	/* FIXME: add range check. */

	/* FIXME: "port" to mrptime. */
#if 0
	x1 = chart->priv->project_start * chart->priv->hscale;
	x2 = t * chart->priv->hscale;

	gnome_canvas_scroll_to (chart->priv->canvas, x2 - x1, 0);
#endif
}

static void
gantt_chart_tree_traverse (TreeNode *node, TreeFunc func, gpointer data)
{
	gint      i;
	TreeNode *child;

	for (i = 0; i < node->num_children; i++) {
		child = node->children[i];

		gantt_chart_tree_traverse (child, func, data);
	}

	func (node, data);
}

static void
gantt_chart_set_scroll_region (PlannerGanttChart *chart,
			       gdouble            x1,
			       gdouble            y1,
			       gdouble            x2,
			       gdouble            y2)
{
	GnomeCanvas *canvas;
	gdouble      ox1, oy1, ox2, oy2;

	canvas = chart->priv->canvas;

	gnome_canvas_get_scroll_region (canvas,
					&ox1,
					&oy1,
					&ox2,
					&oy2);

	if (ox1 == x1 && oy1 == y1 && ox2 == x2 && oy2 == y2) {
		return;
	}

	gnome_canvas_set_scroll_region (canvas,
					x1,
					y1,
					x2,
					y2);
}

static PlannerGanttRow *
gantt_chart_get_row_from_task (PlannerGanttChart *chart,
			       MrpTask           *task)
{
	PlannerGanttModel *model;
	GtkTreePath  *path;
	TreeNode     *node;

	model = PLANNER_GANTT_MODEL (chart->priv->model);

	path = planner_gantt_model_get_path_from_task (model, task);

	node = gantt_chart_tree_node_at_path (chart->priv->tree, path);

	gtk_tree_path_free (path);

	return PLANNER_GANTT_ROW (node->item);
}

static void
gantt_chart_header_date_hint_changed_cb (GtkWidget         *header,
					 const gchar       *hint,
					 PlannerGanttChart *chart)
{
	planner_gantt_chart_status_updated (chart, hint);
}

static void
scale_func (TreeNode *node, gpointer data)
{
	PlannerGanttChart     *chart = PLANNER_GANTT_CHART (data);
	PlannerGanttChartPriv *priv = chart->priv;

	if (node->item) {
		gnome_canvas_item_set (GNOME_CANVAS_ITEM (node->item),
				       "scale", SCALE (priv->zoom),
				       "zoom", priv->zoom,
				       NULL);
	}
}

static void
gantt_chart_set_zoom (PlannerGanttChart *chart, gdouble zoom)
{
	PlannerGanttChartPriv *priv;

	priv = chart->priv;

	priv->zoom = zoom;

	gantt_chart_tree_traverse (priv->tree, scale_func, chart);

	g_object_set (priv->header,
		      "scale", SCALE (priv->zoom),
		      "zoom", priv->zoom,
		      NULL);
	gnome_canvas_item_set (GNOME_CANVAS_ITEM (priv->background),
			       "scale", SCALE (priv->zoom),
			       "zoom", priv->zoom,
			       NULL);

	planner_gantt_chart_reflow_now (chart);
}

static mrptime
gantt_chart_get_center (PlannerGanttChart *chart)
{
	PlannerGanttChartPriv *priv;
	GtkAllocation          allocation;
	gint                   x1, width, x;

	priv = chart->priv;

	gnome_canvas_get_scroll_offsets (priv->canvas, &x1, NULL);
	gtk_widget_get_allocation (GTK_WIDGET (priv->canvas), &allocation);
	width = allocation.width;

	x = x1 + width / 2 - PADDING;

	x += floor (priv->project_start * SCALE (priv->zoom) + 0.5);

	return floor (x / SCALE (priv->zoom) + 0.5);
}

static void
gantt_chart_set_center (PlannerGanttChart *chart, mrptime t)
{
	PlannerGanttChartPriv *priv;
	gint                   x, x1, width;
	GtkAllocation          allocation;

	priv = chart->priv;

	x = floor (t * SCALE (priv->zoom) + 0.5);

	gtk_widget_get_allocation (GTK_WIDGET (priv->canvas), &allocation);
	width = allocation.width;

	x1 = x - width / 2 + PADDING;

	x1 -= floor (priv->project_start * SCALE (priv->zoom) + 0.5);

	gnome_canvas_scroll_to (chart->priv->canvas, x1, 0);
}

void
planner_gantt_chart_zoom_in (PlannerGanttChart *chart)
{
	PlannerGanttChartPriv *priv;
	mrptime                mt;

	g_return_if_fail (PLANNER_IS_GANTT_CHART (chart));

	priv = chart->priv;

	mt = gantt_chart_get_center (chart);
	gantt_chart_set_zoom (chart, priv->zoom + 1);
	gantt_chart_set_center (chart, mt);
}

void
planner_gantt_chart_zoom_out (PlannerGanttChart *chart)
{
	PlannerGanttChartPriv *priv;
	mrptime                mt;

	g_return_if_fail (PLANNER_IS_GANTT_CHART (chart));

	priv = chart->priv;

	mt = gantt_chart_get_center (chart);
	gantt_chart_set_zoom (chart, priv->zoom - 1);
	gantt_chart_set_center (chart, mt);
}

void
planner_gantt_chart_can_zoom (PlannerGanttChart *chart,
			 gboolean     *in,
			 gboolean     *out)
{
	PlannerGanttChartPriv *priv;

	g_return_if_fail (PLANNER_IS_GANTT_CHART (chart));

	priv = chart->priv;

	if (in) {
		*in = (priv->zoom < ZOOM_IN_LIMIT);
	}

	if (out) {
		*out = (priv->zoom > ZOOM_OUT_LIMIT);
	}
}

void
planner_gantt_chart_zoom_to_fit (PlannerGanttChart *chart)
{
	gdouble                t;
	gdouble                zoom;
	gdouble                alloc;
	GtkAllocation          allocation;

	g_return_if_fail (PLANNER_IS_GANTT_CHART (chart));

	t = gantt_chart_get_width (chart);
	if (t == -1) {
		return;
	}

	gtk_widget_get_allocation (GTK_WIDGET (chart), &allocation);
	alloc = allocation.width - PADDING * 2;

	zoom = planner_scale_clamp_zoom (ZOOM (alloc / t));
	gantt_chart_set_zoom (chart, zoom);
}

gdouble
planner_gantt_chart_get_zoom (PlannerGanttChart  *chart)
{
	PlannerGanttChartPriv *priv;

	g_return_val_if_fail (PLANNER_IS_GANTT_CHART (chart), 0);

	priv = chart->priv;

	return priv->zoom;
}

static gint
gantt_chart_get_width (PlannerGanttChart *chart)
{
	PlannerGanttChartPriv *priv;

	priv = chart->priv;

	if (priv->project_start == MRP_TIME_INVALID ||
	    priv->last_time == MRP_TIME_INVALID) {
		return -1;
	}

	return priv->last_time - priv->project_start;
}

void
planner_gantt_chart_status_updated (PlannerGanttChart *chart,
				    const gchar       *message)
{
	g_return_if_fail (PLANNER_IS_GANTT_CHART (chart));

	g_signal_emit (chart, signals[STATUS_UPDATED], 0, message);
}

void
planner_gantt_chart_resource_clicked (PlannerGanttChart *chart,
				      MrpResource       *resource)
{
	g_return_if_fail (PLANNER_IS_GANTT_CHART (chart));

	g_signal_emit (chart, signals[RESOURCE_CLICKED], 0, resource);
}

void
planner_gantt_chart_set_highlight_critical_tasks (PlannerGanttChart *chart,
						  gboolean           state)
{
	PlannerGanttChartPriv *priv;

	g_return_if_fail (PLANNER_IS_GANTT_CHART (chart));

	priv = chart->priv;

	if (priv->highlight_critical == state) {
		return;
	}

	priv->highlight_critical = state;

	gtk_widget_queue_draw (GTK_WIDGET (priv->canvas));

	planner_conf_set_bool (CRITICAL_PATH_KEY, state, "gantt");
}

void
planner_gantt_chart_set_show_guidelines (PlannerGanttChart *chart,
				    gboolean           state)
{
	PlannerGanttChartPriv *priv;

	g_return_if_fail (PLANNER_IS_GANTT_CHART (chart));

	priv = chart->priv;

	if (priv->guidelines == state) {
		return;
	}

	priv->guidelines = state;

	g_object_set (G_OBJECT(chart->priv->background),
		      "show_guidelines",
		      state,
		      NULL);

	gtk_widget_queue_draw (GTK_WIDGET (priv->canvas));

	planner_conf_set_bool (GUIDELINES_PATH_KEY, state, "gantt");
}

gboolean
planner_gantt_chart_get_show_guidelines (PlannerGanttChart *chart)
{
	g_return_val_if_fail (PLANNER_IS_GANTT_CHART (chart), FALSE);

	return chart->priv->guidelines;
}

gboolean
planner_gantt_chart_get_highlight_critical_tasks (PlannerGanttChart *chart)
{
	g_return_val_if_fail (PLANNER_IS_GANTT_CHART (chart), FALSE);

	return chart->priv->highlight_critical;
}

void
planner_gantt_chart_set_nonstandard_days (PlannerGanttChart *chart,
						  gboolean           state)
{
	PlannerGanttChartPriv *priv;

	g_return_if_fail (PLANNER_IS_GANTT_CHART (chart));

	priv = chart->priv;

	if (priv->nonstandard_days == state) {
		return;
	}

	priv->nonstandard_days = state;

	gtk_widget_queue_draw (GTK_WIDGET (priv->canvas));

	planner_conf_set_bool (NOSTDDAYS_PATH_KEY, state, "gantt");
}

gboolean
planner_gantt_chart_get_nonstandard_days (PlannerGanttChart *chart)
{
	g_return_val_if_fail (PLANNER_IS_GANTT_CHART (chart), FALSE);

	return chart->priv->nonstandard_days;
}
