/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
 *
 * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu>
 * Copyright (C) 2010 Red Hat, Inc.
 *
 * 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 <http://www.gnu.org/licenses/>.
 *
 */

#include "config.h"

#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

#include <locale.h>

#include <gdk/gdk.h>

#ifdef GDK_WINDOWING_WAYLAND
#include <gdk/gdkwayland.h>
#endif
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif

#if HAVE_WACOM
#include <libwacom/libwacom.h>
#endif

#include "gsd-enums.h"
#include "gnome-settings-profile.h"
#include "gnome-settings-bus.h"
#include "gsd-wacom-manager.h"
#include "gsd-wacom-oled.h"
#include "gsd-settings-migrate.h"
#include "gsd-input-helper.h"


#define GSD_WACOM_MANAGER_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), GSD_TYPE_WACOM_MANAGER, GsdWacomManagerPrivate))

#define UNKNOWN_DEVICE_NOTIFICATION_TIMEOUT 15000

#define GSD_DBUS_NAME "org.gnome.SettingsDaemon"
#define GSD_DBUS_PATH "/org/gnome/SettingsDaemon"
#define GSD_DBUS_BASE_INTERFACE "org.gnome.SettingsDaemon"

#define GSD_WACOM_DBUS_PATH GSD_DBUS_PATH "/Wacom"
#define GSD_WACOM_DBUS_NAME GSD_DBUS_NAME ".Wacom"

#define LEFT_HANDED_KEY		"left-handed"

static const gchar introspection_xml[] =
"<node name='/org/gnome/SettingsDaemon/Wacom'>"
"  <interface name='org.gnome.SettingsDaemon.Wacom'>"
"    <method name='SetGroupModeLED'>"
"      <arg name='device_path' direction='in' type='s'/>"
"      <arg name='group' direction='in' type='u'/>"
"      <arg name='mode' direction='in' type='u'/>"
"    </method>"
"    <method name='SetOLEDLabels'>"
"      <arg name='device_path' direction='in' type='s'/>"
"      <arg name='labels' direction='in' type='as'/>"
"    </method>"
"  </interface>"
"</node>";

struct GsdWacomManagerPrivate
{
        guint start_idle_id;
        GdkSeat *seat;
        guint device_added_id;

        GsdShell *shell_proxy;

        gchar *machine_id;

#if HAVE_WACOM
        WacomDeviceDatabase *wacom_db;
#endif

        /* DBus */
        GDBusNodeInfo   *introspection_data;
        GDBusConnection *dbus_connection;
        GCancellable    *dbus_cancellable;
        guint            dbus_register_object_id;
        guint            name_id;
};

static void     gsd_wacom_manager_class_init  (GsdWacomManagerClass *klass);
static void     gsd_wacom_manager_init        (GsdWacomManager      *wacom_manager);
static void     gsd_wacom_manager_finalize    (GObject              *object);

static gboolean set_led (const gchar  *device_path,
                         guint         group,
                         guint         index,
                         GError      **error);
static gboolean is_opaque_tablet (GsdWacomManager *manager,
                                  GdkDevice       *device);

G_DEFINE_TYPE (GsdWacomManager, gsd_wacom_manager, G_TYPE_OBJECT)

static gpointer manager_object = NULL;

static GVariant *
map_tablet_mapping (GVariant *value)
{
        const gchar *mapping;

        mapping = g_variant_get_boolean (value) ? "absolute" : "relative";
        return g_variant_new_string (mapping);
}

static GVariant *
map_tablet_left_handed (GVariant *value)
{
        const gchar *rotation = g_variant_get_string (value, NULL);
        return g_variant_new_boolean (g_strcmp0 (rotation, "half") == 0 ||
                                      g_strcmp0 (rotation, "ccw") == 0);
}

static void
migrate_tablet_settings (GsdWacomManager *manager,
                         GdkDevice       *device)
{
        GsdSettingsMigrateEntry tablet_settings[] = {
                { "is-absolute", "mapping", map_tablet_mapping },
                { "keep-aspect", "keep-aspect", NULL },
                { "rotation", "left-handed", map_tablet_left_handed },
        };
        gchar *old_path, *new_path;
        const gchar *vendor, *product;

        vendor = gdk_device_get_vendor_id (device);
        product = gdk_device_get_product_id (device);

        old_path = g_strdup_printf ("/org/gnome/settings-daemon/peripherals/wacom/%s-usb:%s:%s/",
                                    manager->priv->machine_id, vendor, product);
        new_path = g_strdup_printf ("/org/gnome/desktop/peripherals/tablets/%s:%s/",
                                    vendor, product);

        gsd_settings_migrate_check ("org.gnome.settings-daemon.peripherals.wacom.deprecated",
                                    old_path,
                                    "org.gnome.desktop.peripherals.tablet",
                                    new_path,
                                    tablet_settings, G_N_ELEMENTS (tablet_settings));

        /* Opaque tablets' mapping may be modified by users, so only these
         * need migration of settings.
         */
        if (is_opaque_tablet (manager, device)) {
                GsdSettingsMigrateEntry display_setting[] = {
                        { "display", "output", NULL },
                };

                gsd_settings_migrate_check ("org.gnome.desktop.peripherals.tablet.deprecated",
                                            new_path,
                                            "org.gnome.desktop.peripherals.tablet",
                                            new_path,
                                            display_setting, G_N_ELEMENTS (display_setting));
        }

        g_free (old_path);
        g_free (new_path);
}

static void
gsd_wacom_manager_class_init (GsdWacomManagerClass *klass)
{
        GObjectClass   *object_class = G_OBJECT_CLASS (klass);

        object_class->finalize = gsd_wacom_manager_finalize;

        g_type_class_add_private (klass, sizeof (GsdWacomManagerPrivate));
}

static gchar *
get_device_path (GdkDevice *device)
{
#ifdef HAVE_WAYLAND
        if (gnome_settings_is_wayland ())
                return g_strdup (gdk_wayland_device_get_node_path (device));
        else
#endif
                return xdevice_get_device_node (gdk_x11_device_get_id (device));
}

static gboolean
is_opaque_tablet (GsdWacomManager *manager,
                  GdkDevice       *device)
{
        gboolean is_opaque = FALSE;
#if HAVE_WACOM
        WacomDevice *wacom_device;
        gchar *devpath;

        devpath = get_device_path (device);
        wacom_device = libwacom_new_from_path (manager->priv->wacom_db, devpath,
                                               WFALLBACK_GENERIC, NULL);
        if (wacom_device) {
                WacomIntegrationFlags integration_flags;

                integration_flags = libwacom_get_integration_flags (wacom_device);
                is_opaque = (integration_flags &
                             (WACOM_DEVICE_INTEGRATED_DISPLAY | WACOM_DEVICE_INTEGRATED_SYSTEM)) == 0;
                libwacom_destroy (wacom_device);
        }

#endif
        return is_opaque;
}

static GdkDevice *
lookup_device_by_path (GsdWacomManager *manager,
                       const gchar     *path)
{
        GList *devices, *l;

        devices = gdk_seat_get_slaves (manager->priv->seat,
                                       GDK_SEAT_CAPABILITY_ALL);

        for (l = devices; l; l = l->next) {
                GdkDevice *device = l->data;
                gchar *dev_path = get_device_path (device);

                if (g_strcmp0 (dev_path, path) == 0) {
                        g_free (dev_path);
                        return device;
                }

                g_free (dev_path);
        }

        g_list_free (devices);

        return NULL;
}

static GSettings *
device_get_settings (GdkDevice *device)
{
        GSettings *settings;
        gchar *path;

        path = g_strdup_printf ("/org/gnome/desktop/peripherals/tablets/%s:%s/",
                                gdk_device_get_vendor_id (device),
                                gdk_device_get_product_id (device));
        settings = g_settings_new_with_path ("org.gnome.desktop.peripherals.tablet",
                                             path);
        g_free (path);

        return settings;
}

static void
handle_method_call (GDBusConnection       *connection,
                    const gchar           *sender,
                    const gchar           *object_path,
                    const gchar           *interface_name,
                    const gchar           *method_name,
                    GVariant              *parameters,
                    GDBusMethodInvocation *invocation,
                    gpointer               data)
{
	GsdWacomManager *self = GSD_WACOM_MANAGER (data);
        GError *error = NULL;
        GdkDevice *device;

        if (g_strcmp0 (method_name, "SetGroupModeLED") == 0) {
                gchar *device_path;
                guint group, mode;

		g_variant_get (parameters, "(suu)", &device_path, &group, &mode);
                device = lookup_device_by_path (self, device_path);
                if (!device) {
                        g_dbus_method_invocation_return_value (invocation, NULL);
                        return;
                }

                if (set_led (device_path, group, mode, &error))
                        g_dbus_method_invocation_return_value (invocation, NULL);
                else
                        g_dbus_method_invocation_return_gerror (invocation, error);
        } else if (g_strcmp0 (method_name, "SetOLEDLabels") == 0) {
                gchar *device_path, *label;
                gboolean left_handed;
                GSettings *settings;
                GVariantIter *iter;
                gint i = 0;

		g_variant_get (parameters, "(sas)", &device_path, &iter);
                device = lookup_device_by_path (self, device_path);
                if (!device) {
                        g_dbus_method_invocation_return_value (invocation, NULL);
                        return;
                }

                settings = device_get_settings (device);
                left_handed = g_settings_get_boolean (settings, LEFT_HANDED_KEY);
                g_object_unref (settings);

                while (g_variant_iter_loop (iter, "s", &label)) {
                        if (!set_oled (device_path, left_handed, i, label, &error)) {
                                g_free (label);
                                break;
                        }
                        i++;
                }

                g_variant_iter_free (iter);

                if (error)
                        g_dbus_method_invocation_return_gerror (invocation, error);
                else
                        g_dbus_method_invocation_return_value (invocation, NULL);
        }
}

static const GDBusInterfaceVTable interface_vtable =
{
	handle_method_call,
	NULL, /* Get Property */
	NULL, /* Set Property */
};

static gboolean
set_led (const gchar  *device_path,
         guint         group,
	 guint         index,
         GError      **error)
{
	char *command;
	gboolean ret;

#ifndef HAVE_GUDEV
	/* Not implemented on non-Linux systems */
	return TRUE;
#endif

	g_debug ("Switching group ID %d to index %d for device %s", group, index, device_path);

	command = g_strdup_printf ("pkexec " LIBEXECDIR "/gsd-wacom-led-helper --path %s --group %d --led %d",
				   device_path, group, index);
	ret = g_spawn_command_line_sync (command,
					 NULL,
					 NULL,
					 NULL,
					 error);
	g_free (command);

        return ret;
}

static void
device_added_cb (GdkSeat         *seat,
                 GdkDevice       *device,
                 GsdWacomManager *manager)
{
        if (gdk_device_get_source (device) == GDK_SOURCE_PEN &&
            gdk_device_get_device_type (device) == GDK_DEVICE_TYPE_SLAVE) {
                migrate_tablet_settings (manager, device);
        }
}

static void
add_devices (GsdWacomManager     *manager,
             GdkSeatCapabilities  capabilities)
{
        GList *devices, *l;

        devices = gdk_seat_get_slaves (manager->priv->seat, capabilities);
        for (l = devices; l ; l = l->next)
		device_added_cb (manager->priv->seat, l->data, manager);
        g_list_free (devices);
}

static void
set_devicepresence_handler (GsdWacomManager *manager)
{
        GdkSeat *seat;

        seat = gdk_display_get_default_seat (gdk_display_get_default ());
        manager->priv->device_added_id = g_signal_connect (seat, "device-added",
                                                           G_CALLBACK (device_added_cb), manager);
        manager->priv->seat = seat;
}

static void
gsd_wacom_manager_init (GsdWacomManager *manager)
{
        manager->priv = GSD_WACOM_MANAGER_GET_PRIVATE (manager);
#if HAVE_WACOM
        manager->priv->wacom_db = libwacom_database_new ();
#endif
}

static gboolean
gsd_wacom_manager_idle_cb (GsdWacomManager *manager)
{
        gnome_settings_profile_start (NULL);

        set_devicepresence_handler (manager);

        add_devices (manager, GDK_SEAT_CAPABILITY_TABLET_STYLUS);

        gnome_settings_profile_end (NULL);

        manager->priv->start_idle_id = 0;

        return FALSE;
}

static void
on_bus_gotten (GObject		   *source_object,
	       GAsyncResult	   *res,
	       GsdWacomManager	   *manager)
{
	GDBusConnection	       *connection;
	GError		       *error = NULL;
	GsdWacomManagerPrivate *priv;

	priv = manager->priv;

	connection = g_bus_get_finish (res, &error);

	if (connection == NULL) {
		if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
			g_warning ("Couldn't get session bus: %s", error->message);
		g_error_free (error);
		return;
	}

	priv->dbus_connection = connection;
	priv->dbus_register_object_id = g_dbus_connection_register_object (connection,
									   GSD_WACOM_DBUS_PATH,
									   priv->introspection_data->interfaces[0],
									   &interface_vtable,
									   manager,
									   NULL,
									   &error);

	if (priv->dbus_register_object_id == 0) {
		g_warning ("Error registering object: %s", error->message);
		g_error_free (error);
		return;
	}

        manager->priv->name_id = g_bus_own_name_on_connection (connection,
                                                               GSD_WACOM_DBUS_NAME,
                                                               G_BUS_NAME_OWNER_FLAGS_NONE,
                                                               NULL,
                                                               NULL,
                                                               NULL,
                                                               NULL);
}

static void
register_manager (GsdWacomManager *manager)
{
        manager->priv->introspection_data = g_dbus_node_info_new_for_xml (introspection_xml, NULL);
        manager->priv->dbus_cancellable = g_cancellable_new ();
        g_assert (manager->priv->introspection_data != NULL);

        g_bus_get (G_BUS_TYPE_SESSION,
                   manager->priv->dbus_cancellable,
                   (GAsyncReadyCallback) on_bus_gotten,
                   manager);
}

static gchar *
get_machine_id (void)
{
        gchar *no_per_machine_file, *machine_id = NULL;
        gboolean per_machine;
        gsize len;

        no_per_machine_file = g_build_filename (g_get_user_config_dir (), "gnome-settings-daemon", "no-per-machine-config", NULL);
        per_machine = !g_file_test (no_per_machine_file, G_FILE_TEST_EXISTS);
        g_free (no_per_machine_file);

        if (!per_machine ||
            (!g_file_get_contents ("/etc/machine-id", &machine_id, &len, NULL) &&
             !g_file_get_contents ("/var/lib/dbus/machine-id", &machine_id, &len, NULL))) {
                return g_strdup ("00000000000000000000000000000000");
        }

        machine_id[len - 1] = '\0';
        return machine_id;
}

gboolean
gsd_wacom_manager_start (GsdWacomManager *manager,
                         GError         **error)
{
        gnome_settings_profile_start (NULL);

        register_manager (manager_object);

        manager->priv->machine_id = get_machine_id ();

        manager->priv->start_idle_id = g_idle_add ((GSourceFunc) gsd_wacom_manager_idle_cb, manager);
        g_source_set_name_by_id (manager->priv->start_idle_id, "[gnome-settings-daemon] gsd_wacom_manager_idle_cb");

        gnome_settings_profile_end (NULL);

        return TRUE;
}

void
gsd_wacom_manager_stop (GsdWacomManager *manager)
{
        GsdWacomManagerPrivate *p = manager->priv;

        g_debug ("Stopping wacom manager");

        g_clear_pointer (&manager->priv->machine_id, g_free);

        if (manager->priv->name_id != 0) {
                g_bus_unown_name (manager->priv->name_id);
                manager->priv->name_id = 0;
        }

        if (p->dbus_register_object_id) {
                g_dbus_connection_unregister_object (p->dbus_connection,
                                                     p->dbus_register_object_id);
                p->dbus_register_object_id = 0;
        }

        if (p->seat != NULL) {
                g_signal_handler_disconnect (p->seat, p->device_added_id);
                p->seat = NULL;
        }
}

static void
gsd_wacom_manager_finalize (GObject *object)
{
        GsdWacomManager *wacom_manager;

        g_return_if_fail (object != NULL);
        g_return_if_fail (GSD_IS_WACOM_MANAGER (object));

        wacom_manager = GSD_WACOM_MANAGER (object);

        g_return_if_fail (wacom_manager->priv != NULL);

        gsd_wacom_manager_stop (wacom_manager);

        if (wacom_manager->priv->start_idle_id != 0)
                g_source_remove (wacom_manager->priv->start_idle_id);

        g_clear_object (&wacom_manager->priv->shell_proxy);

#if HAVE_WACOM
        libwacom_database_destroy (wacom_manager->priv->wacom_db);
#endif

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

GsdWacomManager *
gsd_wacom_manager_new (void)
{
        if (manager_object != NULL) {
                g_object_ref (manager_object);
        } else {
                manager_object = g_object_new (GSD_TYPE_WACOM_MANAGER, NULL);
                g_object_add_weak_pointer (manager_object,
                                           (gpointer *) &manager_object);
        }

        return GSD_WACOM_MANAGER (manager_object);
}
