/* $Id$ */
/* Copyright (c) 2012-2020 Pierre Pronchery <khorben@defora.org> */
/* This file is part of DeforaOS Desktop Camera */
/* Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY ITS AUTHORS AND CONTRIBUTORS "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */



#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#ifdef __NetBSD__
# include <sys/videoio.h>
# include <paths.h>
#else
# include <linux/videodev2.h>
#endif
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <libintl.h>
#include <gtk/gtk.h>
#if GTK_CHECK_VERSION(3, 0, 0)
# include <gtk/gtkx.h>
#endif
#include <gdk/gdkkeysyms.h>
#include <System.h>
#include <Desktop.h>
#include "camera.h"
#include "../config.h"
#define _(string) gettext(string)
#define N_(string) (string)

/* constants */
#ifndef PROGNAME_CAMERA
# define PROGNAME_CAMERA	"camera"
#endif
#ifndef PROGNAME_GALLERY
# define PROGNAME_GALLERY	"gallery"
#endif
#ifndef PREFIX
# define PREFIX			"/usr/local"
#endif
#ifndef BINDIR
# define BINDIR			PREFIX "/bin"
#endif

/* macros */
#ifndef MIN
# define MIN(a, b)	((a) < (b) ? (a) : (b))
#endif


/* Camera */
/* private */
/* types */
typedef struct _CameraBuffer
{
	void * start;
	size_t length;
} CameraBuffer;

struct _Camera
{
	String * device;
	gboolean hflip;
	gboolean vflip;
	gboolean ratio;
	GdkInterpType interp;
	CameraSnapshotFormat snapshot_format;
	int snapshot_quality;

	guint source;
	int fd;
	struct v4l2_capability cap;
	struct v4l2_format format;

	/* I/O channel */
	GIOChannel * channel;

	/* input data */
	/* XXX for mmap() */
	CameraBuffer * buffers;
	size_t buffers_cnt;
	char * raw_buffer;
	size_t raw_buffer_cnt;

	/* RGB data */
	unsigned char * rgb_buffer;
	size_t rgb_buffer_cnt;

	/* decoding */
	int yuv_amp;

	/* overlays */
	CameraOverlay ** overlays;
	size_t overlays_cnt;

	/* widgets */
	GtkWidget * widget;
	GtkWidget * window;
	PangoFontDescription * bold;
#if !GTK_CHECK_VERSION(3, 0, 0)
	GdkGC * gc;
#endif
#if GTK_CHECK_VERSION(2, 18, 0)
	GtkWidget * infobar;
	GtkWidget * infobar_label;
#endif
	GtkWidget * area;
	GtkAllocation area_allocation;
	GdkPixbuf * pixbuf;
#if !GTK_CHECK_VERSION(3, 0, 0)
	GdkPixmap * pixmap;
#endif
	/* preferences */
	GtkWidget * pr_window;
	GtkWidget * pr_hflip;
	GtkWidget * pr_vflip;
	GtkWidget * pr_ratio;
	GtkWidget * pr_interp;
	GtkWidget * pr_sformat;
	/* properties */
	GtkWidget * pp_window;
};


/* constants */
#ifdef _PATH_VIDEO0
# define VIDEO_DEVICE	_PATH_VIDEO0
#else
# define VIDEO_DEVICE	"/dev/video0"
#endif

typedef enum _CameraToolbar
{
	CT_SNAPSHOT = 0,
	CT_SEPARATOR1,
	CT_GALLERY,
	CT_SEPARATOR2,
#ifdef EMBEDDED
	CT_PROPERTIES,
	CT_SEPARATOR3,
	CT_PREFERENCES
#else
	CT_PROPERTIES
#endif
} CameraToolbar;


/* prototypes */
/* accessors */
static String * _camera_get_config_filename(Camera * camera, char const * name);

/* useful */
static int _camera_error(Camera * camera, char const * message, int ret);

static int _camera_ioctl(Camera * camera, unsigned long request,
		void * data);

/* callbacks */
static gboolean _camera_on_can_mmap(GIOChannel * channel,
		GIOCondition condition, gpointer data);
static gboolean _camera_on_can_read(GIOChannel * channel,
		GIOCondition condition, gpointer data);
#if GTK_CHECK_VERSION(3, 0, 0)
static gboolean _camera_on_drawing_area_draw(GtkWidget * widget, cairo_t * cr,
		gpointer data);
static void _camera_on_drawing_area_size_allocate(GtkWidget * widget,
		GdkRectangle * allocation, gpointer data);
#else
static gboolean _camera_on_drawing_area_configure(GtkWidget * widget,
		GdkEventConfigure * event, gpointer data);
static gboolean _camera_on_drawing_area_expose(GtkWidget * widget,
		GdkEventExpose * event, gpointer data);
#endif
static void _camera_on_fullscreen(gpointer data);
static void _camera_on_gallery(gpointer data);
static gboolean _camera_on_open(gpointer data);
#ifdef EMBEDDED
static void _camera_on_preferences(gpointer data);
#endif
static void _camera_on_properties(gpointer data);
static gboolean _camera_on_refresh(gpointer data);
static void _camera_on_snapshot(gpointer data);


/* variables */
static DesktopToolbar _camera_toolbar[] =
{
	{ N_("Snapshot"), G_CALLBACK(_camera_on_snapshot), "camera-photo", 0, 0,
		NULL },
	{ "", NULL, NULL, 0, 0, NULL },
	{ N_("Gallery"), G_CALLBACK(_camera_on_gallery), "image-x-generic", 0,
		0, NULL },
#ifdef EMBEDDED
	{ "", NULL, NULL, 0, 0, NULL },
	{ N_("Properties"), G_CALLBACK(_camera_on_properties), "gtk-properties",
		GDK_MOD1_MASK, GDK_KEY_Return, NULL },
	{ "", NULL, NULL, 0, 0, NULL },
	{ N_("Preferences"), G_CALLBACK(_camera_on_preferences),
		"gtk-preferences", GDK_CONTROL_MASK, GDK_KEY_P, NULL },
#else
	{ "", NULL, NULL, 0, 0, NULL },
	{ N_("Properties"), G_CALLBACK(_camera_on_properties), "gtk-properties",
		0, 0, NULL },
#endif
	{ "", NULL, NULL, 0, 0, NULL },
	{ NULL, NULL, NULL, 0, 0, NULL }
};


/* public */
/* functions */
/* camera_new */
Camera * camera_new(GtkWidget * window, GtkAccelGroup * group,
		char const * device)
{
	Camera * camera;
	GtkWidget * vbox;
	GtkWidget * widget;
	GtkToolItem * toolitem;

	if((camera = object_new(sizeof(*camera))) == NULL)
		return NULL;
	camera->device = (device != NULL)
		? string_new(device) : string_new(VIDEO_DEVICE);
	camera->hflip = FALSE;
	camera->vflip = FALSE;
	camera->ratio = TRUE;
	camera->interp = GDK_INTERP_BILINEAR;
	camera->snapshot_format = CSF_PNG;
	camera->snapshot_quality = 100;
	camera->source = 0;
	camera->fd = -1;
	memset(&camera->cap, 0, sizeof(camera->cap));
	camera->channel = NULL;
	camera->buffers = NULL;
	camera->buffers_cnt = 0;
	camera->raw_buffer = NULL;
	camera->raw_buffer_cnt = 0;
	camera->rgb_buffer = NULL;
	camera->rgb_buffer_cnt = 0;
	camera->yuv_amp = 255;
	camera->overlays = NULL;
	camera->overlays_cnt = 0;
	camera->widget = NULL;
	camera->window = window;
	camera->bold = NULL;
#if !GTK_CHECK_VERSION(3, 0, 0)
	camera->gc = NULL;
#endif
	camera->pr_window = NULL;
	camera->pp_window = NULL;
	/* check for errors */
	if(camera->device == NULL)
	{
		camera_delete(camera);
		return NULL;
	}
	/* create the window */
	camera->bold = pango_font_description_new();
	pango_font_description_set_weight(camera->bold, PANGO_WEIGHT_BOLD);
#if !GTK_CHECK_VERSION(3, 0, 0)
	camera->gc = gdk_gc_new(window->window); /* XXX */
#endif
	camera->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
	vbox = camera->widget;
	/* toolbar */
	widget = desktop_toolbar_create(_camera_toolbar, camera, group);
	gtk_widget_set_sensitive(
			GTK_WIDGET(_camera_toolbar[CT_SNAPSHOT].widget), FALSE);
	gtk_widget_set_sensitive(
			GTK_WIDGET(_camera_toolbar[CT_GALLERY].widget), FALSE);
	gtk_widget_set_sensitive(
			GTK_WIDGET(_camera_toolbar[CT_PROPERTIES].widget),
			FALSE);
	toolitem = gtk_tool_button_new(
			gtk_image_new_from_icon_name("gtk-fullscreen",
				GTK_ICON_SIZE_LARGE_TOOLBAR),
			_("Fullscreen"));
	g_signal_connect_swapped(toolitem, "clicked", G_CALLBACK(
				_camera_on_fullscreen), camera);
	gtk_toolbar_insert(GTK_TOOLBAR(widget), toolitem, -1);
	gtk_box_pack_start(GTK_BOX(vbox), widget, FALSE, TRUE, 0);
#if GTK_CHECK_VERSION(2, 18, 0)
	/* infobar */
	camera->infobar = gtk_info_bar_new_with_buttons(GTK_STOCK_CLOSE,
			GTK_RESPONSE_CLOSE, NULL);
	gtk_info_bar_set_message_type(GTK_INFO_BAR(camera->infobar),
			GTK_MESSAGE_ERROR);
	g_signal_connect(camera->infobar, "close", G_CALLBACK(gtk_widget_hide),
			NULL);
	g_signal_connect(camera->infobar, "response", G_CALLBACK(
				gtk_widget_hide), NULL);
	widget = gtk_info_bar_get_content_area(GTK_INFO_BAR(camera->infobar));
	camera->infobar_label = gtk_label_new(NULL);
	gtk_widget_show(camera->infobar_label);
	gtk_box_pack_start(GTK_BOX(widget), camera->infobar_label, TRUE, TRUE,
			0);
	gtk_widget_set_no_show_all(camera->infobar, TRUE);
	gtk_box_pack_start(GTK_BOX(vbox), camera->infobar, FALSE, TRUE, 0);
#endif
	camera->area = gtk_drawing_area_new();
	camera->pixbuf = NULL;
#if !GTK_CHECK_VERSION(3, 0, 0)
	camera->pixmap = NULL;
#endif
#if GTK_CHECK_VERSION(3, 0, 0)
	g_signal_connect(camera->area, "draw", G_CALLBACK(
				_camera_on_drawing_area_draw), camera);
	g_signal_connect(camera->area, "size-allocate", G_CALLBACK(
				_camera_on_drawing_area_size_allocate), camera);
#else
	g_signal_connect(camera->area, "configure-event", G_CALLBACK(
				_camera_on_drawing_area_configure), camera);
	g_signal_connect(camera->area, "expose-event", G_CALLBACK(
				_camera_on_drawing_area_expose), camera);
#endif
	gtk_box_pack_start(GTK_BOX(vbox), camera->area, TRUE, TRUE, 0);
	gtk_widget_show_all(vbox);
	camera_start(camera);
	return camera;
}


/* camera_delete */
void camera_delete(Camera * camera)
{
	camera_stop(camera);
	if(camera->bold != NULL)
		pango_font_description_free(camera->bold);
	string_delete(camera->device);
	object_delete(camera);
}


/* accessors */
/* camera_get_device */
char const * camera_get_device(Camera * camera)
{
	return camera->device;
}


/* camera_get_widget */
GtkWidget * camera_get_widget(Camera * camera)
{
	return camera->widget;
}


/* camera_set_aspect_ratio */
void camera_set_aspect_ratio(Camera * camera, gboolean ratio)
{
	camera->ratio = ratio;
}


/* camera_set_device */
int camera_set_device(Camera * camera, char const * device)
{
	String * p;

	if((p = string_new(device)) == NULL)
		return -1;
	camera_stop(camera);
	string_delete(camera->device);
	camera->device = p;
	camera_start(camera);
	return 0;
}


/* camera_set_hflip */
void camera_set_hflip(Camera * camera, gboolean flip)
{
	camera->hflip = flip;
}


/* camera_set_vflip */
void camera_set_vflip(Camera * camera, gboolean flip)
{
	camera->vflip = flip;
}


/* useful */
/* camera_add_overlay */
CameraOverlay * camera_add_overlay(Camera * camera, char const * filename,
		int opacity)
{
	CameraOverlay ** p;

	if((p = realloc(camera->overlays, (camera->overlays_cnt + 1)
					* sizeof(*p))) == NULL)
		return NULL;
	camera->overlays = p;
	if((camera->overlays[camera->overlays_cnt] = cameraoverlay_new(
					filename, opacity)) == NULL)
		return NULL;
	return camera->overlays[camera->overlays_cnt++];
}


/* camera_load */
char const * _load_variable(Camera * camera, Config * config,
		char const * section, char const * variable);

int camera_load(Camera * camera)
{
	int ret = 0;
	char * filename;
	Config * config;
	char const * p;
	char * q;
	char const jpeg[] = "jpeg";
	int i;

	if((filename = _camera_get_config_filename(camera, CAMERA_CONFIG_FILE))
			== NULL)
		return -1;
	if((config = config_new()) == NULL
			|| config_load(config, filename) != 0)
		ret = -1;
	else
	{
		/* horizontal flipping */
		camera->hflip = FALSE;
		if((p = _load_variable(camera, config, NULL, "hflip")) != NULL
				&& strtoul(p, NULL, 0) != 0)
			camera->hflip = TRUE;
		/* vertical flipping */
		camera->vflip = FALSE;
		if((p = _load_variable(camera, config, NULL, "vflip")) != NULL
				&& strtoul(p, NULL, 0) != 0)
			camera->vflip = TRUE;
		/* aspect ratio */
		camera->ratio = TRUE;
		if((p = _load_variable(camera, config, NULL, "ratio")) != NULL
				&& strtoul(p, NULL, 0) == 0)
			camera->ratio = FALSE;
		/* snapshot format */
		camera->snapshot_format = CSF_PNG;
		if((p = _load_variable(camera, config, "snapshot", "format"))
				!= NULL
				&& strcmp(p, jpeg) == 0)
			camera->snapshot_format = CSF_JPEG;
		/* snapshot quality */
		camera->snapshot_quality = 100;
		if((p = _load_variable(camera, config, "snapshot", "quality"))
				!= NULL
				&& p[0] != '\0' && (i = strtol(p, &q, 10)) >= 0
				&& *q == '\0' && i <= 100)
			camera->snapshot_quality = i;
		/* FIXME also implement interpolation and overlay images */
	}
	if(config != NULL)
		config_delete(config);
	free(filename);
	return ret;
}

char const * _load_variable(Camera * camera, Config * config,
		char const * section, char const * variable)
{
	char const * ret;

	/* check for any value specific to this camera */
	if(section == NULL)
		if((ret = config_get(config, camera->device, variable)) != NULL)
			return ret;
	/* return the global value set (if any) */
	return config_get(config, section, variable);
}


/* camera_open_gallery */
void camera_open_gallery(Camera * camera)
{
	char * argv[] = { BINDIR "/" PROGNAME_GALLERY, PROGNAME_GALLERY, NULL };
	const GSpawnFlags flags = G_SPAWN_FILE_AND_ARGV_ZERO;
	GError * error = NULL;

	if(g_spawn_async(NULL, argv, NULL, flags, NULL, NULL, NULL, &error)
			!= TRUE && error != NULL)
	{
		_camera_error(camera, error->message, 1);
		g_error_free(error);
	}
}


/* camera_save */
static int _save_variable_bool(Camera * camera, Config * config,
		char const * section, char const * variable, gboolean value);
static int _save_variable_int(Camera * camera, Config * config,
		char const * section, char const * variable, int value);
static int _save_variable_string(Camera * camera, Config * config,
		char const * section, char const * variable,
		char const * value);

int camera_save(Camera * camera)
{
	int ret = -1;
	char * filename;
	Config * config;
	char const * sformats[CSF_COUNT] = { NULL, "png", "jpeg" };

	if((filename = _camera_get_config_filename(camera, CAMERA_CONFIG_FILE))
			== NULL)
		return -1;
	if((config = config_new()) != NULL
			&& access(filename, R_OK) == 0
			&& config_load(config, filename) == 0)
	{
		/* XXX may fail */
		_save_variable_bool(camera, config, NULL, "hflip",
				camera->hflip);
		_save_variable_bool(camera, config, NULL, "vflip",
				camera->vflip);
		_save_variable_bool(camera, config, NULL, "ratio",
				camera->ratio);
		_save_variable_string(camera, config, "snapshot", "format",
				sformats[camera->snapshot_format]);
		_save_variable_int(camera, config, "snapshot", "quality",
				camera->snapshot_quality);
		/* FIXME also implement interpolation and overlay images */
		ret = config_save(config, filename);
	}
	if(config != NULL)
		config_delete(config);
	free(filename);
	return ret;
}

static int _save_variable_bool(Camera * camera, Config * config,
		char const * section, char const * variable, gboolean value)
{
	if(section == NULL)
		section = camera->device;
	return config_set(config, section, variable, value ? "1" : "0");
}

static int _save_variable_int(Camera * camera, Config * config,
		char const * section, char const * variable, int value)
{
	char buf[16];

	if(section == NULL)
		section = camera->device;
	snprintf(buf, sizeof(buf), "%d", value);
	return config_set(config, section, variable, buf);
}

static int _save_variable_string(Camera * camera, Config * config,
		char const * section, char const * variable,
		char const * value)
{
	if(section == NULL)
		section = camera->device;
	return config_set(config, section, variable, value);
}


/* camera_show_preferences */
static void _preferences_apply(Camera * camera);
static void _preferences_cancel(Camera * camera);
static void _preferences_save(Camera * camera);
static void _preferences_window(Camera * camera);
/* callbacks */
static void _preferences_on_response(GtkWidget * widget, gint arg1,
		gpointer data);

void camera_show_preferences(Camera * camera, gboolean show)
{
	if(camera->pr_window == NULL)
		_preferences_window(camera);
	if(show)
		gtk_window_present(GTK_WINDOW(camera->pr_window));
	else
		gtk_widget_hide(camera->pr_window);
}

static void _preferences_apply(Camera * camera)
{
	GtkTreeModel * model;
	GtkTreeIter iter;

	camera->hflip = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(
				camera->pr_hflip));
	camera->vflip = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(
				camera->pr_vflip));
	camera->ratio = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(
				camera->pr_ratio));
	/* interpolation */
	if(gtk_combo_box_get_active_iter(GTK_COMBO_BOX(camera->pr_interp),
				&iter) == TRUE)
	{
		model = gtk_combo_box_get_model(GTK_COMBO_BOX(
					camera->pr_interp));
		gtk_tree_model_get(model, &iter, 0, &camera->interp, -1);
	}
	/* snapshot format */
	if(gtk_combo_box_get_active_iter(GTK_COMBO_BOX(camera->pr_sformat),
				&iter) == TRUE)
	{
		model = gtk_combo_box_get_model(GTK_COMBO_BOX(
					camera->pr_sformat));
		gtk_tree_model_get(model, &iter, 0, &camera->snapshot_format,
				-1);
	}
}

static void _preferences_cancel(Camera * camera)
{
	GtkTreeModel * model;
	GtkTreeIter iter;
	gboolean valid;
	GdkInterpType interp;
	CameraSnapshotFormat format;

	gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(camera->pr_hflip),
			camera->hflip);
	gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(camera->pr_vflip),
			camera->vflip);
	gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(camera->pr_ratio),
			camera->ratio);
	/* interpolation */
	model = gtk_combo_box_get_model(GTK_COMBO_BOX(camera->pr_interp));
	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, 0, &interp, -1);
		if(interp == camera->interp)
			break;
	}
	if(valid)
		gtk_combo_box_set_active_iter(GTK_COMBO_BOX(camera->pr_interp),
				&iter);
	else
		gtk_combo_box_set_active(GTK_COMBO_BOX(camera->pr_interp), 0);
	/* snapshot format */
	model = gtk_combo_box_get_model(GTK_COMBO_BOX(camera->pr_sformat));
	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, 0, &format, -1);
		if(format == camera->snapshot_format)
			break;
	}
	if(valid)
		gtk_combo_box_set_active_iter(GTK_COMBO_BOX(camera->pr_sformat),
				&iter);
	else
		gtk_combo_box_set_active(GTK_COMBO_BOX(camera->pr_sformat), 0);
}

static void _preferences_save(Camera * camera)
{
	camera_save(camera);
}

static void _preferences_window(Camera * camera)
{
	GtkWidget * dialog;
	GtkWidget * notebook;
	GtkWidget * vbox;
	GtkWidget * widget;
	GtkListStore * store;
	GtkTreeIter iter;
	GtkCellRenderer * renderer;
	const struct {
		GdkInterpType type;
		char const * name;
	} interp[] =
	{
		{ GDK_INTERP_NEAREST, N_("Nearest") },
		{ GDK_INTERP_TILES, N_("Tiles") },
		{ GDK_INTERP_BILINEAR, N_("Bilinear") },
		{ GDK_INTERP_HYPER, N_("Hyperbolic") },
	};
	const struct {
		CameraSnapshotFormat format;
		char const * name;
	} sformats[CSF_COUNT - 1] =
	{
		{ CSF_JPEG, "JPEG" },
		{ CSF_PNG, "PNG" }
	};
	size_t i;

	dialog = gtk_dialog_new_with_buttons(_("Preferences"),
			GTK_WINDOW(camera->window),
			GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
			GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
			GTK_STOCK_APPLY, GTK_RESPONSE_APPLY,
			GTK_STOCK_OK, GTK_RESPONSE_OK, NULL);
	gtk_window_set_resizable(GTK_WINDOW(dialog), FALSE);
	camera->pr_window = dialog;
	g_signal_connect(dialog, "response", G_CALLBACK(
				_preferences_on_response), camera);
	notebook = gtk_notebook_new();
	/* picture */
	vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
	gtk_container_set_border_width(GTK_CONTAINER(vbox), 4);
	camera->pr_hflip = gtk_check_button_new_with_mnemonic(
			_("Flip _horizontally"));
	gtk_box_pack_start(GTK_BOX(vbox), camera->pr_hflip, FALSE, TRUE, 0);
	camera->pr_vflip = gtk_check_button_new_with_mnemonic(
			_("Flip _vertically"));
	gtk_box_pack_start(GTK_BOX(vbox), camera->pr_vflip, FALSE, TRUE, 0);
	camera->pr_ratio = gtk_check_button_new_with_mnemonic(
			_("Keep aspect _ratio"));
	gtk_box_pack_start(GTK_BOX(vbox), camera->pr_ratio, FALSE, TRUE, 0);
	/* interpolation */
	widget = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
	gtk_box_pack_start(GTK_BOX(widget), gtk_label_new(_("Interpolation: ")),
			FALSE, TRUE, 0);
	store = gtk_list_store_new(2, G_TYPE_UINT, G_TYPE_STRING);
	for(i = 0; i < sizeof(interp) / sizeof(*interp); i++)
	{
		gtk_list_store_append(store, &iter);
		gtk_list_store_set(store, &iter, 0, interp[i].type,
				1, _(interp[i].name), -1);
	}
	camera->pr_interp = gtk_combo_box_new_with_model(GTK_TREE_MODEL(store));
	renderer = gtk_cell_renderer_text_new();
	gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(camera->pr_interp), renderer,
			TRUE);
	gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(camera->pr_interp),
			renderer, "text", 1, NULL);
	gtk_box_pack_start(GTK_BOX(widget), camera->pr_interp, TRUE, TRUE, 0);
	gtk_box_pack_start(GTK_BOX(vbox), widget, FALSE, TRUE, 0);
	gtk_notebook_append_page(GTK_NOTEBOOK(notebook), vbox,
			gtk_label_new(_("Picture")));
	/* snapshots */
	vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
	gtk_container_set_border_width(GTK_CONTAINER(vbox), 4);
	/* format */
	widget = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
	gtk_box_pack_start(GTK_BOX(widget), gtk_label_new(_("Format: ")),
			FALSE, TRUE, 0);
	store = gtk_list_store_new(2, G_TYPE_UINT, G_TYPE_STRING);
	for(i = 0; i < sizeof(sformats) / sizeof(*sformats); i++)
	{
		gtk_list_store_append(store, &iter);
		gtk_list_store_set(store, &iter, 0, sformats[i].format,
				1, sformats[i].name, -1);
	}
	camera->pr_sformat = gtk_combo_box_new_with_model(
			GTK_TREE_MODEL(store));
	renderer = gtk_cell_renderer_text_new();
	gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(camera->pr_sformat),
			renderer, TRUE);
	gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(camera->pr_sformat),
			renderer, "text", 1, NULL);
	gtk_box_pack_start(GTK_BOX(widget), camera->pr_sformat, TRUE, TRUE, 0);
	gtk_box_pack_start(GTK_BOX(vbox), widget, FALSE, TRUE, 0);
	gtk_notebook_append_page(GTK_NOTEBOOK(notebook), vbox,
			gtk_label_new(_("Snapshots")));
	vbox = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
	gtk_box_set_spacing(GTK_BOX(vbox), 4);
	gtk_box_pack_start(GTK_BOX(vbox), notebook, TRUE, TRUE, 0);
	gtk_widget_show_all(vbox);
	_preferences_cancel(camera);
}

static void _preferences_on_response(GtkWidget * widget, gint arg1,
		gpointer data)
{
	Camera * camera = data;

	switch(arg1)
	{
		case GTK_RESPONSE_APPLY:
			_preferences_apply(camera);
			break;
		case GTK_RESPONSE_OK:
			gtk_widget_hide(widget);
			_preferences_apply(camera);
			_preferences_save(camera);
			break;
		case GTK_RESPONSE_DELETE_EVENT:
			camera->pr_window = NULL;
			break;
		case GTK_RESPONSE_CANCEL:
		default:
			gtk_widget_hide(widget);
			_preferences_cancel(camera);
			break;
	}
}


/* camera_show_properties */
static GtkWidget * _properties_label(Camera * camera, GtkSizeGroup * group,
		char const * label, char const * value);
static void _properties_window(Camera * camera);
/* callbacks */
static void _properties_on_response(gpointer data);

void camera_show_properties(Camera * camera, gboolean show)
{
	if(camera->rgb_buffer == NULL)
		/* ignore the action */
		return;
	if(show)
	{
		if(camera->pp_window == NULL)
			_properties_window(camera);
		gtk_window_present(GTK_WINDOW(camera->pp_window));
	}
	else
	{
		if(camera->pp_window != NULL)
			gtk_widget_destroy(camera->pp_window);
		camera->pp_window = NULL;
	}
}

static GtkWidget * _properties_label(Camera * camera, GtkSizeGroup * group,
		char const * label, char const * value)
{
	GtkWidget * hbox;
	GtkWidget * widget;

	hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
	widget = gtk_label_new(label);
	gtk_widget_override_font(widget, camera->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);
	widget = gtk_label_new((value != NULL) ? value : "");
#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_box_pack_start(GTK_BOX(hbox), widget, TRUE, TRUE, 0);
	return hbox;
}

static void _properties_window(Camera * camera)
{
	GtkWidget * dialog;
	GtkSizeGroup * group;
	GtkWidget * vbox;
	GtkWidget * hbox;
	char buf[64];
	const struct
	{
		unsigned int capability;
		char const * name;
	} capabilities[] =
	{
		{ V4L2_CAP_VIDEO_CAPTURE,	"capture"	},
		{ V4L2_CAP_VIDEO_OUTPUT,	"output"	},
		{ V4L2_CAP_VIDEO_OVERLAY,	"overlay"	},
		{ V4L2_CAP_TUNER,		"tuner"		},
		{ V4L2_CAP_AUDIO,		"audio"		},
		{ V4L2_CAP_STREAMING,		"streaming"	},
		{ 0,				NULL		}
	};
	unsigned int i;
	char const * sep = "";

	dialog = gtk_message_dialog_new(GTK_WINDOW(camera->window),
			GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
			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
			"");
	camera->pp_window = dialog;
#if GTK_CHECK_VERSION(2, 10, 0)
	gtk_message_dialog_set_image(GTK_MESSAGE_DIALOG(dialog),
			gtk_image_new_from_icon_name("gtk-preferences",
				GTK_ICON_SIZE_DIALOG));
#endif
	gtk_window_set_title(GTK_WINDOW(dialog), _("Properties"));
	g_signal_connect_swapped(dialog, "response", G_CALLBACK(
				_properties_on_response), camera);
	group = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
	vbox = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
	/* driver */
	snprintf(buf, sizeof(buf), "%-16s", (char *)camera->cap.driver);
	hbox = _properties_label(camera, group, _("Driver: "), buf);
	gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, TRUE, 0);
	/* card */
	snprintf(buf, sizeof(buf), "%-32s", (char *)camera->cap.card);
	hbox = _properties_label(camera, group, _("Card: "), buf);
	gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, TRUE, 0);
	/* bus info */
	snprintf(buf, sizeof(buf), "%-32s", (char *)camera->cap.bus_info);
	hbox = _properties_label(camera, group, _("Bus info: "), buf);
	gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, TRUE, 0);
	/* version */
	snprintf(buf, sizeof(buf), "0x%x", camera->cap.version);
	hbox = _properties_label(camera, group, _("Version: "), buf);
	gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, TRUE, 0);
	/* capabilities */
	buf[0] = '\0';
	for(i = 0; capabilities[i].name != NULL; i++)
		if(camera->cap.capabilities & capabilities[i].capability)
		{
			strncat(buf, sep, sizeof(buf) - strlen(buf));
			strncat(buf, capabilities[i].name, sizeof(buf)
					- strlen(buf));
			sep = ", ";
		}
	buf[sizeof(buf) - 1] = '\0';
	hbox = _properties_label(camera, group, _("Capabilities: "), buf);
	gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, TRUE, 0);
	gtk_widget_show_all(vbox);
}

static void _properties_on_response(gpointer data)
{
	Camera * camera = data;

	gtk_widget_destroy(camera->pp_window);
	camera->pp_window = NULL;
}


/* camera_snapshot */
static int _snapshot_dcim(Camera * camera, char const * homedir,
		char const * dcim);
static char * _snapshot_path(Camera * camera, char const * homedir,
		char const * dcim, char const * extension);
static int _snapshot_save(Camera * camera, char const * path,
		CameraSnapshotFormat format);

int camera_snapshot(Camera * camera, CameraSnapshotFormat format)
{
	int ret;
	char const * homedir;
	char const dcim[] = "DCIM";
	char const * ext[CSF_COUNT] = { NULL, ".png", ".jpeg" };
	char const * e;
	char * path;

	if(camera->rgb_buffer == NULL)
		/* ignore the action */
		return 0;
	if(format == CSF_DEFAULT)
		format = camera->snapshot_format;
	switch(format)
	{
		case CSF_JPEG:
		case CSF_PNG:
			e = ext[format];
			break;
		default:
			format = CSF_PNG;
			e = ext[format];
			break;
	}
	if((homedir = getenv("HOME")) == NULL)
		homedir = g_get_home_dir();
	if(_snapshot_dcim(camera, homedir, dcim) != 0)
		return -1;
	if((path = _snapshot_path(camera, homedir, dcim, e)) == NULL)
		return -1;
	ret = _snapshot_save(camera, path, format);
	free(path);
	return ret;
}

static int _snapshot_dcim(Camera * camera, char const * homedir,
		char const * dcim)
{
	char * path;

	if((path = g_build_filename(homedir, dcim, NULL)) == NULL)
		return -_camera_error(camera, _("Could not save picture"), 1);
	if(mkdir(path, 0777) != 0 && errno != EEXIST)
	{
		error_set_code(-errno, "%s: %s: %s",
				_("Could not save picture"), path, strerror(errno));
		free(path);
		return -_camera_error(camera, error_get(NULL), 1);
	}
	free(path);
	return 0;
}

static char * _snapshot_path(Camera * camera, char const * homedir,
		char const * dcim, char const * extension)
{
	struct timeval tv;
	struct tm tm;
	unsigned int i;
	char * filename;
	char * path;

	if(gettimeofday(&tv, NULL) != 0 || gmtime_r(&tv.tv_sec, &tm) == NULL)
	{
		error_set_code(-errno, "%s: %s", _("Could not save picture"),
				strerror(errno));
		_camera_error(camera, error_get(NULL), 1);
		return NULL;
	}
	for(i = 0; i < 64; i++)
	{
		if((filename = g_strdup_printf("%u%02u%02u-%02u%02u%02u-%03u%s",
						tm.tm_year + 1900,
						tm.tm_mon + 1, tm.tm_mday,
						tm.tm_hour, tm.tm_min,
						tm.tm_sec, i + 1, extension))
				== NULL)
			/* XXX report error */
			return NULL;
		path = g_build_filename(homedir, dcim, filename, NULL);
		g_free(filename);
		if(path == NULL)
		{
			_camera_error(camera, _("Could not save picture"), 1);
			return NULL;
		}
#ifdef DEBUG
		fprintf(stderr, "DEBUG: %s() %s\n", __func__, path);
#endif
		if(access(path, R_OK) != 0 && errno == ENOENT)
			return path;
		g_free(path);
	}
	return NULL;
}

static int _snapshot_save(Camera * camera, char const * path,
		CameraSnapshotFormat format)
{
	struct v4l2_pix_format * pix = &camera->format.fmt.pix;
	GdkPixbuf * pixbuf;
	char buf[16];
	gboolean res;
	GError * error = NULL;

	if((pixbuf = gdk_pixbuf_new_from_data(camera->rgb_buffer,
					GDK_COLORSPACE_RGB, FALSE, 8,
					pix->width, pix->height, pix->width * 3,
					NULL, NULL)) == NULL)
		return -_camera_error(camera, _("Could not save picture"), 1);
	switch(format)
	{
		case CSF_JPEG:
			snprintf(buf, sizeof(buf), "%d",
					camera->snapshot_quality);
			res = gdk_pixbuf_save(pixbuf, path, "jpeg", &error,
					"quality", buf, NULL);
			break;
		case CSF_PNG:
			res = gdk_pixbuf_save(pixbuf, path, "png", &error,
					NULL);
			break;
		default:
			res = FALSE;
			break;
	}
	g_object_unref(pixbuf);
	if(res != TRUE)
	{
		error_set_code(1, "%s: %s", _("Could not save picture"),
				(error != NULL) ? error->message
				: _("Unknown error"));
		g_error_free(error);
		return -_camera_error(camera, error_get(NULL), 1);
	}
	return 0;
}


/* camera_start */
void camera_start(Camera * camera)
{
	if(camera->source != 0)
		return;
	camera->source = g_idle_add(_camera_on_open, camera);
}


/* camera_stop */
void camera_stop(Camera * camera)
{
	size_t i;

	if(camera->source != 0)
		g_source_remove(camera->source);
	camera->source = 0;
	if(camera->pp_window != NULL)
		gtk_widget_destroy(camera->pp_window);
	camera->pp_window = NULL;
	if(camera->pr_window != NULL)
		gtk_widget_destroy(camera->pr_window);
	camera->pr_window = NULL;
	for(i = 0; i < camera->overlays_cnt; i++)
		cameraoverlay_delete(camera->overlays[i]);
	free(camera->overlays);
	camera->overlays = NULL;
	camera->overlays_cnt = 0;
	if(camera->channel != NULL)
	{
		/* XXX we ignore errors at this point */
		g_io_channel_shutdown(camera->channel, TRUE, NULL);
		g_io_channel_unref(camera->channel);
	}
	camera->channel = NULL;
	if(camera->pixbuf != NULL)
		g_object_unref(camera->pixbuf);
	camera->pixbuf = NULL;
#if !GTK_CHECK_VERSION(3, 0, 0)
	if(camera->pixmap != NULL)
		g_object_unref(camera->pixmap);
	camera->pixmap = NULL;
	if(camera->gc != NULL)
		g_object_unref(camera->gc);
	camera->gc = NULL;
#endif
	if((char *)camera->rgb_buffer != camera->raw_buffer)
		free(camera->rgb_buffer);
	camera->rgb_buffer = NULL;
	for(i = 0; i < camera->buffers_cnt; i++)
		if(camera->buffers[i].start != MAP_FAILED)
			munmap(camera->buffers[i].start,
					camera->buffers[i].length);
	free(camera->buffers);
	camera->buffers = NULL;
	camera->buffers_cnt = 0;
	free(camera->raw_buffer);
	camera->raw_buffer = NULL;
}


/* private */
/* functions */
/* accessors */
/* camera_get_config_filename */
static String * _camera_get_config_filename(Camera * camera, char const * name)
{
	char const * homedir;
	(void) camera;

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


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

static int _camera_error(Camera * camera, char const * message, int ret)
{
#if !GTK_CHECK_VERSION(2, 18, 0)
	GtkWidget * dialog;
#endif

	if(camera == NULL)
		return _error_text(message, ret);
#if GTK_CHECK_VERSION(2, 18, 0)
	gtk_label_set_text(GTK_LABEL(camera->infobar_label), message);
	gtk_widget_show(camera->infobar);
#else
	dialog = gtk_message_dialog_new(GTK_WINDOW(camera->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_CAMERA, message);
	return ret;
}


/* camera_ioctl */
static int _camera_ioctl(Camera * camera, unsigned long request,
		void * data)
{
	int ret;

	for(;;)
		if((ret = ioctl(camera->fd, request, data)) != -1
				|| errno != EINTR)
			break;
	return ret;
}


/* callbacks */
/* camera_on_can_mmap */
static gboolean _camera_on_can_mmap(GIOChannel * channel,
		GIOCondition condition, gpointer data)
{
	Camera * camera = data;
	struct v4l2_buffer buf;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s()\n", __func__);
#endif
	if(channel != camera->channel || condition != G_IO_IN)
		return FALSE;
	if(_camera_ioctl(camera, VIDIOC_DQBUF, &buf) == -1)
	{
		_camera_error(camera, _("Could not save picture"), 1);
		return FALSE;
	}
	camera->raw_buffer = camera->buffers[buf.index].start;
	camera->raw_buffer_cnt = buf.bytesused;
#if 0 /* FIXME the raw buffer is not meant to be free()'d */
	camera->source = g_idle_add(_camera_on_refresh, camera);
	return FALSE;
#else
	_camera_on_refresh(camera);
	camera->raw_buffer = NULL;
	camera->raw_buffer_cnt = 0;
	return TRUE;
#endif
}


/* camera_on_can_read */
static gboolean _camera_on_can_read(GIOChannel * channel,
		GIOCondition condition, gpointer data)
{
	Camera * camera = data;
	GIOStatus status;
	gsize size;
	GError * error = NULL;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s()\n", __func__);
#endif
	if(channel != camera->channel || condition != G_IO_IN)
		return FALSE;
	status = g_io_channel_read_chars(channel, camera->raw_buffer,
			camera->raw_buffer_cnt, &size, &error);
	/* this status can be ignored */
	if(status == G_IO_STATUS_AGAIN)
		return TRUE;
	if(status == G_IO_STATUS_ERROR)
	{
		g_io_channel_shutdown(camera->channel, TRUE, NULL);
		g_io_channel_unref(camera->channel);
		camera->channel = NULL;
		camera->fd = -1;
		_camera_error(camera, error->message, 1);
		g_error_free(error);
		gtk_widget_set_sensitive(GTK_WIDGET(
					_camera_toolbar[CT_SNAPSHOT].widget),
				FALSE);
		gtk_widget_set_sensitive(GTK_WIDGET(
					_camera_toolbar[CT_GALLERY].widget),
				FALSE);
		gtk_widget_set_sensitive(GTK_WIDGET(
					_camera_toolbar[CT_PROPERTIES].widget),
				FALSE);
		camera->source = 0;
		return FALSE;
	}
#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s() %lu %lu\n", __func__,
			camera->raw_buffer_cnt, size);
#endif
	camera->source = g_idle_add(_camera_on_refresh, camera);
	return FALSE;
}


#if GTK_CHECK_VERSION(3, 0, 0)
/* camera_on_drawing_area_draw */
static gboolean _camera_on_drawing_area_draw(GtkWidget * widget, cairo_t * cr,
		gpointer data)
{
	Camera * camera = data;
	(void) widget;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s() %p\n", __func__, camera->pixbuf);
#endif
	if(camera->pixbuf != NULL)
	{
		gdk_cairo_set_source_pixbuf(cr, camera->pixbuf, 0, 0);
		cairo_paint(cr);
	}
	return TRUE;
}


/* camera_on_drawing_area_size_allocate */
static void _camera_on_drawing_area_size_allocate(GtkWidget * widget,
		GdkRectangle * allocation, gpointer data)
{
	Camera * camera = data;
	(void) widget;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s()\n", __func__);
#endif
	camera->area_allocation = *allocation;
}

#else

/* camera_on_drawing_area_configure */
static gboolean _camera_on_drawing_area_configure(GtkWidget * widget,
		GdkEventConfigure * event, gpointer data)
{
	(void) event;

	/* XXX this code is inspired from GQcam */
	Camera * camera = data;
	GtkAllocation * allocation = &camera->area_allocation;

	if(camera->pixmap != NULL)
		g_object_unref(camera->pixmap);
	/* FIXME requires Gtk+ 2.18 */
	gtk_widget_get_allocation(widget, allocation);
#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s() %dx%d\n", __func__, allocation->width,
			allocation->height);
#endif
	camera->pixmap = gdk_pixmap_new(widget->window, allocation->width,
			allocation->height, -1);
	/* FIXME is it not better to scale the previous pixmap for now? */
	gdk_draw_rectangle(camera->pixmap, camera->gc, TRUE, 0, 0,
			allocation->width, allocation->height);
	return TRUE;
}


/* camera_on_drawing_area_expose */
static gboolean _camera_on_drawing_area_expose(GtkWidget * widget,
		GdkEventExpose * event, gpointer data)
{
	/* XXX this code is inspired from GQcam */
	Camera * camera = data;

	gdk_draw_pixmap(widget->window, camera->gc, camera->pixmap,
			event->area.x, event->area.y,
			event->area.x, event->area.y,
			event->area.width, event->area.height);
	return FALSE;
}
#endif


/* camera_on_fullscreen */
static void _camera_on_fullscreen(gpointer data)
{
	Camera * camera = data;
	GdkWindow * window;
	GdkWindowState state;

	window = gtk_widget_get_window(camera->window);
	state = gdk_window_get_state(window);
	if(state & GDK_WINDOW_STATE_FULLSCREEN)
		gtk_window_unfullscreen(GTK_WINDOW(camera->window));
	else
		gtk_window_fullscreen(GTK_WINDOW(camera->window));
}


/* camera_on_gallery */
static void _camera_on_gallery(gpointer data)
{
	Camera * camera = data;

	camera_open_gallery(camera);
}


/* camera_on_open */
static int _open_setup(Camera * camera);
#ifdef NOTYET
static int _open_setup_mmap(Camera * camera);
#endif
static int _open_setup_read(Camera * camera);

static gboolean _camera_on_open(gpointer data)
{
	Camera * camera = data;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s() \"%s\"\n", __func__, camera->device);
#endif
	camera->source = 0;
	if((camera->fd = open(camera->device, O_RDWR)) < 0)
	{
		error_set_code(-errno, "%s: %s (%s)", camera->device,
				_("Could not open the video capture device"),
				strerror(errno));
		_camera_error(camera, error_get(NULL), 1);
		return FALSE;
	}
	if(_open_setup(camera) != 0)
	{
		_camera_error(camera, error_get(NULL), 1);
		close(camera->fd);
		camera->fd = -1;
		/* FIXME also free camera->buffers */
		return FALSE;
	}
#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s() %dx%d\n", __func__,
			camera->format.fmt.pix.width,
			camera->format.fmt.pix.height);
#endif
	gtk_widget_set_sensitive(
			GTK_WIDGET(_camera_toolbar[CT_SNAPSHOT].widget), TRUE);
	gtk_widget_set_sensitive(
			GTK_WIDGET(_camera_toolbar[CT_GALLERY].widget), TRUE);
	gtk_widget_set_sensitive(
			GTK_WIDGET(_camera_toolbar[CT_PROPERTIES].widget),
			TRUE);
	/* FIXME allow the window to be smaller */
	gtk_widget_set_size_request(camera->area, camera->format.fmt.pix.width,
			camera->format.fmt.pix.height);
	return FALSE;
}

static int _open_setup(Camera * camera)
{
	int ret;
	struct v4l2_cropcap cropcap;
	struct v4l2_crop crop;
	GError * error = NULL;

	/* check for capabilities */
	if(_camera_ioctl(camera, VIDIOC_QUERYCAP, &camera->cap) == -1)
		return error_set_code(-errno, "%s: %s (%s)", camera->device,
				_("Could not obtain the capabilities"),
				strerror(errno));
	if((camera->cap.capabilities & V4L2_CAP_VIDEO_CAPTURE) == 0)
		return -error_set_code(1, "%s: %s", camera->device,
				_("Not a video capture device"));
	/* reset cropping */
	memset(&cropcap, 0, sizeof(cropcap));
	cropcap.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	if(_camera_ioctl(camera, VIDIOC_CROPCAP, &cropcap) == 0)
	{
		/* reset to default */
		crop.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
		crop.c = cropcap.defrect;
		if(_camera_ioctl(camera, VIDIOC_S_CROP, &crop) == -1
				&& errno == EINVAL)
			/* XXX ignore this error for now */
			error_set_code(1, "%s: %s", camera->device,
					_("Cropping not supported"));
	}
	/* obtain the current format */
	if(_camera_ioctl(camera, VIDIOC_G_FMT, &camera->format) == -1)
		return -error_set_code(1, "%s: %s", camera->device,
				_("Could not obtain the video capture format"));
	/* check the current format */
	if(camera->format.type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
		return -error_set_code(1, "%s: %s", camera->device,
				_("Unsupported video capture type"));
	if((camera->cap.capabilities & V4L2_CAP_STREAMING) != 0)
#ifdef NOTYET
		ret = _open_setup_mmap(camera);
#else
		ret = _open_setup_read(camera);
#endif
	else if((camera->cap.capabilities & V4L2_CAP_READWRITE) != 0)
		ret = _open_setup_read(camera);
	else
		ret = -error_set_code(1, "%s: %s", camera->device,
				_("Unsupported capabilities"));
	if(ret != 0)
		return ret;
	/* setup an I/O channel */
	camera->channel = g_io_channel_unix_new(camera->fd);
	if(g_io_channel_set_encoding(camera->channel, NULL, &error)
			!= G_IO_STATUS_NORMAL)
	{
		error_set_code(1, "%s", error->message);
		g_error_free(error);
		return -1;
	}
	g_io_channel_set_buffered(camera->channel, FALSE);
	camera->source = g_io_add_watch(camera->channel, G_IO_IN,
			(camera->buffers != NULL) ? _camera_on_can_mmap
			: _camera_on_can_read, camera);
	return 0;
}

#ifdef NOTYET
static int _open_setup_mmap(Camera * camera)
{
	struct v4l2_requestbuffers req;
	size_t i;
	struct v4l2_buffer buf;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s()\n", __func__);
#endif
	/* memory mapping support */
	req.count = 4;
	req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	req.memory = V4L2_MEMORY_MMAP;
	if(_camera_ioctl(camera, VIDIOC_REQBUFS, &req) == -1)
		return -error_set_code(1, "%s: %s", camera->device,
				_("Could not request buffers"));
	if(req.count < 2)
		return -error_set_code(1, "%s: %s", camera->device,
				_("Could not obtain enough buffers"));
	if((camera->buffers = malloc(sizeof(*camera->buffers) * req.count))
			== NULL)
		return -error_set_code(1, "%s: %s", camera->device,
				_("Could not allocate buffers"));
	camera->buffers_cnt = req.count;
	/* initialize the buffers */
	memset(camera->buffers, 0, sizeof(*camera->buffers)
			* camera->buffers_cnt);
	for(i = 0; i < camera->buffers_cnt; i++)
		camera->buffers[i].start = MAP_FAILED;
	/* map the buffers */
	for(i = 0; i < camera->buffers_cnt; i++)
	{
		memset(&buf, 0, sizeof(buf));
		buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
		buf.memory = V4L2_MEMORY_MMAP;
		buf.index = i;
		if(_camera_ioctl(camera, VIDIOC_QUERYBUF, &buf) == -1)
			return -error_set_code(1, "%s: %s", camera->device,
					_("Could not setup buffers"));
		camera->buffers[i].start = mmap(NULL, buf.length,
				PROT_READ | PROT_WRITE, MAP_SHARED, camera->fd,
				buf.m.offset);
		if(camera->buffers[i].start == MAP_FAILED)
			return -error_set_code(1, "%s: %s", camera->device,
					_("Could not map buffers"));
		camera->buffers[i].length = buf.length;
	}
	return 0;
}
#endif

static int _open_setup_read(Camera * camera)
{
	size_t cnt;
	char * p;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s()\n", __func__);
#endif
	/* FIXME also try to obtain a RGB24 format if possible */
	/* allocate the raw buffer */
	cnt = camera->format.fmt.pix.sizeimage;
	if((p = realloc(camera->raw_buffer, cnt)) == NULL)
		return error_set_code(-errno, "%s: %s", camera->device,
				strerror(errno));
	camera->raw_buffer = p;
	camera->raw_buffer_cnt = cnt;
	/* allocate the rgb buffer */
	cnt = camera->format.fmt.pix.width * camera->format.fmt.pix.height * 3;
	if((p = realloc(camera->rgb_buffer, cnt)) == NULL)
		return error_set_code(-errno, "%s: %s", camera->device,
				strerror(errno));
	camera->rgb_buffer = (unsigned char *)p;
	camera->rgb_buffer_cnt = cnt;
	return 0;
}


#ifdef EMBEDDED
/* camera_on_preferences */
static void _camera_on_preferences(gpointer data)
{
	Camera * camera = data;

	camera_show_preferences(camera, TRUE);
}
#endif


/* camera_on_properties */
static void _camera_on_properties(gpointer data)
{
	Camera * camera = data;

	camera_show_properties(camera, TRUE);
}


/* camera_on_refresh */
static void _refresh_convert(Camera * camera);
static void _refresh_convert_yuv(int amp, uint8_t y, uint8_t u, uint8_t v,
		uint8_t * r, uint8_t * g, uint8_t * b);
static void _refresh_hflip(Camera * camera, GdkPixbuf ** pixbuf);
static void _refresh_overlays(Camera * camera, GdkPixbuf * pixbuf);
static void _refresh_scale(Camera * camera, GdkPixbuf ** pixbuf);
static void _refresh_vflip(Camera * camera, GdkPixbuf ** pixbuf);

static gboolean _camera_on_refresh(gpointer data)
{
	Camera * camera = data;
#if !GTK_CHECK_VERSION(3, 0, 0)
	GtkAllocation * allocation = &camera->area_allocation;
#endif
	int width = camera->format.fmt.pix.width;
	int height = camera->format.fmt.pix.height;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s() 0x%x\n", __func__,
			camera->format.fmt.pix.pixelformat);
#endif
	_refresh_convert(camera);
#if !GTK_CHECK_VERSION(3, 0, 0)
	if(camera->hflip == FALSE
			&& camera->vflip == FALSE
			&& width == allocation->width
			&& height == allocation->height
			&& camera->overlays_cnt == 0)
		/* render directly */
		gdk_draw_rgb_image(camera->pixmap, camera->gc, 0, 0,
				width, height, GDK_RGB_DITHER_NORMAL,
				camera->rgb_buffer, width * 3);
	else
#endif
	{
		if(camera->pixbuf != NULL)
			g_object_unref(camera->pixbuf);
		/* render after scaling */
		camera->pixbuf = gdk_pixbuf_new_from_data(camera->rgb_buffer,
				GDK_COLORSPACE_RGB, FALSE, 8, width, height,
				width * 3, NULL, NULL);
		_refresh_hflip(camera, &camera->pixbuf);
		_refresh_vflip(camera, &camera->pixbuf);
		_refresh_scale(camera, &camera->pixbuf);
		_refresh_overlays(camera, camera->pixbuf);
#if !GTK_CHECK_VERSION(3, 0, 0)
		gdk_pixbuf_render_to_drawable(camera->pixbuf, camera->pixmap,
				camera->gc, 0, 0, 0, 0, -1, -1,
				GDK_RGB_DITHER_NORMAL, 0, 0);
#endif
	}
	/* force a refresh */
	gtk_widget_queue_draw(camera->area);
	camera->source = g_io_add_watch(camera->channel, G_IO_IN,
			(camera->buffers != NULL) ? _camera_on_can_mmap
			: _camera_on_can_read, camera);
	return FALSE;
}

static void _refresh_convert(Camera * camera)
{
	size_t i;
	size_t j;

	switch(camera->format.fmt.pix.pixelformat)
	{
		case V4L2_PIX_FMT_YUYV:
			for(i = 0, j = 0; i + 3 < camera->raw_buffer_cnt;
					i += 4, j += 6)
			{
				/* pixel 0 */
				_refresh_convert_yuv(camera->yuv_amp,
						camera->raw_buffer[i],
						camera->raw_buffer[i + 1],
						camera->raw_buffer[i + 3],
						&camera->rgb_buffer[j + 2],
						&camera->rgb_buffer[j + 1],
						&camera->rgb_buffer[j]);
				/* pixel 1 */
				_refresh_convert_yuv(camera->yuv_amp,
						camera->raw_buffer[i + 2],
						camera->raw_buffer[i + 1],
						camera->raw_buffer[i + 3],
						&camera->rgb_buffer[j + 5],
						&camera->rgb_buffer[j + 4],
						&camera->rgb_buffer[j + 3]);
			}
			break;
		default:
#ifdef DEBUG
			fprintf(stderr, "DEBUG: %s() Unsupported format\n",
					__func__);
#endif
			break;
	}
}

static void _refresh_convert_yuv(int amp, uint8_t y, uint8_t u, uint8_t v,
		uint8_t * r, uint8_t * g, uint8_t * b)
{
	double dr;
	double dg;
	double db;

	dr = amp * (0.004565 * y + 0.007935 * u - 1.088);
	dg = amp * (0.004565 * y - 0.001542 * u - 0.003183 * v + 0.531);
	db = amp * (0.004565 * y + 0.000001 * u + 0.006250 * v - 0.872);
	*r = (dr < 0) ? 0 : ((dr > 255) ? 255 : dr);
	*g = (dg < 0) ? 0 : ((dg > 255) ? 255 : dg);
	*b = (db < 0) ? 0 : ((db > 255) ? 255 : db);
}

static void _refresh_hflip(Camera * camera, GdkPixbuf ** pixbuf)
{
	GdkPixbuf * pixbuf2;

	if(camera->hflip == FALSE)
		return;
	/* XXX could probably be more efficient */
	pixbuf2 = gdk_pixbuf_flip(*pixbuf, TRUE);
	g_object_unref(*pixbuf);
	*pixbuf = pixbuf2;
}

static void _refresh_overlays(Camera * camera, GdkPixbuf * pixbuf)
{
	size_t i;

	for(i = 0; i < camera->overlays_cnt; i++)
		cameraoverlay_blit(camera->overlays[i], pixbuf);
}

static void _refresh_scale(Camera * camera, GdkPixbuf ** pixbuf)
{
	GtkAllocation * allocation = &camera->area_allocation;
	GdkPixbuf * pixbuf2;
	gdouble scale;
	gint width;
	gint height;
	gint x;
	gint y;

	if(allocation->width > 0 && allocation->height > 0
			&& (uint32_t)allocation->width
			== camera->format.fmt.pix.width
			&& (uint32_t)allocation->height
			== camera->format.fmt.pix.height)
		/* no need to scale anything */
		return;
	if(camera->ratio == FALSE)
		pixbuf2 = gdk_pixbuf_scale_simple(*pixbuf, allocation->width,
				allocation->height, camera->interp);
	else
	{
		if((pixbuf2 = gdk_pixbuf_new(GDK_COLORSPACE_RGB, FALSE, 8,
						allocation->width,
						allocation->height)) == NULL)
			/* XXX report errors */
			return;
		/* XXX could be more efficient */
		gdk_pixbuf_fill(pixbuf2, 0);
		scale = (gdouble)allocation->width
			/ camera->format.fmt.pix.width;
		scale = MIN(scale, (gdouble)allocation->height
				/ camera->format.fmt.pix.height);
		width = (gdouble)camera->format.fmt.pix.width * scale;
		width = MIN(width, allocation->width);
		height = (gdouble)camera->format.fmt.pix.height * scale;
		height = MIN(height, allocation->height);
		x = (allocation->width - width) / 2;
		y = (allocation->height - height) / 2;
		gdk_pixbuf_scale(*pixbuf, pixbuf2, x, y, width, height,
				0.0, 0.0, scale, scale, camera->interp);
	}
	g_object_unref(*pixbuf);
	*pixbuf = pixbuf2;
}

static void _refresh_vflip(Camera * camera, GdkPixbuf ** pixbuf)
{
	GdkPixbuf * pixbuf2;

	if(camera->vflip == FALSE)
		return;
	/* XXX could probably be more efficient */
	pixbuf2 = gdk_pixbuf_flip(*pixbuf, FALSE);
	g_object_unref(*pixbuf);
	*pixbuf = pixbuf2;
}


/* camera_on_snapshot */
static void _camera_on_snapshot(gpointer data)
{
	Camera * camera = data;

	camera_snapshot(camera, CSF_DEFAULT);
}
