/*
 statusbar.c : irssi

    Copyright (C) 1999-2001 Timo Sirainen

    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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/

#include "module.h"
#include <irssi/src/core/signals.h>
#include <irssi/src/core/expandos.h>
#include <irssi/src/core/special-vars.h>

#include <irssi/src/fe-common/core/themes.h>

#include <irssi/src/fe-text/statusbar.h>
#include <irssi/src/fe-text/statusbar-config.h>
#include <irssi/src/fe-text/gui-windows.h>
#include <irssi/src/fe-text/gui-printtext.h>

void statusbar_items_init(void);
void statusbar_items_deinit(void);

GSList *statusbar_groups;
STATUSBAR_GROUP_REC *active_statusbar_group;

/*
   sbar_item_defs: char *name => char *value
   sbar_item_funcs: char *name => STATUSBAR_FUNC func
   sbar_signal_items: int signal_id => GSList *(SBAR_ITEM_REC *items)
   sbar_item_signals: SBAR_ITEM_REC *item => GSList *(int *signal_ids)
   named_sbar_items: const char *name => GSList *(SBAR_ITEM_REC *items)
*/
static GHashTable *sbar_item_defs, *sbar_item_funcs;
static GHashTable *sbar_signal_items, *sbar_item_signals;
static GHashTable *named_sbar_items;
static int statusbar_need_recreate_items;

void statusbar_item_register(const char *name, const char *value,
			     STATUSBAR_FUNC func)
{
	gpointer hkey, hvalue;

	statusbar_need_recreate_items = TRUE;
	if (value != NULL) {
		if (g_hash_table_lookup_extended(sbar_item_defs,
						 name, &hkey, &hvalue)) {
			g_hash_table_remove(sbar_item_defs, name);
			g_free(hkey);
                        g_free(hvalue);
		}
		g_hash_table_insert(sbar_item_defs,
				    g_strdup(name), g_strdup(value));
	}

	if (func != NULL) {
		if (g_hash_table_lookup(sbar_item_funcs, name) == NULL) {
			g_hash_table_insert(sbar_item_funcs,
					    g_strdup(name), (void *) func);
		}
	}
}

void statusbar_item_unregister(const char *name)
{
	gpointer key, value;

	statusbar_need_recreate_items = TRUE;
	if (g_hash_table_lookup_extended(sbar_item_defs,
					 name, &key, &value)) {
		g_hash_table_remove(sbar_item_defs, key);
		g_free(key);
                g_free(value);
	}

	if (g_hash_table_lookup_extended(sbar_item_funcs,
					 name, &key, &value)) {
		g_hash_table_remove(sbar_item_funcs, key);
		g_free(key);
	}
}

void statusbar_item_set_size(struct SBAR_ITEM_REC *item, int min_size, int max_size)
{
	item->min_size = min_size;
	item->max_size = max_size;
}

STATUSBAR_GROUP_REC *statusbar_group_create(const char *name)
{
	STATUSBAR_GROUP_REC *rec;

	rec = g_new0(STATUSBAR_GROUP_REC, 1);
	rec->name = g_strdup(name);

        statusbar_groups = g_slist_append(statusbar_groups, rec);
	return rec;
}

void statusbar_group_destroy(STATUSBAR_GROUP_REC *rec)
{
	statusbar_groups = g_slist_remove(statusbar_groups, rec);

	while (rec->bars != NULL)
		statusbar_destroy(rec->bars->data);
	while (rec->config_bars != NULL)
                statusbar_config_destroy(rec, rec->config_bars->data);

        g_free(rec->name);
        g_free(rec);
}

STATUSBAR_GROUP_REC *statusbar_group_find(const char *name)
{
	GSList *tmp;

	for (tmp = statusbar_groups; tmp != NULL; tmp = tmp->next) {
		STATUSBAR_GROUP_REC *rec = tmp->data;

		if (g_strcmp0(rec->name, name) == 0)
                        return rec;
	}

        return NULL;
}

static int sbar_item_cmp(SBAR_ITEM_REC *item1, SBAR_ITEM_REC *item2)
{
	return item1->config->priority == item2->config->priority ? 0 :
		item1->config->priority < item2->config->priority ? -1 : 1;
}

static int sbar_cmp_position(STATUSBAR_REC *bar1, STATUSBAR_REC *bar2)
{
	return bar1->config->position < bar2->config->position ? -1 : 1;
}

/* Shink all items in statusbar to their minimum requested size.
   The items list should be sorted by priority, highest first. */
static int statusbar_shrink_to_min(GSList *items, int size, int max_width)
{
	GSList *tmp;

	for (tmp = items; tmp != NULL; tmp = tmp->next) {
		SBAR_ITEM_REC *rec = tmp->data;

		size -= (rec->max_size-rec->min_size);
		rec->size = rec->min_size;

		if (size <= max_width) {
			rec->size += max_width-size;
                        break;
		}

		if (rec->size == 0) {
			/* min_size was 0, item removed.
			   remove the marginal too */
                        size--;
		}
	}

        return size;
}

/* shink the items in statusbar, even if their size gets smaller than
   their minimum requested size. The items list should be sorted by
   priority, highest first. */
static void statusbar_shrink_forced(GSList *items, int size, int max_width)
{
	GSList *tmp;

	for (tmp = items; tmp != NULL; tmp = tmp->next) {
		SBAR_ITEM_REC *rec = tmp->data;

		if (size-rec->size > max_width) {
			/* remove the whole item */
                        size -= rec->size;
			rec->size = 0;
		} else {
			/* shrink the item */
			rec->size -= size-max_width;
                        break;
		}
	}
}

static void statusbar_resize_items(STATUSBAR_REC *bar, int max_width)
{
	GSList *tmp, *prior_sorted;
        int width;

        /* first give items their max. size */
	prior_sorted = NULL;
	width = 0;
	for (tmp = bar->items; tmp != NULL; tmp = tmp->next) {
		SBAR_ITEM_REC *rec = tmp->data;

		rec->func(rec, TRUE);
		rec->size = rec->max_size;

		if (rec->size > 0) {
			width += rec->max_size;

			prior_sorted = g_slist_insert_sorted(prior_sorted, rec,
							     (GCompareFunc)
							     sbar_item_cmp);
		}
	}

	if (width > max_width) {
		/* too big, start shrinking from items with lowest priority
		   and shrink until everything fits or until we've shrinked
		   all items. */
		width = statusbar_shrink_to_min(prior_sorted, width,
						max_width);
		if (width > max_width) {
			/* still need to shrink, remove the items with lowest
			   priority until everything fits to screen */
			statusbar_shrink_forced(prior_sorted, width,
						max_width);
		}
	}

	g_slist_free(prior_sorted);
}

#define SBAR_ITEM_REDRAW_NEEDED(_bar, _item, _xpos) \
	(((_bar)->dirty_xpos != -1 && (_xpos) >= (_bar)->dirty_xpos) || \
	 (_item)->xpos != (_xpos) || (_item)->current_size != (_item)->size)

static void statusbar_calc_item_positions(STATUSBAR_REC *bar)
{
	WINDOW_REC *window;
	WINDOW_REC *old_active_win;
	GSList *tmp, *right_items;
	int xpos, rxpos;
	int max_width;

	old_active_win = active_win;
	if (bar->parent_window != NULL)
		active_win = bar->parent_window->active;

	window = bar->parent_window != NULL
		? bar->parent_window->active
		: NULL;

	max_width = window != NULL ? window->width : term_width;
	statusbar_resize_items(bar, max_width);

	/* left-aligned items */
	xpos = 0;
	for (tmp = bar->items; tmp != NULL; tmp = tmp->next) {
		SBAR_ITEM_REC *rec = tmp->data;

		if (!rec->config->right_alignment &&
		    (rec->size > 0 || rec->current_size > 0)) {
			if (SBAR_ITEM_REDRAW_NEEDED(bar, rec, xpos)) {
				/* redraw the item */
				rec->dirty = TRUE;
				if (bar->dirty_xpos == -1 ||
				    xpos < bar->dirty_xpos) {
					irssi_set_dirty();
					bar->dirty = TRUE;
					bar->dirty_xpos = xpos;
				}

				rec->xpos = xpos;
			}
			xpos += rec->size;
		}
	}

	/* right-aligned items - first copy them to a new list backwards,
	   easier to draw them in right order */
	right_items = NULL;
	for (tmp = bar->items; tmp != NULL; tmp = tmp->next) {
		SBAR_ITEM_REC *rec = tmp->data;

		if (rec->config->right_alignment) {
			if (rec->size > 0)
				right_items = g_slist_prepend(right_items, rec);
			else if (rec->current_size > 0 &&
				 (bar->dirty_xpos == -1 ||
				  rec->xpos < bar->dirty_xpos)) {
				/* item was hidden - set the dirty position
				   to begin from the item's old xpos */
				irssi_set_dirty();
				bar->dirty = TRUE;
				bar->dirty_xpos = rec->xpos;
			}
		}
	}

	rxpos = max_width;
	for (tmp = right_items; tmp != NULL; tmp = tmp->next) {
		SBAR_ITEM_REC *rec = tmp->data;

		rxpos -= rec->size;
		if (SBAR_ITEM_REDRAW_NEEDED(bar, rec, rxpos)) {
			rec->dirty = TRUE;
			if (bar->dirty_xpos == -1 ||
			    rxpos < bar->dirty_xpos) {
				irssi_set_dirty();
				bar->dirty = TRUE;
				bar->dirty_xpos = rxpos;
			}
			rec->xpos = rxpos;
		}
	}
	g_slist_free(right_items);

	active_win = old_active_win;
}

void statusbar_redraw(STATUSBAR_REC *bar, int force)
{
	if (statusbar_need_recreate_items)
		return; /* don't bother yet */

	if (bar != NULL) {
		if (force) {
			irssi_set_dirty();
			bar->dirty = TRUE;
                        bar->dirty_xpos = 0;
		}
		statusbar_calc_item_positions(bar);
	} else if (active_statusbar_group != NULL) {
		g_slist_foreach(active_statusbar_group->bars,
				(GFunc) statusbar_redraw,
				GINT_TO_POINTER(force));
	}
}

void statusbar_item_redraw(SBAR_ITEM_REC *item)
{
        WINDOW_REC *old_active_win;

	g_return_if_fail(item != NULL);

	old_active_win = active_win;
        if (item->bar->parent_window != NULL)
		active_win = item->bar->parent_window->active;

	item->func(item, TRUE);

	item->dirty = TRUE;
	item->bar->dirty = TRUE;
	irssi_set_dirty();

	if (item->max_size != item->size) {
		/* item wants a new size - we'll need to redraw
		   the statusbar to see if this is allowed */
		statusbar_redraw(item->bar, item->config->right_alignment);
	}

	active_win = old_active_win;
}

void statusbar_items_redraw(const char *name)
{
	g_slist_foreach(g_hash_table_lookup(named_sbar_items, name),
			(GFunc) statusbar_item_redraw, NULL);
}

static void statusbars_recalc_ypos(STATUSBAR_REC *bar)
{
	GSList *tmp, *bar_group;
        int ypos;

	/* get list of statusbars with same type and placement,
	   sorted by position */
        bar_group = NULL;
	tmp = bar->config->type == STATUSBAR_TYPE_ROOT ? bar->group->bars :
                bar->parent_window->statusbars;

        for (; tmp != NULL; tmp = tmp->next) {
		STATUSBAR_REC *rec = tmp->data;

		if (rec->config->type == bar->config->type &&
		    rec->config->placement == bar->config->placement) {
			bar_group = g_slist_insert_sorted(bar_group, rec,
							  (GCompareFunc)
							  sbar_cmp_position);
		}
	}

	if (bar_group == NULL) {
		/* we just destroyed the last statusbar in this
		   type/placement group */
		return;
	}

        /* get the Y-position for the first statusbar */
	if (bar->config->type == STATUSBAR_TYPE_ROOT) {
		ypos = bar->config->placement == STATUSBAR_TOP ? 0 :
			term_height - g_slist_length(bar_group);
	} else {
		ypos = bar->config->placement == STATUSBAR_TOP ?
			bar->parent_window->first_line :
			bar->parent_window->last_line -
			(g_slist_length(bar_group)-1);
	}

        /* set the Y-positions */
	while (bar_group != NULL) {
		bar = bar_group->data;

		if (bar->real_ypos != ypos) {
			bar->real_ypos = ypos;
                        statusbar_redraw(bar, TRUE);
		}

                ypos++;
                bar_group = g_slist_remove(bar_group, bar_group->data);
	}
}

static void sig_terminal_resized(void)
{
	GSList *tmp;

	for (tmp = active_statusbar_group->bars; tmp != NULL; tmp = tmp->next) {
		STATUSBAR_REC *bar = tmp->data;

		if (bar->config->type == STATUSBAR_TYPE_ROOT &&
		    bar->config->placement == STATUSBAR_BOTTOM) {
			statusbars_recalc_ypos(bar);
                        break;
		}
	}
}

static void mainwindow_recalc_ypos(MAIN_WINDOW_REC *window, int placement)
{
	GSList *tmp;

	for (tmp = window->statusbars; tmp != NULL; tmp = tmp->next) {
		STATUSBAR_REC *bar = tmp->data;

		if (bar->config->placement == placement) {
			statusbars_recalc_ypos(bar);
                        break;
		}
	}
}

static void sig_mainwindow_resized(MAIN_WINDOW_REC *window)
{
	GSList *tmp;
	mainwindow_recalc_ypos(window, STATUSBAR_TOP);
	mainwindow_recalc_ypos(window, STATUSBAR_BOTTOM);
	for (tmp = window->statusbars; tmp != NULL; tmp = tmp->next) {
		STATUSBAR_REC *bar = tmp->data;
		statusbar_redraw(bar, TRUE);
	}
}

STATUSBAR_REC *statusbar_create(STATUSBAR_GROUP_REC *group,
                                STATUSBAR_CONFIG_REC *config,
                                MAIN_WINDOW_REC *parent_window)
{
	STATUSBAR_REC *bar;
	THEME_REC *theme;
        GSList *tmp;
	char *name, *value;

        g_return_val_if_fail(group != NULL, NULL);
        g_return_val_if_fail(config != NULL, NULL);
	g_return_val_if_fail(config->type != STATUSBAR_TYPE_WINDOW ||
			     parent_window != NULL, NULL);

	bar = g_new0(STATUSBAR_REC, 1);
	group->bars = g_slist_append(group->bars, bar);

	bar->group = group;

        bar->config = config;
        bar->parent_window = parent_window;

	irssi_set_dirty();
	bar->dirty = TRUE;
        bar->dirty_xpos = 0;

        signal_remove("terminal resized", (SIGNAL_FUNC) sig_terminal_resized);
	signal_remove("mainwindow resized", (SIGNAL_FUNC) sig_mainwindow_resized);
	signal_remove("mainwindow moved", (SIGNAL_FUNC) sig_mainwindow_resized);

	if (config->type == STATUSBAR_TYPE_ROOT) {
		/* top/bottom of the screen */
		mainwindows_reserve_lines(config->placement == STATUSBAR_TOP,
					  config->placement == STATUSBAR_BOTTOM);
                theme = current_theme;
	} else {
		/* top/bottom of the window */
		parent_window->statusbars =
			g_slist_append(parent_window->statusbars, bar);
		mainwindow_set_statusbar_lines(parent_window,
					       config->placement == STATUSBAR_TOP,
					       config->placement == STATUSBAR_BOTTOM);
		theme = parent_window != NULL && parent_window->active != NULL &&
			parent_window->active->theme != NULL ?
			parent_window->active->theme : current_theme;
	}

        signal_add("terminal resized", (SIGNAL_FUNC) sig_terminal_resized);
	signal_add("mainwindow resized", (SIGNAL_FUNC) sig_mainwindow_resized);
	signal_add("mainwindow moved", (SIGNAL_FUNC) sig_mainwindow_resized);

        /* get background color from sb_background abstract */
        name = g_strdup_printf("{sb_%s_bg}", config->name);
	value = theme_format_expand(theme, name);
	g_free(name);

	if (*value == '\0') {
                /* try with the statusbar group name */
		g_free(value);

		name = g_strdup_printf("{sb_%s_bg}", group->name);
		value = theme_format_expand(theme, name);
		g_free(name);

		if (*value == '\0') {
			/* fallback to default statusbar background
			   (also provides backwards compatibility..) */
                        g_free(value);
			value = theme_format_expand(theme, "{sb_background}");
		}
	}

	if (*value == '\0') {
                g_free(value);
		value = g_strdup("%8");
	}
	bar->color = g_strconcat("%n", value, NULL);
	g_free(value);

        statusbars_recalc_ypos(bar);
        signal_emit("statusbar created", 1, bar);

        /* create the items to statusbar */
	for (tmp = config->items; tmp != NULL; tmp = tmp->next) {
		SBAR_ITEM_CONFIG_REC *rec = tmp->data;

                statusbar_item_create(bar, rec);
	}
	return bar;
}

void statusbar_destroy(STATUSBAR_REC *bar)
{
	int top;

	g_return_if_fail(bar != NULL);

	bar->group->bars = g_slist_remove(bar->group->bars, bar);
	if (bar->parent_window != NULL) {
		bar->parent_window->statusbars =
			g_slist_remove(bar->parent_window->statusbars, bar);
	}

        signal_emit("statusbar destroyed", 1, bar);

	while (bar->items != NULL)
		statusbar_item_destroy(bar->items->data);

        g_free(bar->color);

	if (bar->config->type != STATUSBAR_TYPE_WINDOW ||
	    bar->parent_window != NULL)
		statusbars_recalc_ypos(bar);

	top = bar->config->placement == STATUSBAR_TOP;
	if (bar->config->type == STATUSBAR_TYPE_ROOT) {
		/* top/bottom of the screen */
		mainwindows_reserve_lines(top ? -1 : 0, !top ? -1 : 0);
	} else if (bar->parent_window != NULL) {
		/* top/bottom of the window */
		mainwindow_set_statusbar_lines(bar->parent_window,
					       top ? -1 : 0, !top ? -1 : 0);
	}

	g_free(bar);
}

void statusbar_recreate_items(STATUSBAR_REC *bar)
{
	GSList *tmp;

	/* destroy */
	while (bar->items != NULL)
		statusbar_item_destroy(bar->items->data);

        /* create */
	for (tmp = bar->config->items; tmp != NULL; tmp = tmp->next) {
		SBAR_ITEM_CONFIG_REC *rec = tmp->data;

                statusbar_item_create(bar, rec);
	}

        statusbar_redraw(bar, TRUE);
}

void statusbars_recreate_items(void)
{
	if (active_statusbar_group != NULL) {
		g_slist_foreach(active_statusbar_group->bars,
				(GFunc) statusbar_recreate_items, NULL);
	}
}

STATUSBAR_REC *statusbar_find(STATUSBAR_GROUP_REC *group, const char *name,
			      MAIN_WINDOW_REC *window)
{
	GSList *tmp;

	for (tmp = group->bars; tmp != NULL; tmp = tmp->next) {
		STATUSBAR_REC *rec = tmp->data;

		if (rec->parent_window == window &&
		    g_strcmp0(rec->config->name, name) == 0)
                        return rec;
	}

        return NULL;
}

static const char *statusbar_item_get_value(SBAR_ITEM_REC *item)
{
	const char *value;

	value = item->config->value;
	if (value == NULL) {
		value = g_hash_table_lookup(sbar_item_defs,
					    item->config->name);
	}

        return value;
}

static GString *finalize_string(const char *str, const char *color)
{
	GString *out;

	out = g_string_new(color);

	while (*str != '\0') {
		if ((unsigned char) *str < 32 ||
		    (term_type == TERM_TYPE_8BIT &&
		     (unsigned char) (*str & 0x7f) < 32)) {
			/* control char */
			g_string_append_printf(out, "%%8%c%%8",
					  'A'-1 + (*str & 0x7f));
		} else if (*str == '%' && str[1] == 'n') {
			g_string_append(out, color);
			str++;
		} else {
			g_string_append_c(out, *str);
		}

		str++;
	}

	return out;
}

void statusbar_item_default_handler(SBAR_ITEM_REC *item, int get_size_only,
				    const char *str, const char *data,
				    int escape_vars)
{
	SERVER_REC *server;
	WI_ITEM_REC *wiitem;
	char *tmpstr, *tmpstr2;
	theme_rm_col reset;
	int len;
	strcpy(reset.m, "n");

	if (str == NULL)
		str = statusbar_item_get_value(item);
	if (str == NULL || *str == '\0') {
		item->min_size = item->max_size = 0;
		return;
	}

	if (active_win == NULL) {
		server = NULL;
                wiitem = NULL;
	} else {
		server = active_win->active_server != NULL ?
			active_win->active_server : active_win->connect_server;
		wiitem = active_win->active;
	}

	/* expand templates */
	tmpstr = theme_format_expand_data(current_theme, &str,
					  reset, reset,
					  NULL, NULL,
					  EXPAND_FLAG_ROOT |
					  EXPAND_FLAG_IGNORE_REPLACES |
					  EXPAND_FLAG_IGNORE_EMPTY);
	/* expand $variables */
	tmpstr2 = parse_special_string(tmpstr, server, wiitem, data, NULL,
				       (escape_vars ? PARSE_FLAG_ESCAPE_VARS : 0 ));
        g_free(tmpstr);

	/* remove color codes (not %formats) */
	tmpstr = strip_codes(tmpstr2);
        g_free(tmpstr2);

	if (get_size_only) {
		item->min_size = item->max_size = format_get_length(tmpstr);
	} else {
		GString *out;

		if (item->size < item->min_size) {
                        /* they're forcing us smaller than minimum size.. */
			len = format_real_length(tmpstr, item->size);
                        tmpstr[len] = '\0';
		}
		out = finalize_string(tmpstr, item->bar->color);
		/* make sure the str is big enough to fill the
		   requested size, so it won't corrupt screen */
		len = format_get_length(tmpstr);
		if (len < item->size) {
			int i;

			len = item->size-len;
			for (i = 0; i < len; i++)
				g_string_append_c(out, ' ');
		}

		gui_printtext(ITEM_WINDOW_REAL_XPOS(item), item->bar->real_ypos, out->str);
		g_string_free(out, TRUE);
	}
	g_free(tmpstr);
}

static void statusbar_item_default_func(SBAR_ITEM_REC *item, int get_size_only)
{
	statusbar_item_default_handler(item, get_size_only, NULL, "", TRUE);
}

static void statusbar_update_item(void)
{
	GSList *items;

	items = g_hash_table_lookup(sbar_signal_items,
				    GINT_TO_POINTER(signal_get_emitted_id()));
	while (items != NULL) {
		SBAR_ITEM_REC *item = items->data;

		statusbar_item_redraw(item);
		items = items->next;
	}
}

static void statusbar_update_server(SERVER_REC *server)
{
        SERVER_REC *item_server;
	GSList *items;

	items = g_hash_table_lookup(sbar_signal_items,
				    GINT_TO_POINTER(signal_get_emitted_id()));
	while (items != NULL) {
		SBAR_ITEM_REC *item = items->data;

		item_server = item->bar->parent_window != NULL ?
			item->bar->parent_window->active->active_server :
			active_win->active_server;

		if (item_server == server)
			statusbar_item_redraw(item);

		items = items->next;
	}
}

static void statusbar_update_window(WINDOW_REC *window)
{
        WINDOW_REC *item_window;
	GSList *items;

	items = g_hash_table_lookup(sbar_signal_items,
				    GINT_TO_POINTER(signal_get_emitted_id()));
	while (items != NULL) {
		SBAR_ITEM_REC *item = items->data;

		item_window = item->bar->parent_window != NULL ?
			item->bar->parent_window->active : active_win;

		if (item_window == window)
			statusbar_item_redraw(item);

		items = items->next;
	}
}

static void statusbar_update_window_item(WI_ITEM_REC *wiitem)
{
        WI_ITEM_REC *item_wi;
	GSList *items;

	items = g_hash_table_lookup(sbar_signal_items,
				    GINT_TO_POINTER(signal_get_emitted_id()));
	while (items != NULL) {
		SBAR_ITEM_REC *item = items->data;

		item_wi = item->bar->parent_window != NULL ?
			item->bar->parent_window->active->active :
			active_win->active;

		if (item_wi == wiitem)
			statusbar_item_redraw(item);

		items = items->next;
	}
}

static void statusbar_item_default_signals(SBAR_ITEM_REC *item)
{
	SIGNAL_FUNC func;
        GSList *list;
	const char *value;
        void *signal_id;
        int *signals, *pos;

	value = statusbar_item_get_value(item);
	if (value == NULL)
		return;

	signals = special_vars_get_signals(value);
	if (signals == NULL)
		return;

	for (pos = signals; *pos != -1; pos += 2) {
		/* update signal -> item mappings */
                signal_id = GINT_TO_POINTER(*pos);
		list = g_hash_table_lookup(sbar_signal_items, signal_id);
		if (list == NULL) {
			switch (pos[1]) {
			case EXPANDO_ARG_NONE:
				func = (SIGNAL_FUNC) statusbar_update_item;
				break;
			case EXPANDO_ARG_SERVER:
				func = (SIGNAL_FUNC) statusbar_update_server;
				break;
			case EXPANDO_ARG_WINDOW:
				func = (SIGNAL_FUNC) statusbar_update_window;
				break;
			case EXPANDO_ARG_WINDOW_ITEM:
				func = (SIGNAL_FUNC) statusbar_update_window_item;
				break;
			default:
                                func = NULL;
                                break;
			}
			if (func != NULL) {
				signal_add_full_id(MODULE_NAME,
						   SIGNAL_PRIORITY_DEFAULT,
						   *pos, func, NULL);
			}
		}

		if (g_slist_find(list, item) == NULL)
			list = g_slist_append(list, item);
		g_hash_table_insert(sbar_signal_items, signal_id, list);

                /* update item -> signal mappings */
		list = g_hash_table_lookup(sbar_item_signals, item);
                if (g_slist_find(list, signal_id) == NULL)
			list = g_slist_append(list, signal_id);
		g_hash_table_insert(sbar_item_signals, item, list);
	}
        g_free(signals);
}

SBAR_ITEM_REC *statusbar_item_create(STATUSBAR_REC *bar,
				     SBAR_ITEM_CONFIG_REC *config)
{
	SBAR_ITEM_REC *rec;
        GSList *items;

	g_return_val_if_fail(bar != NULL, NULL);
	g_return_val_if_fail(config != NULL, NULL);

	rec = g_new0(SBAR_ITEM_REC, 1);
	bar->items = g_slist_append(bar->items, rec);

	rec->bar = bar;
	rec->config = config;

	rec->func = (STATUSBAR_FUNC) g_hash_table_lookup(sbar_item_funcs,
							 config->name);
	if (rec->func == NULL)
		rec->func = statusbar_item_default_func;
	statusbar_item_default_signals(rec);

	items = g_hash_table_lookup(named_sbar_items, config->name);
	items = g_slist_append(items, rec);
        g_hash_table_insert(named_sbar_items, config->name, items);

	irssi_set_dirty();
	rec->dirty = TRUE;
	bar->dirty = TRUE;

        signal_emit("statusbar item created", 1, rec);
	return rec;
}

static void statusbar_signal_remove(int signal_id)
{
	signal_remove_id(signal_id, (SIGNAL_FUNC) statusbar_update_item, NULL);
	signal_remove_id(signal_id, (SIGNAL_FUNC) statusbar_update_server, NULL);
	signal_remove_id(signal_id, (SIGNAL_FUNC) statusbar_update_window, NULL);
	signal_remove_id(signal_id, (SIGNAL_FUNC) statusbar_update_window_item, NULL);
}

static void statusbar_item_remove_signal(SBAR_ITEM_REC *item, int signal_id)
{
	GSList *list;

        /* update signal -> item hash */
	list = g_hash_table_lookup(sbar_signal_items,
				   GINT_TO_POINTER(signal_id));
	list = g_slist_remove(list, item);
	if (list != NULL) {
		g_hash_table_insert(sbar_signal_items,
				    GINT_TO_POINTER(signal_id), list);
	} else {
		g_hash_table_remove(sbar_signal_items,
				    GINT_TO_POINTER(signal_id));
                statusbar_signal_remove(signal_id);
	}
}

void statusbar_item_destroy(SBAR_ITEM_REC *item)
{
	GSList *list;

	g_return_if_fail(item != NULL);

	item->bar->items = g_slist_remove(item->bar->items, item);

	list = g_hash_table_lookup(named_sbar_items, item->config->name);
	list = g_slist_remove(list, item);
	if (list == NULL)
		g_hash_table_remove(named_sbar_items, item->config->name);
        else
		g_hash_table_insert(named_sbar_items, item->config->name, list);

        signal_emit("statusbar item destroyed", 1, item);

	list = g_hash_table_lookup(sbar_item_signals, item);
        g_hash_table_remove(sbar_item_signals, item);

	while (list != NULL) {
                statusbar_item_remove_signal(item, GPOINTER_TO_INT(list->data));
		list = g_slist_remove(list, list->data);
	}

	g_free(item);
}

static MAIN_WINDOW_BORDER_REC *set_border_info(STATUSBAR_REC *bar)
{
	MAIN_WINDOW_BORDER_REC *orig_border, *new_border;
	orig_border = clrtoeol_info;
	new_border = g_new0(MAIN_WINDOW_BORDER_REC, 1);
	new_border->window = bar->parent_window != NULL ? bar->parent_window->screen_win : NULL;
	new_border->color = bar->color;
	clrtoeol_info = new_border;
	return orig_border;
}

static void restore_border_info(MAIN_WINDOW_BORDER_REC *border_info)
{
	MAIN_WINDOW_BORDER_REC *old_border;
	old_border = clrtoeol_info;
	clrtoeol_info = border_info;
	g_free(old_border);
}

static void statusbar_redraw_needed_items(STATUSBAR_REC *bar)
{
	WINDOW_REC *old_active_win;
	GSList *tmp;
	char *str;

	old_active_win = active_win;
	if (bar->parent_window != NULL)
		active_win = bar->parent_window->active;

	if (bar->dirty_xpos >= 0) {
		MAIN_WINDOW_BORDER_REC *orig_border;
		orig_border = set_border_info(bar);
		str = g_strconcat(bar->color, "%>", NULL);
		gui_printtext(BAR_WINDOW_REAL_DIRTY_XPOS(bar), bar->real_ypos, str);
		g_free(str);
		restore_border_info(orig_border);
	}

	for (tmp = bar->items; tmp != NULL; tmp = tmp->next) {
		SBAR_ITEM_REC *rec = tmp->data;

		if (rec->dirty ||
		    (bar->dirty_xpos != -1 &&
		     rec->xpos >= bar->dirty_xpos)) {
			rec->current_size = rec->size;
			rec->func(rec, FALSE);
			rec->dirty = FALSE;
		}
	}

	active_win = old_active_win;
}

void statusbar_redraw_dirty(void)
{
	GSList *tmp;

	if (statusbar_need_recreate_items) {
		statusbar_need_recreate_items = FALSE;
		statusbars_recreate_items();
	}

	for (tmp = active_statusbar_group->bars; tmp != NULL; tmp = tmp->next) {
		STATUSBAR_REC *rec = tmp->data;

		if (rec->dirty) {
                        statusbar_redraw_needed_items(rec);
			rec->dirty = FALSE;
			rec->dirty_xpos = -1;
		}
	}
}

#define STATUSBAR_IS_VISIBLE(bar, window) \
	((bar)->visible == STATUSBAR_VISIBLE_ALWAYS || \
	(active_mainwin == (window) && \
	 (bar)->visible == STATUSBAR_VISIBLE_ACTIVE) || \
	(active_mainwin != (window) && \
	 (bar)->visible == STATUSBAR_VISIBLE_INACTIVE))

static void statusbars_remove_unvisible(MAIN_WINDOW_REC *window)
{
	GSList *tmp, *next;

	for (tmp = window->statusbars; tmp != NULL; tmp = next) {
		STATUSBAR_REC *bar = tmp->data;

		next = tmp->next;
                if (!STATUSBAR_IS_VISIBLE(bar->config, window))
                        statusbar_destroy(bar);
	}
}

static void statusbars_add_visible(MAIN_WINDOW_REC *window)
{
	STATUSBAR_GROUP_REC *group;
        STATUSBAR_REC *bar;
	GSList *tmp;

        group = active_statusbar_group;
	for (tmp = group->config_bars; tmp != NULL; tmp = tmp->next) {
		STATUSBAR_CONFIG_REC *config = tmp->data;

		if (config->type == STATUSBAR_TYPE_WINDOW &&
		    STATUSBAR_IS_VISIBLE(config, window) &&
		    statusbar_find(group, config->name, window) == NULL) {
			bar = statusbar_create(group, config, window);
			statusbar_redraw(bar, TRUE);
		}
	}
}

static void sig_mainwindow_destroyed(MAIN_WINDOW_REC *window)
{
	while (window->statusbars != NULL) {
		STATUSBAR_REC *bar = window->statusbars->data;

		bar->parent_window->statusbars =
			g_slist_remove(bar->parent_window->statusbars, bar);
		bar->parent_window = NULL;
		statusbar_destroy(bar);
	}
}

static void sig_window_changed(void)
{
	GSList *tmp;

	for (tmp = mainwindows; tmp != NULL; tmp = tmp->next) {
		MAIN_WINDOW_REC *rec = tmp->data;

		statusbars_remove_unvisible(rec);
                statusbars_add_visible(rec);
	}
}

static void sig_gui_window_created(WINDOW_REC *window)
{
        statusbars_add_visible(WINDOW_MAIN(window));
}

static void statusbar_item_def_destroy(void *key, void *value)
{
	g_free(key);
        g_free(value);
}

static void statusbar_signal_item_destroy(void *key, GSList *value)
{
	while (value != NULL) {
		statusbar_signal_remove(GPOINTER_TO_INT(value->data));
                value->data = g_slist_remove(value, value->data);
	}
}

static void statusbar_item_signal_destroy(void *key, GSList *value)
{
        g_slist_free(value);
}

void statusbars_create_window_bars(void)
{
        g_slist_foreach(mainwindows, (GFunc) statusbars_add_visible, NULL);
}

void statusbar_init(void)
{
        statusbar_need_recreate_items = FALSE;
	statusbar_groups = NULL;
	active_statusbar_group = NULL;
	sbar_item_defs = g_hash_table_new((GHashFunc) g_str_hash,
					  (GCompareFunc) g_str_equal);
	sbar_item_funcs = g_hash_table_new((GHashFunc) g_str_hash,
					   (GCompareFunc) g_str_equal);
	sbar_signal_items = g_hash_table_new((GHashFunc) g_direct_hash,
					     (GCompareFunc) g_direct_equal);
	sbar_item_signals = g_hash_table_new((GHashFunc) g_direct_hash,
					     (GCompareFunc) g_direct_equal);
	named_sbar_items = g_hash_table_new((GHashFunc) g_str_hash,
					    (GCompareFunc) g_str_equal);

        signal_add("terminal resized", (SIGNAL_FUNC) sig_terminal_resized);
	signal_add("mainwindow resized", (SIGNAL_FUNC) sig_mainwindow_resized);
	signal_add("mainwindow moved", (SIGNAL_FUNC) sig_mainwindow_resized);
	signal_add("gui window created", (SIGNAL_FUNC) sig_gui_window_created);
	signal_add("window changed", (SIGNAL_FUNC) sig_window_changed);
	signal_add("mainwindow destroyed", (SIGNAL_FUNC) sig_mainwindow_destroyed);

	statusbar_items_init();
	statusbar_config_init(); /* signals need to be before this call */
}

void statusbar_deinit(void)
{
	while (statusbar_groups != NULL)
		statusbar_group_destroy(statusbar_groups->data);

	g_hash_table_foreach(sbar_item_defs,
			     (GHFunc) statusbar_item_def_destroy, NULL);
	g_hash_table_destroy(sbar_item_defs);

	g_hash_table_foreach(sbar_item_funcs, (GHFunc) g_free, NULL);
	g_hash_table_destroy(sbar_item_funcs);

	g_hash_table_foreach(sbar_signal_items,
			     (GHFunc) statusbar_signal_item_destroy, NULL);
	g_hash_table_destroy(sbar_signal_items);
	g_hash_table_foreach(sbar_item_signals,
			     (GHFunc) statusbar_item_signal_destroy, NULL);
	g_hash_table_destroy(sbar_item_signals);
	g_hash_table_destroy(named_sbar_items);

        signal_remove("terminal resized", (SIGNAL_FUNC) sig_terminal_resized);
	signal_remove("mainwindow resized", (SIGNAL_FUNC) sig_mainwindow_resized);
	signal_remove("mainwindow moved", (SIGNAL_FUNC) sig_mainwindow_resized);
	signal_remove("gui window created", (SIGNAL_FUNC) sig_gui_window_created);
	signal_remove("window changed", (SIGNAL_FUNC) sig_window_changed);
	signal_remove("mainwindow destroyed", (SIGNAL_FUNC) sig_mainwindow_destroyed);

	statusbar_items_deinit();
	statusbar_config_deinit();
}
