/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 *
 * Copyright (C) 2006-2007 Richard Hughes <richard@hughsie.com>
 * Copyright (C) 2012-2021 MATE Developers
 *
 * Licensed under the GNU General Public License Version 2
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include "config.h"

#include <string.h>
#include <glib.h>
#include <glib/gi18n.h>

#include <X11/X.h>
#include <gdk/gdkx.h>
#include <gtk/gtk.h>
#include <X11/XF86keysym.h>
#include <libupower-glib/upower.h>

#include "gpm-common.h"
#include "gpm-button.h"

static void     gpm_button_finalize   (GObject	      *object);

struct GpmButtonPrivate
{
	GdkScreen		*screen;
	GdkWindow		*window;
	GHashTable		*keysym_to_name_hash;
	gchar			*last_button;
	GTimer			*timer;
	gboolean		 lid_is_closed;
	UpClient		*client;
};

enum {
	BUTTON_PRESSED,
	LAST_SIGNAL
};

static guint signals [LAST_SIGNAL] = { 0 };
static gpointer gpm_button_object = NULL;

G_DEFINE_TYPE_WITH_PRIVATE (GpmButton, gpm_button, G_TYPE_OBJECT)

#define GPM_BUTTON_DUPLICATE_TIMEOUT	0.125f

/**
 * gpm_button_emit_type:
 **/
static gboolean
gpm_button_emit_type (GpmButton *button, const gchar *type)
{
	g_return_val_if_fail (GPM_IS_BUTTON (button), FALSE);

	/* did we just have this button before the timeout? */
	if (g_strcmp0 (type, button->priv->last_button) == 0 &&
	    g_timer_elapsed (button->priv->timer, NULL) < GPM_BUTTON_DUPLICATE_TIMEOUT) {
		g_debug ("ignoring duplicate button %s", type);
		return FALSE;
	}

	g_debug ("emitting button-pressed : %s", type);
	g_signal_emit (button, signals [BUTTON_PRESSED], 0, type);

	/* save type and last size */
	g_free (button->priv->last_button);
	button->priv->last_button = g_strdup (type);
	g_timer_reset (button->priv->timer);

	return TRUE;
}

/**
 * gpm_button_filter_x_events:
 **/
static GdkFilterReturn
gpm_button_filter_x_events (GdkXEvent *xevent, GdkEvent *event, gpointer data)
{
	GpmButton *button = (GpmButton *) data;
	XEvent *xev = (XEvent *) xevent;
	guint keycode;
	const gchar *key;
	gchar *keycode_str;

	if (xev->type != KeyPress)
		return GDK_FILTER_CONTINUE;

	keycode = xev->xkey.keycode;

	/* is the key string already in our DB? */
	keycode_str = g_strdup_printf ("0x%x", keycode);
	key = g_hash_table_lookup (button->priv->keysym_to_name_hash, (gpointer) keycode_str);
	g_free (keycode_str);

	/* found anything? */
	if (key == NULL) {
		g_debug ("Key %i not found in hash", keycode);
		/* pass normal keypresses on, which might help with accessibility access */
		return GDK_FILTER_CONTINUE;
	}

	g_debug ("Key %i mapped to key %s", keycode, key);
	gpm_button_emit_type (button, key);

	return GDK_FILTER_REMOVE;
}

/**
 * gpm_button_grab_keystring:
 * @button: This button class instance
 * @keystr: The key string, e.g. "<Control><Alt>F11"
 * @hashkey: A unique key made up from the modmask and keycode suitable for
 *           referencing in a hashtable.
 *           You must free this string, or specify NULL to ignore.
 *
 * Grab the key specified in the key string.
 *
 * Return value: TRUE if we parsed and grabbed okay
 **/
static gboolean
gpm_button_grab_keystring (GpmButton *button, guint64 keycode)
{
	guint modmask = AnyModifier;
	Display *display;
	GdkDisplay *gdkdisplay;
	gint ret;

	/* get the current X Display */
	display = GDK_DISPLAY_XDISPLAY (gdk_display_get_default());

	/* don't abort on error */
	gdkdisplay = gdk_display_get_default ();
	gdk_x11_display_error_trap_push (gdkdisplay);

	/* grab the key if possible */
	ret = XGrabKey (display, keycode, modmask,
			GDK_WINDOW_XID (button->priv->window), True,
			GrabModeAsync, GrabModeAsync);
	if (ret == BadAccess) {
		g_warning ("Failed to grab modmask=%u, keycode=%li",
			     modmask, (long int) keycode);
		return FALSE;
	}

	/* grab the lock key if possible */
	ret = XGrabKey (display, keycode, LockMask | modmask,
			GDK_WINDOW_XID (button->priv->window), True,
			GrabModeAsync, GrabModeAsync);
	if (ret == BadAccess) {
		g_warning ("Failed to grab modmask=%u, keycode=%li",
			     LockMask | modmask, (long int) keycode);
		return FALSE;
	}

	/* we are not processing the error */
	gdk_display_flush (gdkdisplay);
	gdk_x11_display_error_trap_pop_ignored (gdkdisplay);

	g_debug ("Grabbed modmask=%x, keycode=%li", modmask, (long int) keycode);
	return TRUE;
}

/**
 * gpm_button_grab_keystring:
 * @button: This button class instance
 * @keystr: The key string, e.g. "<Control><Alt>F11"
 * @hashkey: A unique key made up from the modmask and keycode suitable for
 *           referencing in a hashtable.
 *           You must free this string, or specify NULL to ignore.
 *
 * Grab the key specified in the key string.
 *
 * Return value: TRUE if we parsed and grabbed okay
 **/
static gboolean
gpm_button_xevent_key (GpmButton *button, guint keysym, const gchar *key_name)
{
	gchar *key = NULL;
	gboolean ret;
	gchar *keycode_str;
	guint keycode;

	/* convert from keysym to keycode */
	keycode = XKeysymToKeycode (GDK_DISPLAY_XDISPLAY (gdk_display_get_default()), keysym);
	if (keycode == 0) {
		g_warning ("could not map keysym %x to keycode", keysym);
		return FALSE;
	}

	/* is the key string already in our DB? */
	keycode_str = g_strdup_printf ("0x%x", keycode);
	key = g_hash_table_lookup (button->priv->keysym_to_name_hash, (gpointer) keycode_str);
	if (key != NULL) {
		g_warning ("found in hash %i", keycode);
		g_free (keycode_str);
		return FALSE;
	}

	/* try to register X event */
	ret = gpm_button_grab_keystring (button, keycode);
	if (!ret) {
		g_warning ("Failed to grab %i", keycode);
		g_free (keycode_str);
		return FALSE;
	}

	/* add to hash table */
	g_hash_table_insert (button->priv->keysym_to_name_hash, (gpointer) keycode_str, (gpointer) g_strdup (key_name));

	/* the key is freed in the hash function unref */
	return TRUE;
}

/**
 * gpm_button_class_init:
 * @button: This class instance
 **/
static void
gpm_button_class_init (GpmButtonClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
	object_class->finalize = gpm_button_finalize;

	signals [BUTTON_PRESSED] =
		g_signal_new ("button-pressed",
			      G_TYPE_FROM_CLASS (object_class),
			      G_SIGNAL_RUN_LAST,
			      G_STRUCT_OFFSET (GpmButtonClass, button_pressed),
			      NULL, NULL,
			      g_cclosure_marshal_VOID__STRING,
			      G_TYPE_NONE, 1, G_TYPE_STRING);
}

/**
 * gpm_button_is_lid_closed:
 **/
gboolean
gpm_button_is_lid_closed (GpmButton *button)
{
	GDBusProxy *proxy;
	GVariant *res, *inner;
	gboolean lid;
	GError *error = NULL;

	g_return_val_if_fail (GPM_IS_BUTTON (button), FALSE);

	if (LOGIND_RUNNING()) {
		proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SYSTEM,
						       G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES,
						       NULL,
						       "org.freedesktop.UPower",
						       "/org/freedesktop/UPower",
						       "org.freedesktop.DBus.Properties",
						       NULL,
						       &error );
		if (proxy == NULL) {
			g_error("Error connecting to dbus - %s", error->message);
			g_error_free (error);
			return -1;
		}

		res = g_dbus_proxy_call_sync (proxy, "Get",
					      g_variant_new( "(ss)",
							     "org.freedesktop.UPower",
							     "LidIsClosed"),
					      G_DBUS_CALL_FLAGS_NONE,
					      -1,
					      NULL,
					      &error
					      );
		if (error == NULL && res != NULL) {
			g_variant_get(res, "(v)", &inner );
			lid = g_variant_get_boolean(inner);
			g_variant_unref (inner);
			g_variant_unref (res);
			return lid;
		} else if (error != NULL ) {
			g_error ("Error in dbus - %s", error->message);
			g_error_free (error);
		}
		g_object_unref(proxy);

		return FALSE;
	}
	else {
		return up_client_get_lid_is_closed (button->priv->client);
	}
}

/**
 * gpm_button_reset_time:
 *
 * We have to refresh the event time on resume to handle duplicate buttons
 * properly when the time is significant when we suspend.
 **/
gboolean
gpm_button_reset_time (GpmButton *button)
{
	g_return_val_if_fail (GPM_IS_BUTTON (button), FALSE);
	g_timer_reset (button->priv->timer);
	return TRUE;
}

/**
 * gpm_button_client_changed_cb
 **/
static void
gpm_button_client_changed_cb (UpClient *client, GParamSpec *pspec, GpmButton *button)
{
	gboolean lid_is_closed;

	/* get new state */
	lid_is_closed = gpm_button_is_lid_closed(button);

	/* same state */
	if (button->priv->lid_is_closed == lid_is_closed)
		return;

	/* save state */
	button->priv->lid_is_closed = lid_is_closed;

	/* sent correct event */
	if (lid_is_closed)
		gpm_button_emit_type (button, GPM_BUTTON_LID_CLOSED);
	else
		gpm_button_emit_type (button, GPM_BUTTON_LID_OPEN);
}

/**
 * gpm_button_init:
 * @button: This class instance
 **/
static void
gpm_button_init (GpmButton *button)
{
	button->priv = gpm_button_get_instance_private (button);

	button->priv->screen = gdk_screen_get_default ();
	button->priv->window = gdk_screen_get_root_window (button->priv->screen);

	button->priv->keysym_to_name_hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
	button->priv->last_button = NULL;
	button->priv->timer = g_timer_new ();

	button->priv->client = up_client_new ();
	button->priv->lid_is_closed = up_client_get_lid_is_closed (button->priv->client);
	g_signal_connect (button->priv->client, "notify",
			  G_CALLBACK (gpm_button_client_changed_cb), button);
	/* register the brightness keys */
	gpm_button_xevent_key (button, XF86XK_PowerOff, GPM_BUTTON_POWER);

	/* The kernel messes up suspend/hibernate in some places. One of
	 * them is the key names. Unfortunately, they refuse to see the
	 * errors of their way in the name of 'compatibility'. Meh
	 */
	gpm_button_xevent_key (button, XF86XK_Suspend, GPM_BUTTON_HIBERNATE);
	gpm_button_xevent_key (button, XF86XK_Sleep, GPM_BUTTON_SUSPEND); /* should be configurable */
	gpm_button_xevent_key (button, XF86XK_Hibernate, GPM_BUTTON_HIBERNATE);
	gpm_button_xevent_key (button, XF86XK_MonBrightnessUp, GPM_BUTTON_BRIGHT_UP);
	gpm_button_xevent_key (button, XF86XK_MonBrightnessDown, GPM_BUTTON_BRIGHT_DOWN);
	gpm_button_xevent_key (button, XF86XK_ScreenSaver, GPM_BUTTON_LOCK);
	gpm_button_xevent_key (button, XF86XK_Battery, GPM_BUTTON_BATTERY);
	gpm_button_xevent_key (button, XF86XK_KbdBrightnessUp, GPM_BUTTON_KBD_BRIGHT_UP);
	gpm_button_xevent_key (button, XF86XK_KbdBrightnessDown, GPM_BUTTON_KBD_BRIGHT_DOWN);
	gpm_button_xevent_key (button, XF86XK_KbdLightOnOff, GPM_BUTTON_KBD_BRIGHT_TOGGLE);

	/* use global filter */
	gdk_window_add_filter (button->priv->window,
			       gpm_button_filter_x_events, (gpointer) button);
}

/**
 * gpm_button_finalize:
 * @object: This class instance
 **/
static void
gpm_button_finalize (GObject *object)
{
	GpmButton *button;
	g_return_if_fail (object != NULL);
	g_return_if_fail (GPM_IS_BUTTON (object));

	button = GPM_BUTTON (object);
	button->priv = gpm_button_get_instance_private (button);

	g_object_unref (button->priv->client);
	g_free (button->priv->last_button);
	g_timer_destroy (button->priv->timer);

	g_hash_table_unref (button->priv->keysym_to_name_hash);

	G_OBJECT_CLASS (gpm_button_parent_class)->finalize (object);
}

/**
 * gpm_button_new:
 * Return value: new class instance.
 **/
GpmButton *
gpm_button_new (void)
{
	if (gpm_button_object != NULL) {
		g_object_ref (gpm_button_object);
	} else {
		gpm_button_object = g_object_new (GPM_TYPE_BUTTON, NULL);
		g_object_add_weak_pointer (gpm_button_object, &gpm_button_object);
	}
	return GPM_BUTTON (gpm_button_object);
}