/*------------------------------------------------------------------------
 *  Copyright 2008-2010 (c) Jeff Brown <spadix@users.sourceforge.net>
 *
 *  This file is part of the ZBar Bar Code Reader.
 *
 *  The ZBar Bar Code Reader is free software; you can redistribute it
 *  and/or modify it under the terms of the GNU Lesser Public License as
 *  published by the Free Software Foundation; either version 2.1 of
 *  the License, or (at your option) any later version.
 *
 *  The ZBar Bar Code Reader 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 Lesser Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser Public License
 *  along with the ZBar Bar Code Reader; if not, write to the Free
 *  Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 *  Boston, MA  02110-1301  USA
 *
 *  http://sourceforge.net/projects/zbar
 *------------------------------------------------------------------------*/

#include <gtk/gtk.h>
#ifdef HAVE_X
#include <gdk/gdkx.h>
#elif defined(_WIN32)
#include <gdk/gdkwin32.h>
#endif

#include "zbargtkprivate.h"
#include "zbarmarshal.h"
#include <zbar/zbargtk.h>

#ifndef G_PARAM_STATIC_STRINGS
#define G_PARAM_STATIC_STRINGS \
    (G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB)
#endif

#define DEFAULT_WIDTH  640
#define DEFAULT_HEIGHT 480

enum
{
    DECODED,
    DECODED_TEXT,
    LAST_SIGNAL
};

enum
{
    PROP_0,
    PROP_VIDEO_DEVICE,
    PROP_VIDEO_ENABLED,
    PROP_VIDEO_OPENED,
};

static guint zbar_gtk_signals[LAST_SIGNAL] = { 0 };

G_DEFINE_TYPE(ZBarGtk, zbar_gtk, GTK_TYPE_WIDGET);

/* FIXME what todo w/errors? OOM? */
/* FIXME signal failure notifications to main gui idle handler */

void zbar_gtk_release_pixbuf(zbar_image_t *img)
{
    GdkPixbuf *pixbuf = zbar_image_get_userdata(img);

    g_assert(GDK_IS_PIXBUF(pixbuf));

    /* remove reference */
    zbar_image_set_userdata(img, NULL);

    /* release reference to associated pixbuf and it's data */
    g_object_unref(pixbuf);
}

gboolean zbar_gtk_image_from_pixbuf(zbar_image_t *zimg, GdkPixbuf *pixbuf)
{
    unsigned pitch, width, height;
    GdkColorspace colorspace;
    unsigned long datalen;
    int nchannels, bps;
    long type;

    /* apparently should always be packed RGB? */
    colorspace = gdk_pixbuf_get_colorspace(pixbuf);

    if (colorspace != GDK_COLORSPACE_RGB) {
	g_warning("non-RGB color space not supported: %d\n", colorspace);
	return (FALSE);
    }

    nchannels = gdk_pixbuf_get_n_channels(pixbuf);
    bps	      = gdk_pixbuf_get_bits_per_sample(pixbuf);
    type      = 0;

    /* these are all guesses... */
    if (nchannels == 3 && bps == 8)
	type = zbar_fourcc('R', 'G', 'B', '3');
    else if (nchannels == 4 && bps == 8)
	type = zbar_fourcc('B', 'G', 'R', '4'); /* FIXME alpha flipped?! */
    else if (nchannels == 1 && bps == 8)
	type = zbar_fourcc('Y', '8', '0', '0');
    else if (nchannels == 3 && bps == 5)
	type = zbar_fourcc('R', 'G', 'B', 'R');
    else if (nchannels == 3 && bps == 4)
	type = zbar_fourcc('R', '4', '4', '4'); /* FIXME maybe? */
    else {
	g_warning("unsupported combination: nchannels=%d bps=%d\n", nchannels,
		  bps);
	return (FALSE);
    }
    zbar_image_set_format(zimg, type);

    /* FIXME we don't deal w/bpl...
     * this will cause problems w/unpadded pixbufs :|
     */
    pitch = gdk_pixbuf_get_rowstride(pixbuf);
    width = pitch / ((nchannels * bps) / 8);

    if ((width * nchannels * 8 / bps) != pitch) {
	g_warning("unsupported: width=%d nchannels=%d bps=%d rowstride=%d\n",
		  width, nchannels, bps, pitch);
	return (FALSE);
    }
    height = gdk_pixbuf_get_height(pixbuf);
    /* FIXME this isn't correct either */
    datalen = width * height * nchannels;

    zbar_image_set_size(zimg, width, height);

    /* when the zbar image is released, the pixbuf will be
     * automatically be released
     */
    zbar_image_set_userdata(zimg, pixbuf);
    zbar_image_set_data(zimg, gdk_pixbuf_get_pixels(pixbuf), datalen,
			zbar_gtk_release_pixbuf);
#ifdef DEBUG_ZBARGTK
    g_message("colorspace=%d nchannels=%d bps=%d type=%.4s(%08lx)\n"
	      "\tpitch=%d width=%d height=%d datalen=0x%lx\n",
	      colorspace, nchannels, bps, (char *)&type, type, pitch, width,
	      height, datalen);
#endif
    return (TRUE);
}

static inline gboolean zbar_gtk_video_open(ZBarGtk *self,
					   const char *video_device)
{
    ZBarGtkPrivate *zbar  = ZBAR_GTK_PRIVATE(self->_private);
    gboolean video_opened = FALSE;

    zbar->video_opened = FALSE;
    if (zbar->idle_id)
	g_object_notify(G_OBJECT(self), "video-opened");

    if (zbar->window) {
	/* ensure old video doesn't have image ref
         * (FIXME handle video destroyed w/images outstanding)
         */
	zbar_window_draw(zbar->window, NULL);
	gtk_widget_queue_draw(GTK_WIDGET(self));
    }

    if (zbar->video) {
	zbar_video_destroy(zbar->video);
	zbar->video = NULL;
    }

    if (video_device && video_device[0] && zbar->idle_id) {
	/* create video
         * FIXME video should support re-open
         */
	zbar->video = zbar_video_create();
	g_assert(zbar->video);

	if (zbar_video_open(zbar->video, video_device)) {
	    zbar_video_error_spew(zbar->video, 0);
	    zbar_video_destroy(zbar->video);
	    zbar->video = NULL;
	    /* FIXME error propagation */
	    return (FALSE);
	}

	/* negotiation accesses the window format list,
         * so we hold the lock for this part
         */
	if (zbar->video_width && zbar->video_height)
	    zbar_video_request_size(zbar->video, zbar->video_width,
				    zbar->video_height);

	video_opened = !zbar_negotiate_format(zbar->video, zbar->window);

	if (video_opened) {
	    zbar->req_width  = zbar_video_get_width(zbar->video);
	    zbar->req_height = zbar_video_get_height(zbar->video);
	}
	gtk_widget_queue_resize(GTK_WIDGET(self));

	zbar->video_opened = video_opened;
	if (zbar->idle_id)
	    g_object_notify(G_OBJECT(self), "video-opened");
    }

    return (video_opened);
}

static inline int zbar_gtk_process_image(ZBarGtk *self, zbar_image_t *image)
{
    ZBarGtkPrivate *zbar;
    int rc;

    zbar = ZBAR_GTK_PRIVATE(self->_private);

    if (!image)
	return (-1);

    zbar_image_t *tmp =
	zbar_image_convert(image, zbar_fourcc('Y', '8', '0', '0'));
    if (!tmp)
	return (-1);

    zbar_image_scanner_recycle_image(zbar->scanner, image);
    rc = zbar_scan_image(zbar->scanner, tmp);
    zbar_image_set_symbols(image, zbar_image_get_symbols(tmp));
    zbar_image_destroy(tmp);
    if (rc < 0)
	return (rc);

    if (rc && zbar->idle_id) {
	/* update decode results */
	const zbar_symbol_t *sym;
	for (sym = zbar_image_first_symbol(image); sym;
	     sym = zbar_symbol_next(sym))
	    if (!zbar_symbol_get_count(sym)) {
		zbar_symbol_type_t type = zbar_symbol_get_type(sym);
		const char *data	= zbar_symbol_get_data(sym);
		g_signal_emit(self, zbar_gtk_signals[DECODED], 0, type, data);

		/* FIXME skip this when unconnected? */
		gchar *text =
		    g_strconcat(zbar_get_symbol_name(type), ":", data, NULL);
		g_signal_emit(self, zbar_gtk_signals[DECODED_TEXT], 0, text);
		g_free(text);
	    }
    }

    if (zbar->window) {
	rc = zbar_window_draw(zbar->window, image);
	gtk_widget_queue_draw(GTK_WIDGET(self));
    } else
	rc = -1;

    return (rc);
}

static gboolean zbar_processing_idle_callback(gpointer data)
{
    ZBarGtk *self	 = data;
    ZBarGtkPrivate *zbar = ZBAR_GTK_PRIVATE(self->_private);
    GValue *msg		 = g_async_queue_try_pop(zbar->queue);
    GType type;

    if (!msg) {
	if (zbar->video_enabled_state) {
	    zbar_image_t *image = zbar_video_next_image(zbar->video);
	    if (zbar_gtk_process_image(self, image) < 0)
		zbar->video_enabled_state = FALSE;
	    if (image)
		zbar_image_destroy(image);

	    if (zbar->video_enabled_state)
		return TRUE;

	    if (zbar_video_enable(zbar->video, 0)) {
		zbar_video_error_spew(zbar->video, 0);
		zbar->video_enabled_state = FALSE;
	    }

	    zbar_image_scanner_enable_cache(zbar->scanner, 0);
	    /* release video image and revert to logo */
	    if (zbar->window) {
		zbar_window_draw(zbar->window, NULL);
		gtk_widget_queue_draw(GTK_WIDGET(self));
	    }

	    /* must have been an error while streaming */
	    zbar_gtk_video_open(self, NULL);
	}

	return TRUE;
    }

    g_assert(G_IS_VALUE(msg));

    type = G_VALUE_TYPE(msg);
    if (type == G_TYPE_INT) {
	/* video state change */
	int state = g_value_get_int(msg);
	if (state < 0) {
	    /* error identifying state */
	    g_value_unset(msg);
	    g_free(msg);
	    return TRUE;
	}
	g_assert(state >= 0 && state <= 1);
	zbar->video_enabled_state = (state != 0);
    } else if (type == G_TYPE_STRING) {
	/* open new video device */
	const char *video_device = g_value_get_string(msg);

	zbar->video_enabled_state = zbar_gtk_video_open(self, video_device);
    } else if (type == GDK_TYPE_PIXBUF) {
	/* scan provided image and broadcast results */
	zbar_image_t *image = zbar_image_create();
	GdkPixbuf *pixbuf   = GDK_PIXBUF(g_value_dup_object(msg));

	if (zbar_gtk_image_from_pixbuf(image, pixbuf))
	    zbar_gtk_process_image(self, image);
	else
	    g_object_unref(pixbuf);
	zbar_image_destroy(image);
    } else {
	gchar *dbg = g_strdup_value_contents(msg);

	g_warning("unknown message type (%x) received: %s\n", (unsigned)type,
		  dbg);
	g_free(dbg);
    }
    g_value_unset(msg);
    g_free(msg);
    msg = NULL;

    if (zbar->video_enabled_state) {
	/* release reference to any previous pixbuf */
	if (zbar->window)
	    zbar_window_draw(zbar->window, NULL);

	if (zbar_video_enable(zbar->video, 1)) {
	    zbar_video_error_spew(zbar->video, 0);
	    zbar->video_enabled_state = FALSE;
	    return TRUE;
	}
	zbar_image_scanner_enable_cache(zbar->scanner, 1);

	return TRUE;
    }

    return TRUE;
}

static void zbar_gtk_realize(GtkWidget *widget)
{
    ZBarGtk *self = ZBAR_GTK(widget);
    ZBarGtkPrivate *zbar;
    GdkWindowAttr attributes;
    GtkAllocation allocation;

    if (!self->_private)
	return;
    zbar = ZBAR_GTK_PRIVATE(self->_private);

    gtk_widget_set_realized(widget, TRUE);

#if GTK_CHECK_VERSION(3, 14, 0)
    // FIXME: this is deprecated after 3.14 - no idea what replaces it
#else
    gtk_widget_set_double_buffered(widget, FALSE);
#endif

    gtk_widget_get_allocation(widget, &allocation);
    attributes.x	   = allocation.x;
    attributes.y	   = allocation.y;
    attributes.width	   = allocation.width;
    attributes.height	   = allocation.height;
    attributes.wclass	   = GDK_INPUT_OUTPUT;
    attributes.window_type = GDK_WINDOW_CHILD;
    attributes.event_mask = (gtk_widget_get_events(widget) | GDK_EXPOSURE_MASK);

    GdkWindow *window = gdk_window_new(gtk_widget_get_parent_window(widget),
				       &attributes, GDK_WA_X | GDK_WA_Y);
    gtk_widget_set_window(widget, window);
    gdk_window_set_user_data(window, widget);
#if GTK_MAJOR_VERSION == 3 && GTK_MINOR_VERSION < 18
    gdk_window_set_background_pattern(window, NULL);
#elif GTK_MAJOR_VERSION < 3
    gdk_window_set_back_pixmap(window, NULL, TRUE);
#endif

    /* attach zbar_window to underlying X window */
#ifdef HAVE_X
    if (zbar_window_attach(zbar->window,
			   GDK_DISPLAY_XDISPLAY(gdk_display_get_default()),
			   GDK_WINDOW_XID(window)))
#elif defined(_WIN32)
    if (zbar_window_attach(zbar->window, GDK_WINDOW_HWND(window), 0))
#endif
	zbar_window_error_spew(zbar->window, 0);
}

static inline GValue *zbar_gtk_new_value(GType type)
{
    return (g_value_init(g_malloc0(sizeof(GValue)), type));
}

static void zbar_gtk_unrealize(GtkWidget *widget)
{
    ZBarGtkPrivate *zbar;
    GdkWindow *window;
    ZBarGtk *self;

    if (gtk_widget_get_mapped(widget))
	gtk_widget_unmap(widget);

    gtk_widget_set_mapped(widget, FALSE);
    self = ZBAR_GTK(widget);
    if (!self->_private)
	return;
    zbar = ZBAR_GTK_PRIVATE(self->_private);

    if (zbar->video_enabled) {
	zbar->video_enabled = FALSE;
	GValue *msg	    = zbar_gtk_new_value(G_TYPE_INT);
	g_value_set_int(msg, 0);
	g_async_queue_push(zbar->queue, msg);
    }

    zbar_window_attach(zbar->window, NULL, 0);

    gtk_widget_set_realized(widget, FALSE);

    window = gtk_widget_get_window(widget);
    gdk_window_set_user_data(window, NULL);
    gdk_window_destroy(window);
    gtk_widget_set_window(widget, NULL);
}

#if GTK_MAJOR_VERSION >= 3

static void zbar_get_preferred_width(GtkWidget *widget, gint *minimum_width,
				     gint *natural_width)
{
    unsigned int screen_width;
    ZBarGtkPrivate *zbar;
    ZBarGtk *self;
#if GTK_CHECK_VERSION(3, 22, 0)
    GdkDisplay *display;
    GdkMonitor *monitor;
    GdkRectangle geo;
#endif
    self = ZBAR_GTK(widget);

    if (!self->_private)
	return;
    zbar = ZBAR_GTK_PRIVATE(self->_private);

    /* use native video size (max) if available,
     * arbitrary defaults otherwise.
     * video attributes maintained under main gui idle handler
     */
#if GTK_CHECK_VERSION(3, 22, 0)
    display = gdk_display_get_default();
    monitor = gdk_display_get_monitor(display, 0);
    gdk_monitor_get_geometry(monitor, &geo);

    screen_width = geo.width;
#else
    screen_width = gdk_screen_width();
#endif

    if (zbar->req_width > screen_width) {
	float scale = screen_width * .8 / zbar->req_width;

	zbar->req_width *= scale;
	zbar->req_height *= scale;
    }

    *minimum_width = zbar->req_width;
    *natural_width = zbar->req_width;
}

static void zbar_get_preferred_height(GtkWidget *widget, gint *minimum_height,
				      gint *natural_height)
{
    ZBarGtk *self = ZBAR_GTK(widget);
    unsigned int screen_height;
#if GTK_CHECK_VERSION(3, 22, 0)
    GdkDisplay *display;
    GdkMonitor *monitor;
    GdkRectangle geo;
#endif

    if (!self->_private)
	return;
    ZBarGtkPrivate *zbar = ZBAR_GTK_PRIVATE(self->_private);

    /* use native video size (max) if available,
     * arbitrary defaults otherwise.
     * video attributes maintained under main gui idle handler
     */
#if GTK_CHECK_VERSION(3, 22, 0)
    display = gdk_display_get_default();
    monitor = gdk_display_get_monitor(display, 0);
    gdk_monitor_get_geometry(monitor, &geo);

    screen_height = geo.height;
#else
    screen_height = gdk_screen_height();
#endif

    if (zbar->req_height > screen_height) {
	float scale = screen_height * .8 / zbar->req_height;

	zbar->req_width *= scale;
	zbar->req_height *= scale;
    }

    *minimum_height = zbar->req_height;
    *natural_height = zbar->req_height;
}

static gboolean zbar_gtk_scale_draw(GtkWidget *widget, cairo_t *cr)
{
    // NOTE: should we change something here?

    ZBarGtk *self = ZBAR_GTK(widget);

    if (!self->_private)
	return (FALSE);
    ZBarGtkPrivate *zbar = ZBAR_GTK_PRIVATE(self->_private);

    if (gtk_widget_get_visible(widget) && gtk_widget_get_mapped(widget) &&
	zbar_window_redraw(zbar->window))
	return (TRUE);
    return (FALSE);
}

#else
static void zbar_gtk_size_request(GtkWidget *widget,
				  GtkRequisition *requisition)
{
    ZBarGtk *self = ZBAR_GTK(widget);
    ZBarGtkPrivate *zbar;

    if (!self->_private)
	return;
    zbar = ZBAR_GTK_PRIVATE(self->_private);

    /* use native video size (max) if available,
     * arbitrary defaults otherwise.
     * video attributes maintained under main gui idle handler
     */
    requisition->width = zbar->req_width;
    requisition->height = zbar->req_height;
}

static gboolean zbar_gtk_expose(GtkWidget *widget, GdkEventExpose *event)
{
    ZBarGtk *self = ZBAR_GTK(widget);
    ZBarGtkPrivate *zbar;

    if (!self->_private)
	return (FALSE);
    zbar = ZBAR_GTK_PRIVATE(self->_private);

    if (gtk_widget_get_visible(widget) && gtk_widget_get_mapped(widget) &&
	zbar_window_redraw(zbar->window))
	return (TRUE);
    return (FALSE);
}
#endif

static void zbar_gtk_size_allocate(GtkWidget *widget, GtkAllocation *allocation)
{
    ZBarGtk *self = ZBAR_GTK(widget);
    ZBarGtkPrivate *zbar;

    if (!self->_private)
	return;
    zbar = ZBAR_GTK_PRIVATE(self->_private);

    (*GTK_WIDGET_CLASS(zbar_gtk_parent_class)->size_allocate)(widget,
							      allocation);
    if (zbar->window)
	zbar_window_resize(zbar->window, allocation->width, allocation->height);
}

void zbar_gtk_scan_image(ZBarGtk *self, GdkPixbuf *img)
{
    ZBarGtkPrivate *zbar;

    if (!self->_private)
	return;
    zbar = ZBAR_GTK_PRIVATE(self->_private);

    g_object_ref(G_OBJECT(img));

    /* queue for scanning by the processor idle handler */
    GValue *msg = zbar_gtk_new_value(GDK_TYPE_PIXBUF);

    /* this grabs a new reference to the image,
     * eventually released by the processor idle handler
     */
    g_value_set_object(msg, img);
    g_async_queue_push(zbar->queue, msg);
}

const char *zbar_gtk_get_video_device(ZBarGtk *self)
{
    ZBarGtkPrivate *zbar;

    if (!self->_private)
	return (NULL);
    zbar = ZBAR_GTK_PRIVATE(self->_private);
    if (zbar->video_device)
	return (zbar->video_device);
    else
	return ("");
}

void zbar_gtk_set_video_device(ZBarGtk *self, const char *video_device)
{
    ZBarGtkPrivate *zbar;
    if (!self->_private)
	return;
    zbar = ZBAR_GTK_PRIVATE(self->_private);

    g_free((void *)zbar->video_device);
    zbar->video_device	= g_strdup(video_device);
    zbar->video_enabled = video_device && video_device[0];

    /* push another copy to processor idle handler */
    GValue *msg = zbar_gtk_new_value(G_TYPE_STRING);
    if (video_device)
	g_value_set_string(msg, video_device);
    else
	g_value_set_static_string(msg, "");
    g_async_queue_push(zbar->queue, msg);

    g_object_freeze_notify(G_OBJECT(self));
    g_object_notify(G_OBJECT(self), "video-device");
    g_object_notify(G_OBJECT(self), "video-enabled");
    g_object_thaw_notify(G_OBJECT(self));
}

gboolean zbar_gtk_get_video_enabled(ZBarGtk *self)
{
    ZBarGtkPrivate *zbar;

    if (!self->_private)
	return (FALSE);
    zbar = ZBAR_GTK_PRIVATE(self->_private);
    return (zbar->video_enabled);
}

void zbar_gtk_set_video_enabled(ZBarGtk *self, gboolean video_enabled)
{
    ZBarGtkPrivate *zbar;

    if (!self->_private)
	return;
    zbar = ZBAR_GTK_PRIVATE(self->_private);

    video_enabled = (video_enabled != FALSE);
    if (zbar->video_enabled != video_enabled) {
	zbar->video_enabled = video_enabled;

	/* push state change to processor idle handler */
	GValue *msg = zbar_gtk_new_value(G_TYPE_INT);
	g_value_set_int(msg, zbar->video_enabled);
	g_async_queue_push(zbar->queue, msg);

	g_object_notify(G_OBJECT(self), "video-enabled");
    }
}

gboolean zbar_gtk_get_video_opened(ZBarGtk *self)
{
    ZBarGtkPrivate *zbar;

    if (!self->_private)
	return (FALSE);
    zbar = ZBAR_GTK_PRIVATE(self->_private);

    return (zbar->video_opened);
}

void zbar_gtk_request_video_size(ZBarGtk *self, int width, int height)
{
    ZBarGtkPrivate *zbar;

    if (!self->_private || width < 0 || height < 0)
	return;
    zbar = ZBAR_GTK_PRIVATE(self->_private);

    zbar->req_width = zbar->video_width = width;
    zbar->req_height = zbar->video_height = height;
    gtk_widget_queue_resize(GTK_WIDGET(self));
}

static void zbar_gtk_set_property(GObject *object, guint prop_id,
				  const GValue *value, GParamSpec *pspec)
{
    ZBarGtk *self = ZBAR_GTK(object);

    switch (prop_id) {
    case PROP_VIDEO_DEVICE:
	zbar_gtk_set_video_device(self, g_value_get_string(value));
	break;
    case PROP_VIDEO_ENABLED:
	zbar_gtk_set_video_enabled(self, g_value_get_boolean(value));
	break;
    default:
	G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
    }
}

static void zbar_gtk_get_property(GObject *object, guint prop_id, GValue *value,
				  GParamSpec *pspec)
{
    ZBarGtk *self = ZBAR_GTK(object);
    ZBarGtkPrivate *zbar;

    if (!self->_private)
	return;
    zbar = ZBAR_GTK_PRIVATE(self->_private);

    switch (prop_id) {
    case PROP_VIDEO_DEVICE:
	if (zbar->video_device)
	    g_value_set_string(value, zbar->video_device);
	else
	    g_value_set_static_string(value, "");
	break;
    case PROP_VIDEO_ENABLED:
	g_value_set_boolean(value, zbar->video_enabled);
	break;
    case PROP_VIDEO_OPENED:
	g_value_set_boolean(value, zbar->video_opened);
	break;
    default:
	G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
    }
}

static void zbar_gtk_init(ZBarGtk *self)
{
    ZBarGtkPrivate *zbar = g_object_new(ZBAR_TYPE_GTK_PRIVATE, NULL);
    self->_private	 = (void *)zbar;

    zbar->window = zbar_window_create();
    g_assert(zbar->window);

    zbar->req_width = zbar->video_width = DEFAULT_WIDTH;
    zbar->req_height = zbar->video_height = DEFAULT_HEIGHT;

    /* use a queue to signalize about the need to handle decoding and video */
    zbar->queue		      = g_async_queue_new();
    zbar->idle_id	      = g_idle_add(zbar_processing_idle_callback, self);
    zbar->video_enabled_state = FALSE;

    /* Prepare for idle handler */
    g_object_ref(zbar);
    g_assert(zbar->queue);
    g_async_queue_ref(zbar->queue);

    zbar->scanner = zbar_image_scanner_create();
    g_assert(zbar->scanner);
}

static void zbar_gtk_dispose(GObject *object)
{
    ZBarGtk *self = ZBAR_GTK(object);

    if (!self->_private)
	return;

    ZBarGtkPrivate *zbar = ZBAR_GTK_PRIVATE(self->_private);
    self->_private	 = NULL;

    g_free((void *)zbar->video_device);
    zbar->video_device = NULL;

    /* signal processor idle handler to exit */
    GValue *msg = zbar_gtk_new_value(G_TYPE_INT);
    g_value_set_int(msg, -1);
    g_async_queue_push(zbar->queue, msg);
    zbar->idle_id = 0;

    /* there are no external references which might call other APIs */
    g_async_queue_unref(zbar->queue);

    g_object_unref(G_OBJECT(zbar));
}

static void zbar_gtk_private_finalize(GObject *object)
{
    ZBarGtkPrivate *zbar = ZBAR_GTK_PRIVATE(object);

    if (zbar->idle_id) {
	if (zbar->window)
	    zbar_window_draw(zbar->window, NULL);
	g_object_unref(zbar);
	g_source_remove(zbar->idle_id);
	zbar->idle_id = 0;
    }

    if (zbar->window) {
	zbar_window_destroy(zbar->window);
	zbar->window = NULL;
    }
    if (zbar->scanner) {
	zbar_image_scanner_destroy(zbar->scanner);
	zbar->scanner = NULL;
    }
    if (zbar->video) {
	zbar_video_destroy(zbar->video);
	zbar->video = NULL;
    }
    g_async_queue_unref(zbar->queue);
    zbar->queue = NULL;
}

static void zbar_gtk_class_init(ZBarGtkClass *klass)
{
    GParamSpec *p;

    zbar_gtk_parent_class = g_type_class_peek_parent(klass);

    GObjectClass *object_class = G_OBJECT_CLASS(klass);
    object_class->dispose      = zbar_gtk_dispose;
    object_class->set_property = zbar_gtk_set_property;
    object_class->get_property = zbar_gtk_get_property;

    GtkWidgetClass *widget_class = (GtkWidgetClass *)klass;
    widget_class->realize	 = zbar_gtk_realize;
    widget_class->unrealize	 = zbar_gtk_unrealize;
#if GTK_MAJOR_VERSION >= 3
    widget_class->get_preferred_width  = zbar_get_preferred_width;
    widget_class->get_preferred_height = zbar_get_preferred_height;
    widget_class->draw		       = zbar_gtk_scale_draw;
#else
    widget_class->size_request = zbar_gtk_size_request;
    widget_class->expose_event = zbar_gtk_expose;
#endif
    widget_class->size_allocate = zbar_gtk_size_allocate;

    widget_class->unmap = NULL;

    zbar_gtk_signals[DECODED] =
	g_signal_new("decoded", G_TYPE_FROM_CLASS(object_class),
		     G_SIGNAL_RUN_CLEANUP,
		     G_STRUCT_OFFSET(ZBarGtkClass, decoded), NULL, NULL,
		     zbar_marshal_VOID__INT_STRING, G_TYPE_NONE, 2, G_TYPE_INT,
		     G_TYPE_STRING);

    zbar_gtk_signals[DECODED_TEXT] = g_signal_new(
	"decoded-text", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_CLEANUP,
	G_STRUCT_OFFSET(ZBarGtkClass, decoded_text), NULL, NULL,
	g_cclosure_marshal_VOID__STRING, G_TYPE_NONE, 1, G_TYPE_STRING);

    p = g_param_spec_string("video-device", "Video device",
			    "the platform specific name of the video device",
			    NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
    g_object_class_install_property(object_class, PROP_VIDEO_DEVICE, p);

    p = g_param_spec_boolean("video-enabled", "Video enabled",
			     "controls streaming from the video device", FALSE,
			     G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
    g_object_class_install_property(object_class, PROP_VIDEO_ENABLED, p);

    p = g_param_spec_boolean("video-opened", "Video opened",
			     "current opened state of the video device", FALSE,
			     G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    g_object_class_install_property(object_class, PROP_VIDEO_OPENED, p);
}

static void zbar_gtk_private_class_init(ZBarGtkPrivateClass *klass)
{
    GObjectClass *object_class = G_OBJECT_CLASS(klass);
    object_class->finalize     = zbar_gtk_private_finalize;
}

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

    if (!type) {
	static const GTypeInfo info = {
	    sizeof(ZBarGtkPrivateClass),
	    NULL,
	    NULL,
	    (GClassInitFunc)zbar_gtk_private_class_init,
	    NULL,
	    NULL,
	    sizeof(ZBarGtkPrivate),
	};
	type =
	    g_type_register_static(G_TYPE_OBJECT, "ZBarGtkPrivate", &info, 0);
    }
    return (type);
}

GtkWidget *zbar_gtk_new(void)
{
    return (GTK_WIDGET(g_object_new(ZBAR_TYPE_GTK, NULL)));
}
