/*
 * Copyright (c) 2001-2003, Raphael Manfredi & Richard Eckart
 * Copyright (c) 2017 Raphael Manfredi
 *
 *----------------------------------------------------------------------
 * This file is part of gtk-gnutella.
 *
 *  gtk-gnutella 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.
 *
 *  gtk-gnutella 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 gtk-gnutella; if not, write to the Free Software
 *  Foundation, Inc.:
 *      59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *----------------------------------------------------------------------
 */

/**
 * @ingroup gtk
 * @file
 *
 * General GUI functions and stuff which doesn't fit in anywhere else.
 *
 * @author Raphael Manfredi
 * @date 2017
 *
 * @author Raphael Manfredi
 * @author Richard Eckart
 * @date 2001-2003
 */

#include "gui.h"

#include "gtk-shared/callbacks.h"

#include "gtk/misc.h"

#include "settings.h"

#include "if/gnet_property.h"
#include "if/gui_property.h"
#include "if/gui_property_priv.h"

#include "if/core/net_stats.h"
#include "if/bridge/ui2c.h"

#include "lib/ascii.h"
#include "lib/htable.h"
#include "lib/hset.h"
#include "lib/parse.h"
#include "lib/str.h"
#include "lib/stringify.h"

#include "lib/override.h"		/* Must be the last header included */

/**
 * Associates the objects whose column widths we need to persist with the
 * property holding these column widths.
 */
static htable_t *parent_to_property;

/**
 * Records the mapping between a column and its parent object.
 */
static htable_t *column_to_parent;

/**
 * Records the parent objects whose column widths need to be persisted.
 */
static hset_t *columns_resized;

/*
 * Implementation
 */

void
gui_update_files_scanned(void)
{
    static GtkLabel *label_files_scanned = NULL;
	ulong n = guc_shared_files_scanned();

	if (label_files_scanned == NULL)
		label_files_scanned =
			GTK_LABEL(gui_dlg_prefs_lookup("label_files_scanned"));

	gtk_label_printf(label_files_scanned,
		NG_("%lu file shared (%s)", "%lu files shared (%s)", n),
		n, short_kb_size(guc_shared_kbytes_scanned(), show_metric_units()));
}

void
gui_allow_rescan_dir(gboolean flag)
{
	gtk_widget_set_sensitive
        (gui_dlg_prefs_lookup("button_config_rescan_dir"), flag);
}

/**
 * Record the property holding the column widths of the parent object.
 *
 * @param parent		the object containing columns whose widths we preserve
 * @param prop			the GUI property used to persist column widths
 */
void
gui_parent_widths_saveto(const void *parent, property_t prop)
{
	g_assert(parent != NULL);
	g_assert(gui_prop_get_def(prop));	/* Validates GUI properry */

	/*
	 * Assertion in disguise, to be able to display more context on failures
	 * and loudly warn if we attempt to re-map parent to the same property.
	 */

	if G_UNLIKELY(htable_contains(parent_to_property, parent)) {
		property_t old_prop =
			pointer_to_property(htable_lookup(parent_to_property, parent));

		if (old_prop == prop) {
			s_carp_once("%s(): attempting to re-register parent %p with prop %s",
				G_STRFUNC, parent, gui_prop_name(prop));
			return;
		}

		g_error("%s(): parent=%p, prop=%s, parent already saving to %s",
			G_STRFUNC, parent, gui_prop_name(prop), gui_prop_name(old_prop));
	}

	htable_insert(parent_to_property, parent, property_to_pointer(prop));
}

/**
 * Hash iterator to remove columns whose parent matches the one supplied.
 */
static bool
strip_parent(const void *unused_column, void *parent, void *data)
{
	(void) unused_column;

	if G_LIKELY(parent != data)
		return FALSE;		/* Keep */

	return TRUE;	/* Parent matches, drop from table */
}

/**
 * Forget about parent object (widget being destroyed).
 */
void
gui_parent_forget(const void *parent)
{
	g_assert(parent != NULL);
	g_return_if_fail(htable_contains(parent_to_property, parent));

	htable_remove(parent_to_property, parent);
	hset_remove(columns_resized, parent);
	htable_foreach_remove(
		column_to_parent, strip_parent, deconstify_pointer(parent));
}

/**
 * Map a resizable column to a parent object.
 *
 * The parent object is required to persist the column widths, and only the
 * parent is associated to the property that will capture the widths.
 *
 * This installs a GTK notification that will monitor changes made to the
 * column widths and will trigger persistence of the new widths.
 *
 * @attention
 * It is recommended to call gui_parent_widths_saveto() prior to calling this
 * routine.  This makes sure we are not simply catching these events: we will
 * be able to request a property change to capture the new widths.
 * Failure to do this will result in a loud warning at the time this routine
 * is called!
 *
 * @param column		the column object we want to capture resizing events for
 * @param parent		the parent object containing that column
 */
void
gui_column_map(const void *column, const void *parent)
{
	g_assert(column != NULL);
	g_assert(parent != NULL);

	/*
	 * Assertion in disguise, to be able to display more context on failures.
	 */

	if G_UNLIKELY(htable_contains(column_to_parent, column)) {
		const void *known_parent = htable_lookup(column_to_parent, column);
		property_t parent_prop =
			pointer_to_property(htable_lookup(parent_to_property, parent));
		property_t known_parent_prop =
			pointer_to_property(htable_lookup(parent_to_property, known_parent));

		g_error("%s(): column=%p, parent=%p [%s], already known parent=%p [%s]",
			G_STRFUNC, column, parent,
			parent_prop != NO_PROP ? gui_prop_name(parent_prop) : "?",
			known_parent,
			known_parent_prop != NO_PROP ?
				gui_prop_name(known_parent_prop) : "?"
		);
	}

	/*
	 * Make sure they have already called gui_parent_widths_saveto().
	 * Otherwise, we're going to collect resuzing events for nothing!
	 *
	 * We do not enforce that (it would be OK to do the recording afterwards)
	 * but we loudly warn them since it's likely to be a mistake.
	 */

	if (!htable_contains(parent_to_property, parent)) {
		g_carp_once("%s(): no call to gui_parent_widths_saveto() made yet!",
			G_STRFUNC);
		/* Continue nonetheless, they may do it later */
	}

	htable_insert(column_to_parent, column, deconstify_pointer(parent));

	/*
	 * Ensure gui_column_resized() will be called each time the column
	 * is resized.
	 */

	gui_signal_connect_after(
		deconstify_pointer(column), "notify::width", gui_column_resized, NULL);
}

/**
 * GUI signal callback.
 *
 * Record that a column was changed and that we will need to capture its
 * new width, so as to persist the changes.
 */
void
gui_column_resized(void *column)
{
	void *parent;

	g_return_if_fail(column != NULL);

	parent = htable_lookup(column_to_parent, column);
	g_return_if_fail(parent != NULL);

	hset_insert(columns_resized, parent);
}

/**
 * Hashset iterator to capture new column widths after a change was recorded.
 */
static bool
update_column_widths(const void *parent, void *data)
{
	property_t prop;

	g_return_val_if_fail(parent != NULL, TRUE);

	(void) data;

	prop = pointer_to_property(htable_lookup(parent_to_property, parent));
	g_return_val_if_fail(prop != NO_PROP, TRUE);

	if (GUI_PROPERTY(gui_debug) > 2) {
		g_debug("%s(): updating %s", G_STRFUNC, gui_prop_name(prop));
		g_debug("%s(): before: %s", G_STRFUNC, gui_prop_to_string(prop));
	}

#ifdef USE_GTK2
	tree_view_save_widths(deconstify_pointer(parent), prop);
#else
	clist_save_widths(deconstify_pointer(parent), prop);
#endif

	if (GUI_PROPERTY(gui_debug) > 2)
		g_debug("%s(): after: %s", G_STRFUNC, gui_prop_to_string(prop));

	return TRUE;	/* Remove key from the hash table */
}

/**
 * Update some general information displayed in the gui.
 */
void
gui_general_timer(time_t now)
{
	static GtkLabel *label;
	const gchar *uptime;
	time_t start;

	if (label == NULL) {
		label = GTK_LABEL(gui_main_window_lookup("label_statusbar_uptime"));
	}
	gnet_prop_get_timestamp_val(PROP_START_STAMP, &start);
	uptime = short_uptime(delta_time(now, start));

#ifdef USE_GTK2
	{
		gchar buf[128];

		str_bprintf(buf, sizeof buf, "<tt>%s</tt>", uptime);
		gtk_label_set_use_markup(label, TRUE);
		gtk_label_set_markup(label, buf);
	}
#else

	gtk_label_set_text(label, uptime);
#endif /* USE_GTK2 */

	hset_foreach_remove(columns_resized, update_column_widths, NULL);
}

static void
update_stat(uint64 *max, GtkProgressBar *pg,
    gnet_bw_stats_t *stats, gboolean avg_mode, bool inout)
{
    float frac = 0;
    uint64 high_limit;
    uint64 current;
	uint64 max_bw = *max;
	char buf[128];

    current = avg_mode ? stats->average : stats->current;
    if (max_bw < current)
        max_bw = *max = current;
	else {
		uint64 new_max = stats->average + (stats->average >> 1);	/* 1.5 */
		if (max_bw > new_max)
			max_bw = *max = new_max;
	}

    high_limit = MAX(
        stats->enabled ? stats->limit : max_bw,
        current);
    frac = (high_limit == 0) ? 0 : (float) current / high_limit;

	str_bprintf(buf, sizeof buf, "%s %s %s",
        short_rate(current, show_metric_units()),
        inout ? _("in") : _("out"),
        avg_mode ? _("(avg)") : "");
	gtk_progress_bar_set_text(pg, buf);
    gtk_progress_bar_set_fraction(pg, frac);
}

/**
 * Sum `dest' and `other', putting results in `dest'.
 */
static void
gnet_bw_stats_sum(gnet_bw_stats_t *dest, gnet_bw_stats_t *other)
{
	dest->current += other->current;
	dest->average += other->average;
	dest->limit += other->limit;
}

/* FIXME: stats that are turned off need not be calculated! */
void
gui_update_traffic_stats(void)
{
    static uint64 http_in_max = 0;
    static uint64 http_out_max = 0;
    static uint64 gnet_in_max = 0;
    static uint64 gnet_out_max = 0;
    static uint64 leaf_in_max = 0;
    static uint64 leaf_out_max = 0;
    static uint64 dht_in_max = 0;
    static uint64 dht_out_max = 0;
    gnet_bw_stats_t s;
    gnet_bw_stats_t s2;
    static GtkProgressBar *pg_http_in = NULL;
    static GtkProgressBar *pg_http_out = NULL;
    static GtkProgressBar *pg_gnet_in = NULL;
    static GtkProgressBar *pg_gnet_out = NULL;
    static GtkProgressBar *pg_leaf_in = NULL;
    static GtkProgressBar *pg_leaf_out = NULL;
    static GtkProgressBar *pg_dht_in = NULL;
    static GtkProgressBar *pg_dht_out = NULL;

	if (pg_http_in == NULL) {
		pg_http_in = GTK_PROGRESS_BAR(
			gui_main_window_lookup("progressbar_bws_in"));
		pg_http_out = GTK_PROGRESS_BAR
			(gui_main_window_lookup("progressbar_bws_out"));
		pg_gnet_in = GTK_PROGRESS_BAR
			(gui_main_window_lookup("progressbar_bws_gin"));
		pg_gnet_out = GTK_PROGRESS_BAR
			(gui_main_window_lookup("progressbar_bws_gout"));
		pg_leaf_in = GTK_PROGRESS_BAR
			(gui_main_window_lookup("progressbar_bws_lin"));
		pg_leaf_out = GTK_PROGRESS_BAR
			(gui_main_window_lookup("progressbar_bws_lout"));
		pg_dht_in = GTK_PROGRESS_BAR
			(gui_main_window_lookup("progressbar_bws_dht_in"));
		pg_dht_out = GTK_PROGRESS_BAR
			(gui_main_window_lookup("progressbar_bws_dht_out"));
	}

  	/*
	 * Since gtk_progress does not give us enough control over the format
     * of the displayed values, we have regenerate the value string on each
     * update.
	 *      --BLUE, 21/04/2002
	 */

   	/*
	 * If the bandwidth usage peaks above the maximum, then GTK will not
	 * update the progress bar, so we have to cheat and limit the value
	 * displayed.
	 *		--RAM, 16/04/2002
	 */

    guc_gnet_get_bw_stats(BW_HTTP_IN, &s);
    update_stat(&http_in_max, pg_http_in, &s,
		GUI_PROPERTY(progressbar_bws_in_avg), 1);

    guc_gnet_get_bw_stats(BW_HTTP_OUT, &s);
    update_stat(&http_out_max, pg_http_out, &s,
		GUI_PROPERTY(progressbar_bws_out_avg), 0);

    guc_gnet_get_bw_stats(BW_GNET_IN, &s);
    guc_gnet_get_bw_stats(BW_GNET_UDP_IN, &s2);
	gnet_bw_stats_sum(&s, &s2);
    update_stat(&gnet_in_max, pg_gnet_in, &s,
		GUI_PROPERTY(progressbar_bws_gin_avg), 1);

    guc_gnet_get_bw_stats(BW_GNET_OUT, &s);
    guc_gnet_get_bw_stats(BW_GNET_UDP_OUT, &s2);
	gnet_bw_stats_sum(&s, &s2);
    update_stat(&gnet_out_max, pg_gnet_out, &s,
		GUI_PROPERTY(progressbar_bws_gout_avg), 0);

    guc_gnet_get_bw_stats(BW_LEAF_IN, &s);
    update_stat(&leaf_in_max, pg_leaf_in, &s,
		GUI_PROPERTY(progressbar_bws_glin_avg), 1);

    guc_gnet_get_bw_stats(BW_LEAF_OUT, &s);
    update_stat(&leaf_out_max, pg_leaf_out, &s,
		GUI_PROPERTY(progressbar_bws_glout_avg), 0);

    guc_gnet_get_bw_stats(BW_DHT_IN, &s);
    update_stat(&dht_in_max, pg_dht_in, &s,
		GUI_PROPERTY(progressbar_bws_dht_in_avg), 1);

    guc_gnet_get_bw_stats(BW_DHT_OUT, &s);
    update_stat(&dht_out_max, pg_dht_out, &s,
		GUI_PROPERTY(progressbar_bws_dht_out_avg), 0);
}

/**
 * Utility routine to dynamically resize a widget after some items it holds
 * were hidden, so that the GTK layer can remove any uncessary hole that
 * would remain.
 */
void
gui_shrink_widget_named(const char *name)
{
#ifdef USE_GTK1
	GtkWidget *w = gui_main_window_lookup(name);

	if (w == NULL)
		return;

	gtk_widget_hide(w);
	gtk_widget_show(w);
#else	/* !USE_GTK1 */
	(void) name;
#endif	/* USE_GTK1 */
}

void
gui_update_stats_frames(void)
{
    static GtkWidget *frame_bws_inout = NULL;
    static GtkWidget *frame_bws_ginout = NULL;
    static GtkWidget *frame_bws_glinout = NULL;
    static GtkWidget *frame_bws_dht_inout = NULL;
    guint32 peermode;
	gboolean hidden = FALSE;

	if (frame_bws_inout == NULL) {
		frame_bws_inout = gui_main_window_lookup("frame_bws_inout");
		frame_bws_ginout = gui_main_window_lookup("frame_bws_ginout");
		frame_bws_glinout = gui_main_window_lookup("frame_bws_glinout");
		frame_bws_dht_inout = gui_main_window_lookup("frame_bws_dht_inout");
	}

   	gnet_prop_get_guint32_val(PROP_CURRENT_PEERMODE, &peermode);

    if (
		GUI_PROPERTY(progressbar_bws_in_visible) ||
		GUI_PROPERTY(progressbar_bws_out_visible)
	) {
        gtk_widget_show(frame_bws_inout);
    } else {
        gtk_widget_hide(frame_bws_inout);
		hidden = TRUE;
	}

    if (
		GUI_PROPERTY(progressbar_bws_gin_visible) ||
		GUI_PROPERTY(progressbar_bws_gout_visible)
	) {
        gtk_widget_show(frame_bws_ginout);
    } else {
        gtk_widget_hide(frame_bws_ginout);
		hidden = TRUE;
	}

    if (
		(
			GUI_PROPERTY(progressbar_bws_glin_visible) ||
			GUI_PROPERTY(progressbar_bws_glout_visible)
		) &&
        (peermode == NODE_P_ULTRA || !GUI_PROPERTY(autohide_bws_gleaf))
	) {
        gtk_widget_show(frame_bws_glinout);
    } else {
        gtk_widget_hide(frame_bws_glinout);
		hidden = TRUE;
	}

    if (
		(
			GUI_PROPERTY(progressbar_bws_dht_in_visible) ||
			GUI_PROPERTY(progressbar_bws_dht_out_visible)
		) &&
        (guc_dht_enabled() || !GUI_PROPERTY(autohide_bws_dht))
	) {
        gtk_widget_show(frame_bws_dht_inout);
    } else {
        gtk_widget_hide(frame_bws_dht_inout);
		hidden = TRUE;
	}

	if (hidden)
		gui_shrink_widget_named("vbox_sidebar_stats");
}

/**
 * If the given coordinates are not reasonable, they're appropriately
 * adjusted.
 *
 * @param	coord must point to an array of 4 guint32 values
 *			which describe an position an size [x, y, width, height].
 */
void
gui_fix_coords(guint32 *coord)
{
	gint x, y, w, h;
	gint screen_w, screen_h;

	g_return_if_fail(coord);

	screen_w = gdk_screen_width();
	screen_h = gdk_screen_height();
	if (GUI_PROPERTY(gui_debug))
		g_debug("screen: %dx%d", screen_w, screen_h);

	x = coord[0];
	y = coord[1];
	w = coord[2];
	h = coord[3];
	if (GUI_PROPERTY(gui_debug))
		g_debug("before: %dx%d+%d+%d", w, h, x, y);

	if (w < 200)
		w = MAX(screen_w / 2, 200);
	if (w > (screen_w / 10) * 15)
		w = screen_w;
	if (h < 200)
		h = MAX(screen_h / 2, 200);
	if (h > (screen_h / 10) * 15)
		h = screen_h;
	if (x > screen_w - 32 || x + w < 32)
		x = 0;
	if (y > screen_h - 32 || y + h < 32)
		y = 0;

	coord[0] = x;
	coord[1] = y;
	coord[2] = w;
	coord[3] = h;
	if (GUI_PROPERTY(gui_debug))
		g_debug("after: %dx%d+%d+%d", w, h, x, y);
}

/**
 * Parses an geometry string of the form [WIDTHxHEIGHT][+X+Y]. If the string
 * is not parsable a failure occurs. In case of failure, the coord[] array
 * is not touched. The coord[] array should be initialized with default
 * values before calling this function because the coordinates and the
 * dimensions are both optional and are not touched if omitted.
 *
 * @param spec  The string holding the geometry specification.
 * @param coord An array of four 32-bit integers representing
 *              x, y, width, height.
 * @return 0 on success, -1 on failure.
 */
gint
gui_parse_geometry_spec(const gchar *spec, guint32 coord[4])
{
	const gchar *endptr, *s;
	guint32 x, y, w, h;
	gint error;
	gboolean sign;

	g_return_val_if_fail(spec, -1);
	g_return_val_if_fail(coord, -1);

	x = coord[0];
	y = coord[1];
	w = coord[2];
	h = coord[3];

	s = spec;

	if (is_ascii_digit(s[0])) {
		w = parse_uint32(s, &endptr, 10, &error);
		if (error || 'x' != endptr[0]) {
			return -1;
		}

		s = &endptr[1];
		h = parse_uint32(s, &endptr, 10, &error);
		if (error) {
			return -1;
		}
		s = endptr;
	}

	switch (s[0]) {
	case '-':
		sign = -1;
		break;
	case '+':
		sign = 1;
		break;
	case '\0':
		goto done;
	default:
		return -1;
	}
	s++;

	x = parse_uint32(s, &endptr, 10, &error);
	if (error) {
		return -1;
	}
	s = endptr;
	if (sign < 0) {
		x = gdk_screen_width() - 1 - x - w;
	}

	switch (s[0]) {
	case '-':
		sign = -1;
		break;
	case '+':
		sign = 1;
		break;
	default:
		return -1;
	}
	s++;

	y = parse_uint32(s, &endptr, 10, &error);
	if (error) {
		return -1;
	}
	if (sign < 0) {
		y = gdk_screen_height() - 1 - y - h;
	}

done:
	coord[0] = x;
	coord[1] = y;
	coord[2] = w;
	coord[3] = h;
	return 0;
}

#if !GTK_CHECK_VERSION(2,0,0)
static void
gtk_window_get_position(GtkWindow *widget, int *x, int *y)
{
	g_assert(widget);
	gdk_window_get_root_origin(GTK_WIDGET(widget)->window, x, y);
}

static void
gtk_window_get_size(GtkWindow *widget, int *width, int *height)
{
	g_assert(widget);
	gdk_window_get_size(GTK_WIDGET(widget)->window, width, height);
}

static void
gtk_window_move(GtkWindow *widget, int x, int y)
{
	g_return_if_fail(widget);
	gdk_window_move(GTK_WIDGET(widget)->window, x, y);
}
#endif	/* Gtk+ < 2.x */

static void
gui_window_move_resize(GtkWidget *widget, int x, int y, int width, int height)
{
	g_return_if_fail(widget);
#if GTK_CHECK_VERSION(2,0,0)
	gtk_window_move(GTK_WINDOW(widget), x, y);
	gtk_window_resize(GTK_WINDOW(widget), width, height);
#else
	gdk_window_move_resize(GTK_WIDGET(widget)->window, x, y, width, height);
#endif	/* Gtk+ 2.x */
}


static void
anti_window_shift_hack(GtkWidget *widget, int x, int y, int width, int height)
{
	int ax, ay, dx, dy;

	/* First, move the window to the supposed location. Next make the
	 * window visible by gtk_window_get_position()... */

	gui_window_move_resize(widget, x, y, width, height);

	ax = x;
	ay = y;
	gtk_window_get_position(GTK_WINDOW(widget), &ax, &ay);

	/*
	 * (At least) FVWM2 doesn't take the window decoration into account
	 * when handling positions requests. Readjust the window position
	 * if we detect that the window manager added an offset.
	 */

	dx = ax - x;
	dy = ay - y;
	if (0 == dx && 0 == dy)
		return;

	if (abs(dx) > 64 || abs(dy) > 64)
		return;

	if (GUI_PROPERTY(gui_debug)) {
		g_debug("%s(): x=%d, y=%d, ax=%d, ay=%d , dx=%d, dy=%d",
				G_STRFUNC, x, y, ax, ay, dx, dy);
	}

	gtk_window_move(GTK_WINDOW(widget), x - dx, y - dy);
	gtk_window_get_position(GTK_WINDOW(widget), &ax, &ay);
	if (ax == x && ay == y)
		return;

	g_warning("%s() failed: ax=%d, ay=%d", G_STRFUNC, ax, ay);
}

void
gui_restore_window(GtkWidget *widget, property_t prop)
{
    guint32 coord[4] = { 0, 0, 0, 0 };
	int x, y, width, height;

    gui_prop_get_guint32(prop, coord, 0, N_ITEMS(coord));
	gui_fix_coords(coord);
	x = coord[0];
	y = coord[1];
	width = coord[2];
	height = coord[3];

    /*
     * We need to tell Gtk the size of the window, otherwise we'll get
     * strange side effects when the window is shown (like implicitly
     * resized widgets).
     *      -- Richard, 8/9/2002
     */

    gtk_window_set_default_size(GTK_WINDOW(widget), width, height);
    if (width != 0 && height != 0) {
		anti_window_shift_hack(widget, x, y, width, height);
	}

}

void
gui_save_window(GtkWidget *widget, property_t prop)
{
    guint32 coord[4] = { 0, 0, 0, 0};
	int x, y, w, h;

	gtk_window_get_position(GTK_WINDOW(widget), &x, &y);
	gtk_window_get_size(GTK_WINDOW(widget), &w, &h);
	coord[0] = x;
	coord[1] = y;
	coord[2] = w;
	coord[3] = h;
    gui_prop_set_guint32(prop, coord, 0, N_ITEMS(coord));
}

#ifdef USE_GTK2
/**
 * The following handles UI joining since the glade code is now
 * splitted into several files. Prevents huge UI creation functions
 * and allows GTK2 compilation on some platforms.
 *
 * @author ko (junkpile@free.fr)
 * @date 2003-02-08
 *
 */
typedef struct steal_dict_params {
	GtkWidget *target;
	GtkWidget *source;
} steal_dict_params_t;

/**
 * Transfers the widget dictionary for specified widget
 * from specified window to the main window.
 * If the widget is a container, recursively calls
 * itself on each child.
 *
 */
static void
gui_steal_widget_dict_recursive(GtkWidget *widget, gpointer user_data)
{
	const gchar *name;
	steal_dict_params_t *params = user_data;

	g_return_if_fail(widget);
	g_return_if_fail(user_data);

	name = gtk_widget_get_name(widget);
	if (name != NULL) {
		gpointer data = g_object_steal_data(G_OBJECT(params->source), name);
		if (data != NULL)
			g_object_set_data_full(G_OBJECT(params->target), name,
				data, (GDestroyNotify) gtk_widget_unref);
	}

	if (GTK_IS_CONTAINER(widget))
		gtk_container_foreach(GTK_CONTAINER(widget),
			gui_steal_widget_dict_recursive, user_data);
}

/**
 * Reparents children of specified window into a new notebook tab.
 * Also transfers the widget dictionary to specified toplevel
 * window so lookup_widget() is not broken afterwards.
 *
 * @author ko (junkpile@free.fr)
 * @date 2003-02-08
 */
void
gui_merge_window_as_tab(GtkWidget *toplvl,
	GtkWidget *notebook, GtkWidget *window)
{
	const gchar *title;
	steal_dict_params_t params;
	GList *children = NULL;

	params.target = toplvl;
	params.source = window;

	/*
	 * First recursively steal widget dictionary.
	 */
	gtk_container_foreach(GTK_CONTAINER(window),
		gui_steal_widget_dict_recursive, &params);

	/*
	 * Then reparent the first child of the window,
	 * using the window title as the new tab title.
	 */
	title = gtk_window_get_title(GTK_WINDOW(window));
	children = GLISTTRACK(gtk_container_get_children(GTK_CONTAINER(window)));

	if (children != NULL) {
		GtkWidget *child = GTK_WIDGET(children->data);
		if (child) {
			gtk_widget_reparent(child, notebook);
			gtk_notebook_set_tab_label_text(GTK_NOTEBOOK(notebook),
				child, title);
		}
		g_list_free(children);
	}
}

struct find_iter_by_data_context {
	GtkTreeIter iter;		/**< The iter to initialize */
	gconstpointer data;		/**< The data pointer to look for */
	guint column;			/**< The column to check for data */
	gboolean found;			/**< Set to TRUE when data was found */
};

static gboolean
tree_find_iter_by_data_helper(GtkTreeModel *model, GtkTreePath *unused_path,
	GtkTreeIter *iter, gpointer ctx_ptr)
{
	struct find_iter_by_data_context *ctx = ctx_ptr;
	static const GValue zero_value;
	GValue value = zero_value;

	(void) unused_path;

   	gtk_tree_model_get_value(model, iter, ctx->column, &value);
	g_assert(G_TYPE_POINTER == G_VALUE_TYPE(&value));

	if (g_value_peek_pointer(&value) == ctx->data) {
		ctx->iter = *iter;
		ctx->found = TRUE;
		return TRUE;	/* stop traversal */
	}

	return FALSE; /* continue traversal */
}

gboolean
tree_find_iter_by_data(GtkTreeModel *model,
	guint column, gconstpointer data, GtkTreeIter *iter)
{
	struct find_iter_by_data_context ctx;

	ctx.data = data;
	ctx.found = FALSE;
	ctx.column = column;
	gtk_tree_model_foreach(model, tree_find_iter_by_data_helper, &ctx);

	if (ctx.found && iter)
		*iter = ctx.iter;

	return ctx.found;
}

#endif	/* USE_GTK2 */

void
paned_save_position(GtkPaned *paned, property_t prop)
{
	guint32 pos;

	g_return_if_fail(paned);

	pos = gtk_paned_get_position(paned);
	gui_prop_set_guint32_val(prop, pos);
}

void
paned_restore_position(GtkPaned *paned, property_t prop)
{
	guint32 pos;

	g_return_if_fail(paned);

	gui_prop_get_guint32_val(prop, &pos);
	gtk_paned_set_position(paned, pos);
}

#ifdef USE_GTK2
void
tree_view_save_widths(GtkTreeView *treeview, property_t prop)
{
	gint i;

	g_return_if_fail(treeview);

	for (i = 0; i < INT_MAX; i++) {
		GtkTreeViewColumn *c;
		guint32 width;

		c = gtk_tree_view_get_column(treeview, i);
		if (!c)
			break;

		width = gtk_tree_view_column_get_width(c);
		if ((gint) width > 0)
			gui_prop_set_guint32(prop, &width, i, 1);
	}
}

void
tree_view_restore_widths(GtkTreeView *treeview, property_t prop)
{
	gint i;

	g_return_if_fail(treeview);

	for (i = 0; i < INT_MAX; i++) {
		GtkTreeViewColumn *c;
		guint32 width;

		c = gtk_tree_view_get_column(treeview, i);
		if (!c)
			break;
		gui_prop_get_guint32(prop, &width, i, 1);
		g_object_set(G_OBJECT(c),
			"fixed-width", MAX(1, (gint32) width),
			NULL_PTR);
	}
}

void
tree_view_save_visibility(GtkTreeView *treeview, property_t prop)
{
	guint i;

	g_return_if_fail(treeview);

	for (i = 0; i < INT_MAX; i++) {
		GtkTreeViewColumn *c;
		gboolean val;

		c = gtk_tree_view_get_column(treeview, i);
		if (!c)
			break;
		val = gtk_tree_view_column_get_visible(c);
		gui_prop_set_boolean(prop, &val, i, 1);
	}
}

void
tree_view_restore_visibility(GtkTreeView *treeview, property_t prop)
{
	guint i;

	g_return_if_fail(treeview);

	for (i = 0; i < INT_MAX; i++) {
		GtkTreeViewColumn *c;
		gboolean val;

		c = gtk_tree_view_get_column(treeview, i);
		if (!c)
			break;
		gui_prop_get_boolean(prop, &val, i, 1);
		gtk_tree_view_column_set_visible(c, val);
	}
}
#endif /* USE_GTK2 */

#ifdef USE_GTK1
/**
 * Save widths of columns in given property which must by a boolean array
 * property with at least as many elements as there are columns.
 */
void
clist_save_widths(GtkCList *clist, property_t prop)
{
	gint i;

	g_return_if_fail(clist);

    for (i = 0; i < clist->columns; i++) {
		guint32 width = clist->column[i].width;
		gui_prop_set_guint32(prop, &width, i, 1);
	}
}

/**
 * Restore widths of columns from given property which must by a boolean
 * array property with at least as many elements as there are columns.
 */
void
clist_restore_widths(GtkCList *clist, property_t prop)
{
	gint i;

	g_return_if_fail(clist);

    for (i = 0; i < clist->columns; i++) {
		guint32 width;

		gui_prop_get_guint32(prop, &width, i, 1);
    	gtk_clist_set_column_width(clist, i, width);
	}
}
/**
 * Save visibility of columns in given property which must by a boolean array
 * property with at least as many elements as there are columns.
 */
void
clist_save_visibility(GtkCList *clist, property_t prop)
{
	gint i;

	g_return_if_fail(clist);

    for (i = 0; i < clist->columns; i++) {
		gboolean val = clist->column[i].visible;
		gui_prop_set_boolean(prop, &val, i, 1);
	}
}

/**
 * Restore visibility of columns from given property which must by a boolean
 * array property with at least as many elements as there are columns.
 */
void
clist_restore_visibility(GtkCList *clist, property_t prop)
{
	gint i;

	g_return_if_fail(clist);

    for (i = 0; i < clist->columns; i++) {
		gboolean val;

		gui_prop_get_boolean(prop, &val, i, 1);
    	gtk_clist_set_column_visibility(clist, i, val);
	}
}
#endif /* USE_GTK1 */

static gboolean
show_popup_menu(widget_popup_menu_cb handler)
{
	GtkMenu *menu;

	g_return_val_if_fail(handler, FALSE);

	menu = (*handler)();
	if (menu) {
		int button;

		/*
		 * NOTE: Use 0 as button here even though 3 was probably pressed
		 * (right-click) because under Gtk+ 1.2 the popup will otherwise
		 * immediately disappear on button release if we use 3. Button 1
		 * does not work well for Gtk+ 2.x, thus use 0 then.
		 */
		button = GTK_CHECK_VERSION(2,0,0) ? 0 : 1;
		gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL,
			button, gtk_get_current_event_time());
		return TRUE;
	} else {
		return FALSE;
	}
}

static inline widget_popup_menu_cb
cast_to_widget_popup_menu_cb(const void *p)
{
	return (widget_popup_menu_cb) cast_pointer_to_func(p);
}

#if GTK_CHECK_VERSION(2,0,0)
static gboolean
on_popup_menu(GtkWidget *unused_widget, void *user_data)
{
	(void) unused_widget;
	return show_popup_menu(cast_to_widget_popup_menu_cb(user_data));
}
#endif	/* Gtk+ >= 2.0*/

static gboolean
on_key_press_event(GtkWidget *unused_widget,
	GdkEventKey *event, void *user_data)
{
	(void) unused_widget;

	switch (event->keyval) {
	unsigned modifier;
	case GDK_F10:
	case 0x1008FE0A:	/* Shift+F10 under Xnest */
		modifier = gtk_accelerator_get_default_mod_mask() & event->state;
		if (GDK_SHIFT_MASK == modifier) {
			return show_popup_menu(cast_to_widget_popup_menu_cb(user_data));
		}
		break;
	}
	return FALSE;
}

static gboolean
on_button_press_event(GtkWidget *unused_widget,
	GdkEventButton *event, void *user_data)
{
	(void) unused_widget;

	if (
		GDK_BUTTON_PRESS == event->type &&
		3 == event->button &&
		0 == (gtk_accelerator_get_default_mod_mask() & event->state)
	) {
		return show_popup_menu(cast_to_widget_popup_menu_cb(user_data));
	}
	return FALSE;
}

/**
 * "handler" will be called whenever a popup menu is requested for "widget".
 */
void
widget_add_popup_menu(GtkWidget *widget, widget_popup_menu_cb handler)
{
	void *data = func_to_pointer(handler);

	g_return_if_fail(widget);
	g_return_if_fail(handler);

	GTK_WIDGET_SET_FLAGS(widget, GTK_CAN_FOCUS);
	GTK_WIDGET_SET_FLAGS(widget, GTK_CAN_DEFAULT);
#if GTK_CHECK_VERSION(2,0,0)
	gui_signal_connect(widget, "popup-menu", on_popup_menu, data);
#endif	/* Gtk+ >= 2.0*/

	gui_signal_connect(widget, "key-press-event", on_key_press_event, data);
	gui_signal_connect(widget, "button-press-event", on_button_press_event,
		data);
}

void
misc_gui_early_init(void)
{
	parent_to_property = htable_create(HASH_KEY_SELF, 0);;
	column_to_parent = htable_create(HASH_KEY_SELF, 0);
	columns_resized = hset_create(HASH_KEY_SELF, 0);
}

void
misc_gui_shutdown(void)
{
	htable_free_null(&parent_to_property);
	htable_free_null(&column_to_parent);
	hset_free_null(&columns_resized);
}

/* vi: set ts=4 sw=4 cindent: */
