/*
 * Pidgin - Internet Messenger
 * Copyright (C) Pidgin Developers <devel@pidgin.im>
 *
 * Pidgin is the legal property of its developers, whose names are too numerous
 * to list here. Please refer to the COPYRIGHT file distributed with this
 * source distribution.
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program; if not, see <https://www.gnu.org/licenses/>.
 */

#include <glib/gi18n-lib.h>

#include <birb.h>

#include <qrencode.h>

#include "pidginqrcode.h"

struct _PidginQrCode {
	GtkWidget parent;

	PurpleQrCode *qr_code;

	GtkWidget *popover;
	GSignalGroup *signal_group;
	GdkTexture *texture;
};

enum {
	PROP_0,
	PROP_QR_CODE,
	N_PROPERTIES,
};
static GParamSpec *properties[N_PROPERTIES] = {NULL, };

/******************************************************************************
 * Helpers
 *****************************************************************************/
static void
pidgin_qr_code_update(PidginQrCode *widget) {
	GByteArray *raw_data = NULL;
	GBytes *raw_bytes = NULL;
	QRcode *qr_code = NULL;
	const char *text = NULL;
	guint width = 0;
	guint8 background = 0xFF; /* White */
	guint8 foreground = 0x0; /* Black */

	/* Clear any existing popups as the text could change between a URI and not
	 * which would invalidate the menu.
	 */
	g_clear_pointer(&widget->popover, gtk_widget_unparent);

	text = purple_qr_code_get_text(widget->qr_code);
	qr_code = QRcode_encodeString(text, 0, QR_ECLEVEL_L, QR_MODE_8, 1);

	/* Encode the raw image data. */
	width = qr_code->width;
	raw_data = g_byte_array_sized_new(width * width);
	for(guint i = 0; i < width * width; i++) {
		if((qr_code->data[i] & 1) == 1) {
			g_byte_array_append(raw_data, &foreground, 1);
		} else {
			g_byte_array_append(raw_data, &background, 1);
		}
	}
	raw_bytes = g_byte_array_free_to_bytes(raw_data);
	QRcode_free(qr_code);

	/* Build the texture. */
	widget->texture = gdk_memory_texture_new(width,
	                                         width,
	                                         GDK_MEMORY_G8,
	                                         raw_bytes,
	                                         width);

	g_clear_pointer(&raw_bytes, g_bytes_unref);
}

static void
pidgin_qr_code_do_popup(PidginQrCode *qr_code, GdkRectangle *rect) {
	if(!GTK_IS_POPOVER(qr_code->popover)) {
		GMenu *menu = NULL;
		const char *text = NULL;

		menu = g_menu_new();

		text = purple_qr_code_get_text(qr_code->qr_code);
		if(g_uri_is_valid(text, G_URI_FLAGS_PARSE_RELAXED, NULL)) {
			g_menu_append(menu, _("_Open Link"), "link.open");
			g_menu_append(menu, _("Copy _Link Address"), "link.copy");
		} else {
			g_menu_append(menu, _("Copy _Text"), "text.copy");
		}

		qr_code->popover = gtk_popover_menu_new_from_model(G_MENU_MODEL(menu));
		gtk_widget_set_parent(qr_code->popover, GTK_WIDGET(qr_code));
		gtk_popover_set_has_arrow(GTK_POPOVER(qr_code->popover), FALSE);
		gtk_widget_set_halign(qr_code->popover, GTK_ALIGN_START);

		gtk_accessible_update_property(GTK_ACCESSIBLE(qr_code->popover),
		                               GTK_ACCESSIBLE_PROPERTY_LABEL, _("Context menu"),
		                              -1);

		g_clear_object(&menu);
	}

	gtk_popover_set_pointing_to(GTK_POPOVER(qr_code->popover), rect);

	gtk_popover_popup(GTK_POPOVER(qr_code->popover));
}

/******************************************************************************
 * Callbacks
 *****************************************************************************/
static void
pidgin_qr_code_text_notify(G_GNUC_UNUSED GObject *obj,
                           G_GNUC_UNUSED GParamSpec *pspec,
                           gpointer data)
{
	PidginQrCode *qr_code = PIDGIN_QR_CODE(data);

	pidgin_qr_code_update(qr_code);
	gtk_widget_queue_draw(GTK_WIDGET(qr_code));
}

static void
pidgin_qr_code_gesture_click_pressed(GtkGestureClick *self,
                                     G_GNUC_UNUSED gint n_press,
                                     gdouble x,
                                     gdouble y,
                                     gpointer data)
{
	PidginQrCode *qr_code = data;
	GdkRectangle rect = {x, y, 1, 1};

	gtk_gesture_set_state(GTK_GESTURE(self), GTK_EVENT_SEQUENCE_CLAIMED);

	pidgin_qr_code_do_popup(qr_code, &rect);
}

/******************************************************************************
 * Actions
 *****************************************************************************/
static void
pidgin_qr_code_copy_text(GtkWidget *self,
                         G_GNUC_UNUSED const char *action_name,
                         G_GNUC_UNUSED GVariant *parameter)
{
	PidginQrCode *qr_code = PIDGIN_QR_CODE(self);
	GdkClipboard *clipboard = NULL;
	const char *text = NULL;

	text = purple_qr_code_get_text(qr_code->qr_code);

	clipboard = gtk_widget_get_clipboard(self);
	gdk_clipboard_set_text(clipboard, text);
}

static void
pidgin_qr_code_menu_popup(GtkWidget *self,
                          G_GNUC_UNUSED const char *action_name,
                          G_GNUC_UNUSED GVariant *parameter)
{
	pidgin_qr_code_do_popup(PIDGIN_QR_CODE(self), NULL);
}

static void
pidgin_qr_code_open_link(GtkWidget *self,
                         G_GNUC_UNUSED const char *action_name,
                         G_GNUC_UNUSED GVariant *parameter)
{
	PidginQrCode *qr_code = PIDGIN_QR_CODE(self);
	PurpleUi *ui = NULL;
	const char *text = NULL;

	text = purple_qr_code_get_text(qr_code->qr_code);
	ui = purple_core_get_ui(purple_core_get_default());

	purple_ui_open_uri(ui, text, NULL, NULL, NULL);
}

/******************************************************************************
 * GtkWidget Implementation
 *****************************************************************************/
static GtkSizeRequestMode
pidgin_qr_code_get_request_mode(G_GNUC_UNUSED GtkWidget *widget) {
	return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
}

static void
pidgin_qr_code_measure(GtkWidget *widget,
                       G_GNUC_UNUSED GtkOrientation orientation,
                       int for_size,
                       int *minimum,
                       int *natural,
                       G_GNUC_UNUSED int *minimum_baseline,
                       G_GNUC_UNUSED int *natural_baseline)
{
	PidginQrCode *qr_code = PIDGIN_QR_CODE(widget);
	int texture_size = 0;
	double concrete_size = 0.0;

	if(qr_code->texture) {
		texture_size = gdk_texture_get_width(qr_code->texture);
	}

	gdk_paintable_compute_concrete_size(GDK_PAINTABLE(qr_code->texture),
	                                    (for_size < 0) ? 0 : for_size,
	                                    0,
	                                    texture_size,
	                                    texture_size,
	                                    &concrete_size,
	                                    &concrete_size);

	*minimum = texture_size;
	*natural = ceil(concrete_size);
}

static void
pidgin_qr_code_snapshot(GtkWidget *widget, GtkSnapshot *snapshot) {
	PidginQrCode *qr_code = PIDGIN_QR_CODE(widget);
	int height = 0;
	int size = 0;
	int width = 0;
	int x = 0;
	int y = 0;

	if(!GDK_IS_TEXTURE(qr_code->texture)) {
		return;
	}

	width = gtk_widget_get_width(widget);
	height = gtk_widget_get_height(widget);

	size = MIN(width, height);
	x = (width - size) / 2;
	y = (height - size) / 2;

	gtk_snapshot_append_scaled_texture(snapshot,
	                                   qr_code->texture,
	                                   GSK_SCALING_FILTER_NEAREST,
	                                   &GRAPHENE_RECT_INIT(x, y, size, size));
}

/******************************************************************************
 * GObject Implementation
 *****************************************************************************/
G_DEFINE_FINAL_TYPE(PidginQrCode, pidgin_qr_code, GTK_TYPE_WIDGET)

static void
pidgin_qr_code_finalize(GObject *obj) {
	PidginQrCode *widget = PIDGIN_QR_CODE(obj);

	g_clear_pointer(&widget->popover, gtk_widget_unparent);
	g_clear_object(&widget->qr_code);
	g_clear_object(&widget->texture);

	G_OBJECT_CLASS(pidgin_qr_code_parent_class)->finalize(obj);
}

static void
pidgin_qr_code_get_property(GObject *obj,
                            guint param_id,
                            GValue *value,
                            GParamSpec *pspec)
{
	PidginQrCode *widget = PIDGIN_QR_CODE(obj);

	switch(param_id) {
	case PROP_QR_CODE:
		g_value_set_object(value, pidgin_qr_code_get_qr_code(widget));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
		break;
	}
}

static void
pidgin_qr_code_set_property(GObject *obj,
                            guint param_id,
                            const GValue *value,
                            GParamSpec *pspec)
{
	PidginQrCode *widget = PIDGIN_QR_CODE(obj);

	switch(param_id) {
	case PROP_QR_CODE:
		pidgin_qr_code_set_qr_code(widget, g_value_get_object(value));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
		break;
	}
}

static void
pidgin_qr_code_init(PidginQrCode *self) {
	GtkGesture *gesture = NULL;

	gtk_widget_set_can_focus(GTK_WIDGET(self), TRUE);
	gtk_widget_set_focusable(GTK_WIDGET(self), TRUE);
	gtk_widget_set_focus_on_click(GTK_WIDGET(self), TRUE);

	gesture = gtk_gesture_click_new();
	gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(gesture),
	                              GDK_BUTTON_SECONDARY);
	g_signal_connect_object(gesture,
	                        "pressed",
	                        G_CALLBACK(pidgin_qr_code_gesture_click_pressed),
	                        self,
	                        G_CONNECT_DEFAULT);
	gtk_widget_add_controller(GTK_WIDGET(self), GTK_EVENT_CONTROLLER(gesture));

	self->signal_group = g_signal_group_new(PURPLE_TYPE_QR_CODE);
	g_signal_group_connect_object(self->signal_group,
	                              "notify::text",
	                              G_CALLBACK(pidgin_qr_code_text_notify),
	                              self,
	                              G_CONNECT_DEFAULT);

	gtk_accessible_update_property(GTK_ACCESSIBLE(self),
	                               GTK_ACCESSIBLE_PROPERTY_HAS_POPUP, TRUE,
	                               -1);
}

static void
pidgin_qr_code_class_init(PidginQrCodeClass *klass) {
	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
	GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);

	obj_class->finalize = pidgin_qr_code_finalize;
	obj_class->get_property = pidgin_qr_code_get_property;
	obj_class->set_property = pidgin_qr_code_set_property;

	/**
	 * PidginQrCode:qr-code:
	 *
	 * The QR Code being displayed.
	 *
	 * Since: 3.0
	 */
	properties[PROP_QR_CODE] = g_param_spec_object(
	    "qr-code", NULL, NULL,
	    PURPLE_TYPE_QR_CODE,
	    G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);

	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);

	widget_class->get_request_mode = pidgin_qr_code_get_request_mode;
	widget_class->measure = pidgin_qr_code_measure;
	widget_class->snapshot = pidgin_qr_code_snapshot;

	gtk_widget_class_set_accessible_role(widget_class,
	                                     GTK_ACCESSIBLE_ROLE_IMG);
	gtk_widget_class_set_css_name(widget_class, "pidgin-qr-code");

	/* Actions. */
	/**
	 * PidginQrCode|link.copy:
	 *
	 * Copies the contents of the QR Code link.
	 *
	 * Since: 3.0
	 */
	gtk_widget_class_install_action(widget_class,
	                                "link.copy",
	                                NULL,
	                                pidgin_qr_code_copy_text);

	/**
	 * PidginQrCode|link.open:
	 *
	 * Opens the URI of the QR Code.
	 *
	 * Since: 3.0
	 */
	gtk_widget_class_install_action(widget_class,
	                                "link.open",
	                                NULL,
	                                pidgin_qr_code_open_link);

	/**
	 * PidginQrCode|menu.popup:
	 *
	 * Opens the context menu.
	 *
	 * Since: 3.0
	 */
	gtk_widget_class_install_action(widget_class,
	                                "menu.popup",
	                                NULL,
	                                pidgin_qr_code_menu_popup);

	/**
	 * PidginQrCode|text.copy:
	 *
	 * Copies the contents of the QR Code.
	 *
	 * Since: 3.0
	 */
	gtk_widget_class_install_action(widget_class,
	                                "text.copy",
	                                NULL,
	                                pidgin_qr_code_copy_text);

	/* Keybindings. */
	gtk_widget_class_add_binding_action(widget_class,
	                                    GDK_KEY_F10,
	                                    GDK_SHIFT_MASK,
	                                    "menu.popup",
	                                    NULL);
	gtk_widget_class_add_binding_action(widget_class,
	                                    GDK_KEY_Menu,
	                                    GDK_NO_MODIFIER_MASK,
	                                    "menu.popup",
	                                    NULL);
#ifdef __APPLE__
	gtk_widget_class_add_binding_action(widget_class,
	                                    GDK_KEY_c,
	                                    GDK_META_MASK,
	                                    "text.copy",
	                                    NULL);
#else /* __APPLE__ */
	gtk_widget_class_add_binding_action(widget_class,
	                                    GDK_KEY_c,
	                                    GDK_CONTROL_MASK,
	                                    "text.copy",
	                                    NULL);
#endif /* __APPLE__ */
}

/******************************************************************************
 * Public API
 *****************************************************************************/
PurpleQrCode *
pidgin_qr_code_get_qr_code(PidginQrCode *widget) {
	g_return_val_if_fail(PIDGIN_IS_QR_CODE(widget), NULL);

	return widget->qr_code;
}

GtkWidget *
pidgin_qr_code_new(PurpleQrCode *qr_code) {
	g_return_val_if_fail(PURPLE_IS_QR_CODE(qr_code), NULL);

	return g_object_new(PIDGIN_TYPE_QR_CODE, "qr-code", qr_code, NULL);
}

void
pidgin_qr_code_set_qr_code(PidginQrCode *widget, PurpleQrCode *qr_code) {
	g_return_if_fail(PIDGIN_IS_QR_CODE(widget));

	if(g_set_object(&widget->qr_code, qr_code)) {
		pidgin_qr_code_update(widget);

		g_signal_group_set_target(widget->signal_group, qr_code);

		g_object_notify_by_pspec(G_OBJECT(widget), properties[PROP_QR_CODE]);
	}
}
