/* $Id$ */
static char const _copyright[] =
"Copyright © 2013-2020 Pierre Pronchery <khorben@defora.org>";
/* This file is part of DeforaOS Desktop HexEditor */
static char const _license[] =
"This program is free software: you can redistribute it and/or modify\n"
"it under the terms of the GNU General Public License as published by\n"
"the Free Software Foundation, version 3 of the License.\n"
"\n"
"This program is distributed in the hope that it will be useful,\n"
"but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
"MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n"
"GNU General Public License for more details.\n"
"\n"
"You should have received a copy of the GNU General Public License\n"
"along with this program.  If not, see <http://www.gnu.org/licenses/>.";



#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <time.h>
#include <errno.h>
#include <libintl.h>
#include <gtk/gtk.h>
#include <gdk/gdkkeysyms.h>
#include <System.h>
#include <Desktop.h>
#include "HexEditor/plugin.h"
#include "hexeditor.h"
#include "../config.h"
#define _(string) gettext(string)
#define N_(string) string

/* constants */
#ifndef PROGNAME_HEXEDITOR
# define PROGNAME_HEXEDITOR	"hexeditor"
#endif
#ifndef PREFIX
# define PREFIX			"/usr/local"
#endif
#ifndef BINDIR
# define BINDIR			PREFIX "/bin"
#endif


/* HexEditor */
/* private */
/* types */
struct _HexEditor
{
	Config * config;

	char * filename;
	int fd;
	GIOChannel * channel;
	guint source;
	gsize offset;
	gsize size;
	time_t time;

	/* preferences */
	HexEditorPrefs prefs;

	/* widgets */
	GtkWidget * widget;
	GtkWidget * window;
	PangoFontDescription * bold;
#if GTK_CHECK_VERSION(2, 18, 0)
	GtkWidget * infobar;
	GtkWidget * infobar_label;
#endif
	GtkWidget * view_addr;
	GtkTextBuffer * view_addr_tbuf;
	GtkTextIter view_addr_iter;
	GtkWidget * view_hex;
	GtkTextBuffer * view_hex_tbuf;
	GtkTextIter view_hex_iter;
	GtkWidget * view_data;
	GtkTextBuffer * view_data_tbuf;
	GtkTextIter view_data_iter;
	/* progress */
	GtkWidget * pg_window;
	GtkWidget * pg_progress;
	/* plug-ins */
	GtkWidget * pl_view;
	GtkListStore * pl_store;
	GtkWidget * pl_combo;
	GtkWidget * pl_box;
	HexEditorPluginHelper pl_helper;
};


/* constants */
typedef enum _HexEditorPluginColumn
{
	HEPC_NAME = 0,
	HEPC_ENABLED,
	HEPC_ICON,
	HEPC_NAME_DISPLAY,
	HEPC_PLUGIN,
	HEPC_HEXEDITORPLUGINDEFINITION,
	HEPC_HEXEDITORPLUGIN,
	HEPC_WIDGET
} HexEditorPluginColumn;
#define HEPC_LAST	HEPC_WIDGET
#define HEPC_COUNT	(HEPC_LAST + 1)


/* prototypes */
/* accessors */
static String * _hexeditor_get_config_filename(char const * filename);
static int _hexeditor_plugin_is_enabled(HexEditor * hexeditor,
		char const * plugin);

/* useful */
static void _hexeditor_close(HexEditor * hexeditor, gboolean plugins);
static int _hexeditor_config_load(HexEditor * hexeditor);
static int _hexeditor_error(HexEditor * hexeditor, char const * message,
		int ret);

/* callbacks */
static void _hexeditor_on_open(gpointer data);
static void _hexeditor_on_plugin_combo_change(gpointer data);
#ifdef EMBEDDED
static void _hexeditor_on_preferences(gpointer data);
#endif
static void _hexeditor_on_progress_cancel(gpointer data);
static gboolean _hexeditor_on_progress_delete(gpointer data);
#ifdef EMBEDDED
static void _hexeditor_on_properties(gpointer data);
#endif


/* variables */
static DesktopToolbar _hexeditor_toolbar[] =
{
	{ N_("Open"), G_CALLBACK(_hexeditor_on_open), GTK_STOCK_OPEN, 0, 0,
		NULL },
#ifdef EMBEDDED
	{ "", NULL, NULL, 0, 0, NULL },
	{ N_("Properties"), G_CALLBACK(_hexeditor_on_properties),
		GTK_STOCK_PROPERTIES, GDK_MOD1_MASK, GDK_KEY_Return, NULL },
	{ "", NULL, NULL, 0, 0, NULL },
	{ N_("Preferences"), G_CALLBACK(_hexeditor_on_preferences),
		GTK_STOCK_PREFERENCES, GDK_CONTROL_MASK, GDK_KEY_P, NULL },
#endif
	{ NULL, NULL, NULL, 0, 0, NULL }
};


/* public */
/* functions */
/* hexeditor_new */
static void _new_plugins(HexEditor * hexeditor);
static void _new_progress(HexEditor * hexeditor);

HexEditor * hexeditor_new(GtkWidget * window, GtkAccelGroup * group,
		HexEditorPrefs * prefs, char const * filename)
{
	HexEditor * hexeditor;
	GtkWidget * vbox;
	GtkWidget * hpaned;
	GtkWidget * hbox;
	GtkWidget * widget;
	GtkAdjustment * adjustment;
	char const * p;

	if((hexeditor = object_new(sizeof(*hexeditor))) == NULL)
		return NULL;
	/* default preferences */
	hexeditor->prefs.uppercase = 0;
	if((hexeditor->config = config_new()) == NULL
			|| _hexeditor_config_load(hexeditor) != 0)
		_hexeditor_error(NULL, _("Error while loading configuration"),
				1);
	hexeditor->filename = NULL;
	hexeditor->fd = -1;
	hexeditor->channel = NULL;
	hexeditor->source = 0;
	hexeditor->offset = 0;
	hexeditor->size = 0;
	hexeditor->time = 0;
	if(prefs != NULL)
		hexeditor->prefs = *prefs;
	hexeditor->bold = pango_font_description_new();
	pango_font_description_set_weight(hexeditor->bold, PANGO_WEIGHT_BOLD);
	hexeditor->window = window;
	/* create the widget */
	hexeditor->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
	vbox = hexeditor->widget;
	/* toolbar */
	widget = desktop_toolbar_create(_hexeditor_toolbar, hexeditor, group);
	gtk_box_pack_start(GTK_BOX(vbox), widget, FALSE, TRUE, 0);
#if GTK_CHECK_VERSION(2, 18, 0)
	/* infobar */
	hexeditor->infobar = gtk_info_bar_new_with_buttons(GTK_STOCK_CLOSE,
			GTK_RESPONSE_CLOSE, NULL);
	gtk_info_bar_set_message_type(GTK_INFO_BAR(hexeditor->infobar),
			GTK_MESSAGE_ERROR);
	g_signal_connect(hexeditor->infobar, "close", G_CALLBACK(
				gtk_widget_hide), NULL);
	g_signal_connect(hexeditor->infobar, "response", G_CALLBACK(
				gtk_widget_hide), NULL);
	widget = gtk_info_bar_get_content_area(GTK_INFO_BAR(
				hexeditor->infobar));
	hexeditor->infobar_label = gtk_label_new(NULL);
	gtk_widget_show(hexeditor->infobar_label);
	gtk_box_pack_start(GTK_BOX(widget), hexeditor->infobar_label, TRUE,
			TRUE, 0);
	gtk_widget_set_no_show_all(hexeditor->infobar, TRUE);
	gtk_box_pack_start(GTK_BOX(vbox), hexeditor->infobar, FALSE, TRUE, 0);
#endif
	/* view */
	hpaned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
	gtk_paned_set_position(GTK_PANED(hpaned), 500);
	hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
	/* view: address */
	widget = gtk_scrolled_window_new(NULL, NULL);
	gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(widget),
			GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
	adjustment = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(
				widget));
	hexeditor->view_addr = gtk_text_view_new();
	hexeditor->view_addr_tbuf = gtk_text_view_get_buffer(
			GTK_TEXT_VIEW(hexeditor->view_addr));
	gtk_text_buffer_get_end_iter(hexeditor->view_addr_tbuf,
			&hexeditor->view_addr_iter);
	gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(hexeditor->view_addr),
			FALSE);
	gtk_text_view_set_editable(GTK_TEXT_VIEW(hexeditor->view_addr), FALSE);
	gtk_container_add(GTK_CONTAINER(widget), hexeditor->view_addr);
	gtk_box_pack_start(GTK_BOX(hbox), widget, FALSE, TRUE, 0);
	/* view: hexadecimal */
	widget = gtk_scrolled_window_new(NULL, adjustment);
	gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(widget),
			GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
	hexeditor->view_hex = gtk_text_view_new();
	hexeditor->view_hex_tbuf = gtk_text_view_get_buffer(
			GTK_TEXT_VIEW(hexeditor->view_hex));
	gtk_text_buffer_get_end_iter(hexeditor->view_hex_tbuf,
			&hexeditor->view_hex_iter);
	gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(hexeditor->view_hex),
			FALSE);
	gtk_text_view_set_editable(GTK_TEXT_VIEW(hexeditor->view_hex), FALSE);
	gtk_container_add(GTK_CONTAINER(widget), hexeditor->view_hex);
	gtk_box_pack_start(GTK_BOX(hbox), widget, TRUE, TRUE, 4);
	/* view: data */
	widget = gtk_scrolled_window_new(NULL, adjustment);
	gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(widget),
			GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
	hexeditor->view_data = gtk_text_view_new();
	hexeditor->view_data_tbuf = gtk_text_view_get_buffer(
			GTK_TEXT_VIEW(hexeditor->view_data));
	gtk_text_buffer_get_end_iter(hexeditor->view_data_tbuf,
			&hexeditor->view_data_iter);
	gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(hexeditor->view_data),
			FALSE);
	gtk_text_view_set_editable(GTK_TEXT_VIEW(hexeditor->view_data), FALSE);
	gtk_container_add(GTK_CONTAINER(widget), hexeditor->view_data);
	gtk_box_pack_start(GTK_BOX(hbox), widget, FALSE, TRUE, 0);
	gtk_paned_add1(GTK_PANED(hpaned), hbox);
	gtk_box_pack_start(GTK_BOX(vbox), hpaned, TRUE, TRUE, 0);
	p = (hexeditor->config != NULL)
		? config_get(hexeditor->config, NULL, "font") : NULL;
	hexeditor_set_font(hexeditor, p);
	gtk_widget_set_sensitive(hexeditor->view_addr, FALSE);
	gtk_widget_set_sensitive(hexeditor->view_hex, FALSE);
	gtk_widget_set_sensitive(hexeditor->view_data, FALSE);
	_new_progress(hexeditor);
	_new_plugins(hexeditor);
	gtk_paned_add2(GTK_PANED(hpaned), hexeditor->pl_view);
	if(filename != NULL)
		hexeditor_open(hexeditor, filename);
	gtk_widget_show_all(vbox);
	return hexeditor;
}

static void _new_plugins(HexEditor * hexeditor)
{
	GtkCellRenderer * renderer;
	char const * plugins;
	char * p;
	char * q;
	size_t i;

	hexeditor->pl_view = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
	gtk_container_set_border_width(GTK_CONTAINER(hexeditor->pl_view), 4);
	gtk_widget_set_no_show_all(hexeditor->pl_view, TRUE);
	hexeditor->pl_store = gtk_list_store_new(HEPC_COUNT, G_TYPE_STRING,
			G_TYPE_BOOLEAN, GDK_TYPE_PIXBUF, G_TYPE_STRING,
			G_TYPE_POINTER, G_TYPE_POINTER, G_TYPE_POINTER,
			G_TYPE_POINTER);
	hexeditor->pl_combo = gtk_combo_box_new_with_model(GTK_TREE_MODEL(
				hexeditor->pl_store));
	g_signal_connect_swapped(hexeditor->pl_combo, "changed", G_CALLBACK(
				_hexeditor_on_plugin_combo_change), hexeditor);
	renderer = gtk_cell_renderer_pixbuf_new();
	gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(hexeditor->pl_combo),
			renderer, FALSE);
	gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(hexeditor->pl_combo),
			renderer, "pixbuf", HEPC_ICON, NULL);
	renderer = gtk_cell_renderer_text_new();
	gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(hexeditor->pl_combo),
			renderer, TRUE);
	gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(hexeditor->pl_combo),
			renderer, "text", HEPC_NAME_DISPLAY, NULL);
	gtk_box_pack_start(GTK_BOX(hexeditor->pl_view), hexeditor->pl_combo,
			FALSE, TRUE, 0);
	hexeditor->pl_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
	gtk_box_pack_start(GTK_BOX(hexeditor->pl_view), hexeditor->pl_box, TRUE,
			TRUE, 0);
	hexeditor->pl_helper.hexeditor = hexeditor;
	hexeditor->pl_helper.error = _hexeditor_error;
	/* load the plug-ins */
	if((plugins = config_get(hexeditor->config, NULL, "plugins")) == NULL
			|| strlen(plugins) == 0)
		return;
	if((p = strdup(plugins)) == NULL)
		return; /* XXX report error */
	for(q = p, i = 0;;)
	{
		if(q[i] == '\0')
		{
			hexeditor_load(hexeditor, q);
			break;
		}
		if(q[i++] != ',')
			continue;
		q[i - 1] = '\0';
		hexeditor_load(hexeditor, q);
		q += i;
		i = 0;
	}
}

static void _new_progress(HexEditor * hexeditor)
{
	GtkWidget * hbox;
	GtkWidget * widget;

	hexeditor->pg_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
	gtk_container_set_border_width(GTK_CONTAINER(hexeditor->pg_window), 16);
	gtk_window_set_decorated(GTK_WINDOW(hexeditor->pg_window), FALSE);
	gtk_window_set_default_size(GTK_WINDOW(hexeditor->pg_window), 200, 50);
	gtk_window_set_modal(GTK_WINDOW(hexeditor->pg_window), TRUE);
	gtk_window_set_title(GTK_WINDOW(hexeditor->pg_window), _("Progress"));
	gtk_window_set_transient_for(GTK_WINDOW(hexeditor->pg_window),
			GTK_WINDOW(hexeditor->window));
	gtk_window_set_position(GTK_WINDOW(hexeditor->pg_window),
			GTK_WIN_POS_CENTER_ON_PARENT);
	g_signal_connect_swapped(hexeditor->pg_window, "delete-event",
			G_CALLBACK(_hexeditor_on_progress_delete), hexeditor);
	hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
	hexeditor->pg_progress = gtk_progress_bar_new();
#if GTK_CHECK_VERSION(3, 0, 0)
	gtk_progress_bar_set_show_text(GTK_PROGRESS_BAR(hexeditor->pg_progress),
			TRUE);
#endif
	gtk_progress_bar_set_text(GTK_PROGRESS_BAR(hexeditor->pg_progress), "");
	gtk_box_pack_start(GTK_BOX(hbox), hexeditor->pg_progress, FALSE, TRUE,
			0);
	widget = gtk_button_new_from_stock(GTK_STOCK_CANCEL);
	g_signal_connect_swapped(widget, "clicked", G_CALLBACK(
				_hexeditor_on_progress_cancel), hexeditor);
	gtk_box_pack_start(GTK_BOX(hbox), widget, FALSE, TRUE, 0);
	gtk_container_add(GTK_CONTAINER(hexeditor->pg_window), hbox);
}


/* hexeditor_delete */
static void _delete_plugins(HexEditor * hexeditor);

void hexeditor_delete(HexEditor * hexeditor)
{
	_hexeditor_close(hexeditor, FALSE);
	_delete_plugins(hexeditor);
	pango_font_description_free(hexeditor->bold);
	if(hexeditor->config != NULL)
		config_delete(hexeditor->config);
	object_delete(hexeditor);
}

static void _delete_plugins(HexEditor * hexeditor)
{
	GtkTreeModel * model = GTK_TREE_MODEL(hexeditor->pl_store);
	GtkTreeIter iter;
	gboolean valid;
	Plugin * plugin;
	HexEditorPluginDefinition * hepd;
	HexEditorPlugin * hep;

	for(valid = gtk_tree_model_get_iter_first(model, &iter); valid == TRUE;
			valid = gtk_tree_model_iter_next(model, &iter))
	{
		gtk_tree_model_get(model, &iter, HEPC_PLUGIN, &plugin,
				HEPC_HEXEDITORPLUGINDEFINITION, &hepd,
				HEPC_HEXEDITORPLUGIN, &hep, -1);
		if(hepd->destroy != NULL)
			hepd->destroy(hep);
		plugin_delete(plugin);
	}
}


/* accessors */
/* hexeditor_get_widget */
GtkWidget * hexeditor_get_widget(HexEditor * hexeditor)
{
	return hexeditor->widget;
}


/* hexeditor_set_font */
void hexeditor_set_font(HexEditor * hexeditor, char const * font)
{
	PangoFontDescription * desc;

	if(font == NULL)
	{
		desc = pango_font_description_new();
		pango_font_description_set_family(desc, "Monospace");
	}
	else
		desc = pango_font_description_from_string(font);
	gtk_widget_override_font(hexeditor->view_addr, desc);
	gtk_widget_override_font(hexeditor->view_hex, desc);
	gtk_widget_override_font(hexeditor->view_data, desc);
	pango_font_description_free(desc);
}


/* useful */
/* hexeditor_close */
void hexeditor_close(HexEditor * hexeditor)
{
	_hexeditor_close(hexeditor, TRUE);
}


/* hexeditor_load */
int hexeditor_load(HexEditor * hexeditor, char const * plugin)
{
	Plugin * p;
	HexEditorPluginDefinition * hepd;
	HexEditorPlugin * hep;
	GtkWidget * widget;
	GtkTreeIter iter;
	GtkIconTheme * theme;
	GdkPixbuf * icon = NULL;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s(\"%s\")\n", __func__, plugin);
#endif
	if(_hexeditor_plugin_is_enabled(hexeditor, plugin))
		return 0;
	if((p = plugin_new(LIBDIR, PACKAGE, "plugins", plugin)) == NULL)
		return -_hexeditor_error(hexeditor, error_get(NULL), 1);
	if((hepd = plugin_lookup(p, "plugin")) == NULL)
	{
		plugin_delete(p);
		return -_hexeditor_error(hexeditor, error_get(NULL), 1);
	}
	if(hepd->init == NULL || hepd->destroy == NULL
			|| hepd->get_widget == NULL
			|| (hep = hepd->init(&hexeditor->pl_helper)) == NULL)
	{
		plugin_delete(p);
		/* FIXME the error may not be set */
		return -_hexeditor_error(hexeditor, error_get(NULL), 1);
	}
	widget = hepd->get_widget(hep);
	gtk_widget_hide(widget);
	theme = gtk_icon_theme_get_default();
	if(hepd->icon != NULL)
		icon = gtk_icon_theme_load_icon(theme, hepd->icon, 24, 0, NULL);
	if(icon == NULL)
		icon = gtk_icon_theme_load_icon(theme, "gnome-settings", 24, 0,
				NULL);
#if GTK_CHECK_VERSION(2, 6, 0)
	gtk_list_store_insert_with_values(hexeditor->pl_store, &iter, -1,
#else
	gtk_list_store_append(hexeditor->pl_store, &iter);
	gtk_list_store_set(hexeditor->pl_store, &iter,
#endif
			HEPC_NAME, plugin, HEPC_ICON, icon,
			HEPC_NAME_DISPLAY, hepd->name,
			HEPC_PLUGIN, p, HEPC_HEXEDITORPLUGINDEFINITION, hepd,
			HEPC_HEXEDITORPLUGIN, hep, HEPC_WIDGET, widget, -1);
	if(icon != NULL)
		g_object_unref(icon);
	gtk_box_pack_start(GTK_BOX(hexeditor->pl_box), widget, TRUE, TRUE, 0);
	if(gtk_widget_get_no_show_all(hexeditor->pl_view) == TRUE)
	{
		gtk_combo_box_set_active(GTK_COMBO_BOX(hexeditor->pl_combo), 0);
		gtk_widget_set_no_show_all(hexeditor->pl_view, FALSE);
		gtk_widget_show_all(hexeditor->pl_view);
	}
	return 0;
}


/* hexeditor_open */
static gboolean _open_on_can_read(GIOChannel * channel, GIOCondition condition,
		gpointer data);
static gboolean _open_on_idle(gpointer data);
static void _open_plugins_read(HexEditor * hexeditor, char const * buf,
		size_t size);
static void _open_progress(HexEditor * hexeditor);
static void _open_read_1(HexEditor * hexeditor, char * buf, gsize pos);
static void _open_read_16(HexEditor * hexeditor, char * buf, gsize pos);

int hexeditor_open(HexEditor * hexeditor, char const * filename)
{
	char buf[256];
	gchar * p;
	struct stat st;

	if(filename == NULL)
		return hexeditor_open_dialog(hexeditor);
	hexeditor_close(hexeditor);
	if((hexeditor->filename = strdup(filename)) == NULL)
		return -_hexeditor_error(hexeditor, strerror(errno), 1);
	if((hexeditor->fd = open(filename, O_RDONLY)) < 0)
	{
		free(hexeditor->filename);
		hexeditor->filename = NULL;
		return -_hexeditor_error(hexeditor, strerror(errno), 1);
	}
	p = g_filename_display_name(filename);
	snprintf(buf, sizeof(buf), "%s - %s", _("Hexadecimal editor"), p);
	g_free(p);
	gtk_window_set_title(GTK_WINDOW(hexeditor->window), buf);
	hexeditor->channel = g_io_channel_unix_new(hexeditor->fd);
	g_io_channel_set_encoding(hexeditor->channel, NULL, NULL);
	hexeditor->source = g_io_add_watch(hexeditor->channel, G_IO_IN,
			_open_on_can_read, hexeditor);
	hexeditor->offset = 0;
	hexeditor->size = 0;
	gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(hexeditor->pg_progress),
			0.0);
	gtk_progress_bar_set_text(GTK_PROGRESS_BAR(hexeditor->pg_progress), "");
	if(fstat(hexeditor->fd, &st) == 0)
		hexeditor->size = st.st_size;
	hexeditor->time = time(NULL);
	gtk_widget_show_all(hexeditor->pg_window);
	return 0;
}

static gboolean _open_on_can_read(GIOChannel * channel, GIOCondition condition,
		gpointer data)
{
	HexEditor * hexeditor = data;
	GIOStatus status;
	char buf[BUFSIZ];
	gsize size = sizeof(buf);
	GError * error = NULL;
	gsize i;

	if(channel != hexeditor->channel || condition != G_IO_IN)
		return FALSE;
	status = g_io_channel_read_chars(channel, buf, size, &size, &error);
	if(status == G_IO_STATUS_AGAIN)
		/* this status can be ignored */
		return TRUE;
	if(status != G_IO_STATUS_NORMAL && status != G_IO_STATUS_EOF)
	{
		hexeditor->source = 0;
		hexeditor_close(hexeditor);
		if(status == G_IO_STATUS_ERROR)
		{
			_hexeditor_error(hexeditor, error->message, 1);
			g_error_free(error);
		}
		gtk_widget_hide(hexeditor->pg_window);
		return FALSE;
	}
	/* read until the end of a line */
	for(i = 0; ((hexeditor->offset + i) % 16) != 0 && i < size; i++)
		_open_read_1(hexeditor, buf, i);
	/* read complete lines */
	for(; i + 15 < size; i += 16)
		_open_read_16(hexeditor, buf, i);
	/* read until the end of the buffer */
	for(; i < size; i++)
		_open_read_1(hexeditor, buf, i);
	/* tell the plug-ins */
	_open_plugins_read(hexeditor, buf, size);
	hexeditor->offset += size;
	if(status == G_IO_STATUS_EOF)
	{
		/* tell the plug-ins if relevant */
		if(size != 0)
			_open_plugins_read(hexeditor, NULL, 0);
		hexeditor->source = 0;
		gtk_widget_set_sensitive(hexeditor->view_addr, TRUE);
		gtk_widget_set_sensitive(hexeditor->view_hex, TRUE);
		gtk_widget_set_sensitive(hexeditor->view_data, TRUE);
		gtk_widget_hide(hexeditor->pg_window);
		return FALSE;
	}
	_open_progress(hexeditor);
	hexeditor->source = g_idle_add(_open_on_idle, hexeditor);
	return FALSE;
}

static gboolean _open_on_idle(gpointer data)
{
	HexEditor * hexeditor = data;

	hexeditor->source = g_io_add_watch(hexeditor->channel, G_IO_IN,
			_open_on_can_read, hexeditor);
	return FALSE;
}

static void _open_plugins_read(HexEditor * hexeditor, char const * buf,
		size_t size)
{
	GtkTreeModel * model = GTK_TREE_MODEL(hexeditor->pl_store);
	GtkTreeIter iter;
	gboolean valid;
	HexEditorPluginDefinition * hepd;
	HexEditorPlugin * hep;

	for(valid = gtk_tree_model_get_iter_first(model, &iter); valid == TRUE;
			valid = gtk_tree_model_iter_next(model, &iter))
	{
		gtk_tree_model_get(model, &iter,
				HEPC_HEXEDITORPLUGINDEFINITION, &hepd,
				HEPC_HEXEDITORPLUGIN, &hep, -1);
		if(hepd->read != NULL)
			hepd->read(hep, hexeditor->offset, buf, size);
	}
}

static void _open_progress(HexEditor * hexeditor)
{
	time_t t;
	gdouble fraction;
	char buf[16];
	GtkProgressBar * progress = GTK_PROGRESS_BAR(hexeditor->pg_progress);

	/* pulse the progress bar once per second */
	if((t = time(NULL)) <= hexeditor->time)
		return;
	hexeditor->time = t;
	if(hexeditor->size == 0)
	{
		gtk_progress_bar_pulse(progress);
		return;
	}
	fraction = hexeditor->offset;
	fraction = fraction / hexeditor->size;
	gtk_progress_bar_set_fraction(progress, fraction);
	snprintf(buf, sizeof(buf), "%.1f%%", fraction * 100);
	gtk_progress_bar_set_text(progress, buf);
}

static void _open_read_1(HexEditor * hexeditor, char * buf, gsize pos)
{
	char buf2[16];
	GtkTextBuffer * taddr;
	GtkTextBuffer * thex;
	GtkTextBuffer * tdata;
	int c;
	int size;

	taddr = hexeditor->view_addr_tbuf;
	thex = hexeditor->view_hex_tbuf;
	tdata = hexeditor->view_data_tbuf;
	c = (unsigned char)buf[pos];
	if(((hexeditor->offset + pos) % 16) == 0)
	{
		/* address */
		size = snprintf(buf2, sizeof(buf2), hexeditor->prefs.uppercase
				? "%s%08X" : "%s%08x",
				(hexeditor->offset + pos) ? "\n" : "",
				(unsigned int)(hexeditor->offset + pos));
		gtk_text_buffer_insert(taddr, &hexeditor->view_addr_iter,
				buf2, size);
		/* hexadecimal value */
		size = snprintf(buf2, sizeof(buf2), hexeditor->prefs.uppercase
				? "%s%02X" : "%s%02x",
				(hexeditor->offset + pos) ? "\n" : "", c);
		gtk_text_buffer_insert(thex, &hexeditor->view_hex_iter,
				buf2, size);
		if(hexeditor->offset + pos != 0)
			/* character value */
			gtk_text_buffer_insert(tdata,
					&hexeditor->view_data_iter, "\n", 1);
	}
	else
	{
		/* hexadecimal value */
		size = snprintf(buf2, sizeof(buf2), hexeditor->prefs.uppercase
				? " %02X" : " %02x", c);
		gtk_text_buffer_insert(thex, &hexeditor->view_hex_iter,
				buf2, size);
	}
	/* character value */
	size = snprintf(buf2, sizeof(buf2), "%c", (isascii(c) && isprint(c))
			? c : '.');
	gtk_text_buffer_insert(tdata, &hexeditor->view_data_iter, buf2, size);
}

static void _open_read_16(HexEditor * hexeditor, char * buf, gsize pos)
{
	GtkTextBuffer * taddr;
	GtkTextBuffer * thex;
	GtkTextBuffer * tdata;
	unsigned char c[16];
	int i;
	char buf2[64];

	taddr = hexeditor->view_addr_tbuf;
	thex = hexeditor->view_hex_tbuf;
	tdata = hexeditor->view_data_tbuf;
	/* address */
	i = snprintf(buf2, sizeof(buf2), hexeditor->prefs.uppercase
			? "%s%08X" : "%s%08x",
			(hexeditor->offset + pos) ? "\n" : "",
			(unsigned int)(hexeditor->offset + pos));
	gtk_text_buffer_insert(taddr, &hexeditor->view_addr_iter, buf2, i);
	/* hexadecimal values */
	for(i = 0; i < 16; i++)
		c[i] = (unsigned char)buf[pos + i];
	i = snprintf(buf2, sizeof(buf2), hexeditor->prefs.uppercase
			? "%s%02X %02X %02X %02X %02X %02X %02X %02X"
			" %02X %02X %02X %02X %02X %02X %02X %02X"
			: "%s%02x %02x %02x %02x %02x %02x %02x %02x"
			" %02x %02x %02x %02x %02x %02x %02x %02x",
			(hexeditor->offset + pos) ? "\n" : "",
			c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7],
			c[8], c[9], c[10], c[11], c[12], c[13], c[14], c[15]);
	gtk_text_buffer_insert(thex, &hexeditor->view_hex_iter, buf2, i);
	/* character values */
	if(hexeditor->offset + pos != 0)
		gtk_text_buffer_insert(tdata, &hexeditor->view_data_iter,
				"\n", 1);
	for(i = 0; i < 16; i++)
		if(!isascii(c[i]) || !isprint(c[i]))
			c[i] = '.';
	gtk_text_buffer_insert(tdata, &hexeditor->view_data_iter,
			(char const *)c, 16);
}


/* hexeditor_open_dialog */
int hexeditor_open_dialog(HexEditor * hexeditor)
{
	int ret;
	GtkWidget * dialog;
	GtkFileFilter * filter;
	gchar * filename = NULL;

	dialog = gtk_file_chooser_dialog_new(_("Open file..."),
			GTK_WINDOW(hexeditor->window),
			GTK_FILE_CHOOSER_ACTION_OPEN,
			GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
			GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT, NULL);
	filter = gtk_file_filter_new();
	gtk_file_filter_set_name(filter, _("All files"));
	gtk_file_filter_add_pattern(filter, "*");
	gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filter);
	if(gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT)
		filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(
					dialog));
	gtk_widget_destroy(dialog);
	if(filename == NULL)
		return -1;
	ret = hexeditor_open(hexeditor, filename);
	g_free(filename);
	return ret;
}


/* hexeditor_show_preferences */
void hexeditor_show_preferences(HexEditor * hexeditor, gboolean show)
{
	/* FIXME implement */
}


/* hexeditor_show_properties */
static GtkWidget * _properties_widget(HexEditor * hexeditor,
		GtkSizeGroup * group, char const * label, GtkWidget * value);

void hexeditor_show_properties(HexEditor * hexeditor, gboolean show)
{
	const unsigned int flags = GTK_DIALOG_MODAL
		| GTK_DIALOG_DESTROY_WITH_PARENT;
	GtkWidget * dialog;
	GtkSizeGroup * hgroup;
	GtkSizeGroup * vgroup;
	GtkWidget * vbox;
	GtkWidget * widget;
	String * s;
	gchar * p;
	gchar * q;
	GError * error = NULL;

	if(show == FALSE)
		/* XXX should really hide the window */
		return;
	dialog = gtk_message_dialog_new(GTK_WINDOW(hexeditor->window), flags,
			GTK_MESSAGE_INFO, GTK_BUTTONS_CLOSE,
#if GTK_CHECK_VERSION(2, 6, 0)
			"%s", _("Properties"));
	gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog),
#endif
			"");
#if GTK_CHECK_VERSION(2, 10, 0)
	gtk_message_dialog_set_image(GTK_MESSAGE_DIALOG(dialog),
			gtk_image_new_from_icon_name("gtk-properties",
				GTK_ICON_SIZE_DIALOG));
#endif
	if(hexeditor->filename == NULL)
		s = string_new(_("Properties"));
	else
	{
		p = g_path_get_basename(hexeditor->filename);
		s = string_new_format(_("Properties of %s"), p);
		g_free(p);
	}
	if(s != NULL)
		gtk_window_set_title(GTK_WINDOW(dialog), s);
	string_delete(s);
	hgroup = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
	vgroup = gtk_size_group_new(GTK_SIZE_GROUP_VERTICAL);
#if GTK_CHECK_VERSION(2, 14, 0)
	vbox = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
#else
	vbox = dialog->vbox;
#endif
	/* filename */
	/* XXX the filename may be relative */
	p = g_strdup((hexeditor->filename != NULL) ? hexeditor->filename : "");
	if((q = g_filename_to_utf8(p, -1, NULL, NULL, &error)) == NULL)
	{
		_hexeditor_error(NULL, error->message, 1);
		g_error_free(error);
		q = p;
	}
	widget = gtk_entry_new();
	gtk_editable_set_editable(GTK_EDITABLE(widget), FALSE);
	gtk_entry_set_text(GTK_ENTRY(widget), q);
	gtk_size_group_add_widget(vgroup, widget);
	g_free(p);
	widget = _properties_widget(hexeditor, hgroup, _("Filename:"), widget);
	gtk_box_pack_start(GTK_BOX(vbox), widget, FALSE, FALSE, 0);
	/* FIXME implement more properties */
	gtk_widget_show_all(vbox);
	gtk_dialog_run(GTK_DIALOG(dialog));
	gtk_widget_destroy(dialog);
}

static GtkWidget * _properties_widget(HexEditor * hexeditor,
		GtkSizeGroup * group, char const * label, GtkWidget * value)
{
	GtkWidget * hbox;
	GtkWidget * widget;

	hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
	widget = gtk_label_new(label);
	gtk_widget_override_font(widget, hexeditor->bold);
#if GTK_CHECK_VERSION(3, 0, 0)
	g_object_set(widget, "halign", GTK_ALIGN_START, NULL);
#else
	gtk_misc_set_alignment(GTK_MISC(widget), 0.0, 0.5);
#endif
	gtk_size_group_add_widget(group, widget);
	gtk_box_pack_start(GTK_BOX(hbox), widget, FALSE, TRUE, 0);
	gtk_box_pack_start(GTK_BOX(hbox), value, TRUE, TRUE, 0);
	return hbox;
}


/* hexeditor_unload */
int hexeditor_unload(HexEditor * hexeditor, char const * plugin)
{
	GtkTreeModel * model = GTK_TREE_MODEL(hexeditor->pl_store);
	GtkTreeIter iter;
	gboolean valid;
	gchar * p;
	Plugin * pp;
	HexEditorPluginDefinition * hepd;
	HexEditorPlugin * hep;
	GtkWidget * widget;

	for(valid = gtk_tree_model_get_iter_first(model, &iter); valid == TRUE;
			valid = gtk_tree_model_iter_next(model, &iter))
	{
		gtk_tree_model_get(model, &iter, HEPC_NAME, &p,
				HEPC_PLUGIN, &pp,
				HEPC_HEXEDITORPLUGINDEFINITION, &hepd,
				HEPC_HEXEDITORPLUGIN, &hep,
				HEPC_WIDGET, &widget, -1);
		if(strcmp(plugin, p) == 0)
			break;
		g_free(p);
	}
	if(valid != TRUE)
		return 0;
	g_free(p);
	gtk_list_store_remove(hexeditor->pl_store, &iter);
	gtk_container_remove(GTK_CONTAINER(hexeditor->pl_box), widget);
	hepd->destroy(hep);
	plugin_delete(pp);
	if(gtk_tree_model_iter_n_children(model, NULL) == 0)
	{
		gtk_widget_set_no_show_all(hexeditor->pl_view, TRUE);
		gtk_widget_hide(hexeditor->pl_view);
	}
	else if(gtk_combo_box_get_active(GTK_COMBO_BOX(hexeditor->pl_combo))
			< 0)
		gtk_combo_box_set_active(GTK_COMBO_BOX(hexeditor->pl_combo), 0);
	return 0;
}


/* private */
/* functions */
/* accessors */
/* hexeditor_get_config_filename */
static String * _hexeditor_get_config_filename(char const * filename)
{
	char const * homedir;

	if((homedir = getenv("HOME")) == NULL)
		homedir = g_get_home_dir();
	return string_new_append(homedir, "/", filename, NULL);
}


/* hexeditor_plugin_is_enabled */
static int _hexeditor_plugin_is_enabled(HexEditor * hexeditor,
		char const * plugin)
{
	GtkTreeModel * model = GTK_TREE_MODEL(hexeditor->pl_store);
	GtkTreeIter iter;
	gchar * p;
	gboolean valid;
	int res;

	for(valid = gtk_tree_model_get_iter_first(model, &iter); valid == TRUE;
			valid = gtk_tree_model_iter_next(model, &iter))
	{
		gtk_tree_model_get(model, &iter, HEPC_NAME, &p, -1);
		res = strcmp(p, plugin);
		g_free(p);
		if(res == 0)
			return TRUE;
	}
	return FALSE;
}


/* useful */
/* hexeditor_close */
static void _close_reset(HexEditor * hexeditor);

static void _hexeditor_close(HexEditor * hexeditor, gboolean plugins)
{
	if(hexeditor->source != 0)
		g_source_remove(hexeditor->source);
	hexeditor->source = 0;
	hexeditor->offset = 0;
	hexeditor->size = 0;
	hexeditor->time = 0;
	gtk_text_buffer_set_text(hexeditor->view_addr_tbuf, "", 0);
	gtk_text_buffer_get_end_iter(hexeditor->view_addr_tbuf,
			&hexeditor->view_addr_iter);
	gtk_text_buffer_set_text(hexeditor->view_hex_tbuf, "", 0);
	gtk_text_buffer_get_end_iter(hexeditor->view_hex_tbuf,
			&hexeditor->view_hex_iter);
	gtk_text_buffer_set_text(hexeditor->view_data_tbuf, "", 0);
	gtk_text_buffer_get_end_iter(hexeditor->view_data_tbuf,
			&hexeditor->view_data_iter);
	if(hexeditor->channel != NULL)
	{
		g_io_channel_shutdown(hexeditor->channel, TRUE, NULL);
		g_io_channel_unref(hexeditor->channel);
		hexeditor->channel = NULL;
		hexeditor->fd = -1;
	}
	if(hexeditor->fd >= 0 && close(hexeditor->fd) != 0)
		_hexeditor_error(hexeditor, strerror(errno), 1);
	hexeditor->fd = -1;
	free(hexeditor->filename);
	hexeditor->filename = NULL;
	gtk_widget_hide(hexeditor->pg_window);
	gtk_widget_set_sensitive(hexeditor->view_addr, FALSE);
	gtk_widget_set_sensitive(hexeditor->view_hex, FALSE);
	gtk_widget_set_sensitive(hexeditor->view_data, FALSE);
	gtk_window_set_title(GTK_WINDOW(hexeditor->window),
			_("Hexadecimal editor"));
	if(plugins == TRUE)
		_close_reset(hexeditor);
}

static void _close_reset(HexEditor * hexeditor)
{
	GtkTreeModel * model = GTK_TREE_MODEL(hexeditor->pl_store);
	GtkTreeIter iter;
	gboolean valid;
	Plugin * plugin;
	HexEditorPluginDefinition * hepd;
	HexEditorPlugin * hep;
	GtkWidget * widget;

	/* reset every plug-in */
	for(valid = gtk_tree_model_get_iter_first(model, &iter); valid == TRUE;)
	{
		gtk_tree_model_get(model, &iter, HEPC_PLUGIN, &plugin,
				HEPC_HEXEDITORPLUGINDEFINITION, &hepd,
				HEPC_HEXEDITORPLUGIN, &hep, -1);
		hepd->destroy(hep);
		if((hep = hepd->init(&hexeditor->pl_helper)) == NULL)
		{
			gtk_list_store_remove(hexeditor->pl_store, &iter);
			continue;
		}
		widget = hepd->get_widget(hep);
		gtk_list_store_set(hexeditor->pl_store, &iter,
				HEPC_HEXEDITORPLUGIN, hep,
				HEPC_WIDGET, widget, -1);
		valid = gtk_tree_model_iter_next(model, &iter);
	}
}


/* hexeditor_config_load */
static int _hexeditor_config_load(HexEditor * hexeditor)
{
	int ret;
	String * filename;
	String const * p;

	if(hexeditor->config == NULL)
		return -1; /* XXX report error */
	if((filename = _hexeditor_get_config_filename(HEXEDITOR_CONFIG_FILE))
			== NULL)
		return -1;
	if((ret = config_load(hexeditor->config, filename)) != 0)
		ret = -_hexeditor_error(NULL, error_get(NULL), 1);
	free(filename);
	/* uppercase */
	if((p = config_get(hexeditor->config, NULL, "uppercase")) != NULL)
		hexeditor->prefs.uppercase = (strtol(p, NULL, 10) > 0) ? 1 : 0;
	/* FIXME also import the font and plug-in values from here */
	return ret;
}


/* hexeditor_error */
static int _error_text(char const * message, int ret);

static int _hexeditor_error(HexEditor * hexeditor, char const * message,
		int ret)
{
#if !GTK_CHECK_VERSION(2, 18, 0)
	GtkWidget * dialog;
#endif

	if(hexeditor == NULL)
		return _error_text(message, ret);
#if GTK_CHECK_VERSION(2, 18, 0)
	gtk_label_set_text(GTK_LABEL(hexeditor->infobar_label), message);
	gtk_widget_show(hexeditor->infobar);
#else
	dialog = gtk_message_dialog_new(GTK_WINDOW(hexeditor->window),
			GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR,
			GTK_BUTTONS_CLOSE,
# if GTK_CHECK_VERSION(2, 6, 0)
			"%s", _("Error"));
	gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog),
# endif
			"%s", message);
	gtk_window_set_title(GTK_WINDOW(dialog), _("Error"));
	gtk_dialog_run(GTK_DIALOG(dialog));
	gtk_widget_destroy(dialog);
#endif
	return ret;
}

static int _error_text(char const * message, int ret)
{
	fprintf(stderr, "%s: %s\n", PROGNAME_HEXEDITOR, message);
	return ret;
}


/* callbacks */
/* hexeditor_on_open */
static void _hexeditor_on_open(gpointer data)
{
	HexEditor * hexeditor = data;

	hexeditor_open_dialog(hexeditor);
}


/* hexeditor_on_plugin_combo_change */
static void _hexeditor_on_plugin_combo_change(gpointer data)
{
	/* FIXME implement */
}


#ifdef EMBEDDED
/* hexeditor_on_preferences */
static void _hexeditor_on_preferences(gpointer data)
{
	HexEditor * hexeditor = data;

	hexeditor_show_preferences(hexeditor, TRUE);
}
#endif


/* hexeditor_on_progress_cancel */
static void _hexeditor_on_progress_cancel(gpointer data)
{
	HexEditor * hexeditor = data;

	hexeditor_close(hexeditor);
}


/* hexeditor_on_progress_delete */
static gboolean _hexeditor_on_progress_delete(gpointer data)
{
	HexEditor * hexeditor = data;

	gtk_widget_show(hexeditor->pg_window);
	return TRUE;
}


#ifdef EMBEDDED
/* hexeditor_on_properties */
static void _hexeditor_on_properties(gpointer data)
{
	HexEditor * hexeditor = data;

	hexeditor_show_properties(hexeditor, TRUE);
}
#endif
