/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 *
 * Copyright (C) 2004-2006 William Jon McCann <mccann@jhu.edu>
 * Copyright (C) 2018      Sean Davis <bluesabre@xfce.org>
 *
 * 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 St, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 *
 * Authors: William Jon McCann <mccann@jhu.edu>
 *
 */

#include <config.h>

#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <gdk/gdk.h>
#include <gdk/gdkx.h>
#include <gtk/gtk.h>

#include "gs-window.h"
#include "gs-grab.h"
#include "gs-debug.h"

#define SLEEPTIMEOUT 100000
static void     gs_grab_class_init (GSGrabClass *klass);
static void     gs_grab_init       (GSGrab      *grab);
static void     gs_grab_finalize   (GObject     *object);

static gpointer grab_object = NULL;

struct GSGrabPrivate {
    GdkWindow  *grab_window;
    GdkDisplay *grab_display;
    guint       no_pointer_grab : 1;
    guint       hide_cursor : 1;

    GtkWidget  *invisible;
};

G_DEFINE_TYPE_WITH_PRIVATE (GSGrab, gs_grab, G_TYPE_OBJECT)

static const char *
grab_string (int status) {
    switch (status) {
        case GDK_GRAB_SUCCESS:
            return "GrabSuccess";
        case GDK_GRAB_ALREADY_GRABBED:
            return "AlreadyGrabbed";
        case GDK_GRAB_INVALID_TIME:
            return "GrabInvalidTime";
        case GDK_GRAB_NOT_VIEWABLE:
            return "GrabNotViewable";
        case GDK_GRAB_FROZEN:
            return "GrabFrozen";
        case GDK_GRAB_FAILED:
            return "GrabFailed";
        default:
        {
            static char foo[255];
            sprintf (foo, "unknown status: %d", status);
            return foo;
        }
    }
}

static void
prepare_window_grab_cb (GdkSeat   *seat,
                        GdkWindow *window,
                        gpointer   user_data) {
    gdk_window_show_unraised (window);
}

static int
gs_grab_get (GSGrab     *grab,
             GdkWindow  *window,
             GdkDisplay *display,
             gboolean    no_pointer_grab,
             gboolean    hide_cursor) {
    GdkGrabStatus        status;
    GdkSeat             *seat;
    GdkSeatCapabilities  caps;
    GdkCursor           *cursor;

    g_return_val_if_fail (window != NULL, FALSE);
    g_return_val_if_fail (display != NULL, FALSE);

    cursor = gdk_cursor_new_for_display (display, GDK_BLANK_CURSOR);

    gs_debug ("Grabbing devices for window=%X", (guint32) GDK_WINDOW_XID (window));

    seat = gdk_display_get_default_seat (display);
    if (!no_pointer_grab)
        caps = GDK_SEAT_CAPABILITY_ALL;
    else
        caps = GDK_SEAT_CAPABILITY_KEYBOARD;

    status = gdk_seat_grab (seat, window,
                            caps, TRUE,
                            (hide_cursor ? cursor : NULL),
                            NULL,
                            prepare_window_grab_cb,
                            NULL);

    /* make it release grabbed pointer if requested and if any;
       time between grabbing and ungrabbing is minimal as grab was already
       completed once */
    if (status == GDK_GRAB_SUCCESS && no_pointer_grab &&
            gdk_display_device_is_grabbed (display, gdk_seat_get_pointer (seat))) {
        gs_grab_release (grab, FALSE);
        gs_debug ("Regrabbing keyboard");
        status = gdk_seat_grab (seat, window,
                                caps, TRUE,
                                (hide_cursor ? cursor : NULL),
                                NULL, NULL, NULL);
    }

    if (status == GDK_GRAB_SUCCESS) {
        if (grab->priv->grab_window != NULL) {
            g_object_remove_weak_pointer (G_OBJECT (grab->priv->grab_window),
                                          (gpointer *) &grab->priv->grab_window);
        }
        grab->priv->grab_window = window;

        g_object_add_weak_pointer (G_OBJECT (grab->priv->grab_window),
                                   (gpointer *) &grab->priv->grab_window);

        grab->priv->grab_display = display;
        grab->priv->no_pointer_grab = no_pointer_grab;
        grab->priv->hide_cursor = hide_cursor;
    }

    g_object_unref (G_OBJECT (cursor));

    return status;
}

void
gs_grab_reset (GSGrab *grab) {
    if (grab->priv->grab_window != NULL) {
        g_object_remove_weak_pointer (G_OBJECT (grab->priv->grab_window),
                                      (gpointer *) &grab->priv->grab_window);
    }
    grab->priv->grab_window = NULL;
    grab->priv->grab_display = NULL;
}

void
gs_grab_release (GSGrab *grab, gboolean flush) {
    GdkDisplay *display;
    GdkSeat    *seat;

    display = gdk_display_get_default ();
    seat = gdk_display_get_default_seat (display);

    gs_debug ("Ungrabbing devices");

    gdk_seat_ungrab (seat);

    gs_grab_reset (grab);

    /* FIXME: decide when this is good and when not */
    if (flush) {
        /* FIXME: is it right to enable this? */
        gdk_display_sync (display);
        gdk_display_flush (display);
    }
}

static gboolean
gs_grab_move (GSGrab     *grab,
              GdkWindow  *window,
              GdkDisplay *display,
              gboolean    no_pointer_grab,
              gboolean    hide_cursor) {
    int         result;
    GdkWindow  *old_window;
    GdkDisplay *old_display;
    gboolean    old_hide_cursor;

    if (grab->priv->grab_window == window &&
        grab->priv->no_pointer_grab == no_pointer_grab) {
        gs_debug ("Window %X is already grabbed, skipping",
                  (guint32) GDK_WINDOW_XID (grab->priv->grab_window));
        return TRUE;
    }

    if (grab->priv->grab_window != NULL) {
        gs_debug ("Moving devices grab from %X to %X",
                  (guint32) GDK_WINDOW_XID (grab->priv->grab_window),
                  (guint32) GDK_WINDOW_XID (window));
    } else {
        gs_debug ("Getting devices grab on %X",
                  (guint32) GDK_WINDOW_XID (window));
    }

    gs_debug ("*** Doing X server grab");
    gdk_x11_display_grab (display);

    old_window = grab->priv->grab_window;
    old_display = grab->priv->grab_display;
    old_hide_cursor = grab->priv->hide_cursor;

    if (old_window) {
        gs_grab_release (grab, FALSE);
    }

    result = gs_grab_get (grab, window, display,
                          no_pointer_grab, hide_cursor);

    if (result != GDK_GRAB_SUCCESS) {
        g_usleep (SLEEPTIMEOUT);
        result = gs_grab_get (grab, window, display,
                              no_pointer_grab, hide_cursor);
    }

    if ((result != GDK_GRAB_SUCCESS) && old_window) {
        int old_result;

        gs_debug ("Could not grab devices for new window. Resuming previous grab.");
        old_result = gs_grab_get (grab, old_window, old_display,
                                  no_pointer_grab, old_hide_cursor);
        if (old_result != GDK_GRAB_SUCCESS)
            gs_debug ("Could not grab devices for old window");
    }

    gs_debug ("*** Releasing X server grab");
    gdk_x11_display_ungrab (display);
    gdk_display_flush (display);

    return (result == GDK_GRAB_SUCCESS);
}

static void
gs_grab_nuke_focus (GdkDisplay *display) {
    Window focus = 0;
    int    rev = 0;

    gs_debug ("Nuking focus");

    gdk_x11_display_error_trap_push (display);

    XGetInputFocus (GDK_DISPLAY_XDISPLAY (display), &focus, &rev);
    XSetInputFocus (GDK_DISPLAY_XDISPLAY (display), None,
                    RevertToNone, CurrentTime);

    gdk_x11_display_error_trap_pop_ignored (display);
}

static void
gs_grab_restore_focus (GdkDisplay *display, GdkWindow *window) {
    gs_debug ("Restoring focus");

    gdk_x11_display_error_trap_push (display);

    XSetInputFocus (GDK_DISPLAY_XDISPLAY (display), GDK_WINDOW_XID (window),
                    RevertToParent, CurrentTime);

    gdk_x11_display_error_trap_pop_ignored (display);
}

gboolean
gs_grab_grab_window (GSGrab     *grab,
                     GdkWindow  *window,
                     GdkDisplay *display,
                     gboolean    no_pointer_grab,
                     gboolean    hide_cursor) {
    gint        status = FALSE;
    gboolean    nuked = FALSE;
    int         i;
    int         retries = 12;

    for (i = 0; i < retries; i++) {
        status = gs_grab_get (grab, window, display,
                              no_pointer_grab, hide_cursor);
        if (status == GDK_GRAB_SUCCESS) {
            break;
        } else if (i == (int) (retries / 2)) {
            /* try nuking focus in the middle */
            gs_grab_nuke_focus (display);
            nuked = TRUE;
        }

        /* else, wait a second and try to grab again */
        g_usleep (SLEEPTIMEOUT);
    }

    /* Something else has grab. Begin aggressive grab cycle. */
    while (status == GDK_GRAB_ALREADY_GRABBED) {
        for (i = 0; i < retries; i++) {
            status = gs_grab_get (grab, window, display,
                                no_pointer_grab, hide_cursor);
            if (status == GDK_GRAB_SUCCESS) {
                break;
            } else if (i == (int) (retries / 2)) {
                /* try nuking focus in the middle */
                gs_grab_nuke_focus (display);
                nuked = TRUE;
            }

            /* else, wait a second and try to grab again */
            g_usleep (SLEEPTIMEOUT);
        }
    }

    if (status != GDK_GRAB_SUCCESS) {
        gs_debug ("Couldn't grab devices!  (%s)",
                  grab_string (status));

        /* do not blank without a devices grab */
        return FALSE;
    }

    if (nuked) {
        gs_grab_restore_focus (display, window);
    }

    /* grab is good, go ahead and blank  */
    return TRUE;
}

/* this is used to grab devices to the root */
gboolean
gs_grab_grab_root (GSGrab   *grab,
                   gboolean  no_pointer_grab,
                   gboolean  hide_cursor) {
    GdkDisplay *display;
    GdkWindow  *root;
    GdkScreen  *screen;
    GdkDevice  *device;
    gboolean    res;

    gs_debug ("Grabbing the root window");

    display = gdk_display_get_default ();
    device = gdk_seat_get_pointer (gdk_display_get_default_seat (display));
    gdk_device_get_position (device, &screen, NULL, NULL);
    root = gdk_screen_get_root_window (screen);

    res = gs_grab_grab_window (grab, root, display,
                               no_pointer_grab, hide_cursor);

    return res;
}

/* this is similar to gs_grab_grab_window but doesn't fail */
void
gs_grab_move_to_window (GSGrab     *grab,
                        GdkWindow  *window,
                        GdkDisplay *display,
                        gboolean    no_pointer_grab,
                        gboolean    hide_cursor) {
    gboolean result = FALSE;

    g_return_if_fail (GS_IS_GRAB (grab));

    while (!result) {
        result = gs_grab_move (grab, window, display,
                               no_pointer_grab, hide_cursor);
        gdk_display_flush (display);
    }
}

static void
gs_grab_class_init (GSGrabClass *klass) {
    GObjectClass *object_class = G_OBJECT_CLASS (klass);

    object_class->finalize = gs_grab_finalize;
}

static void
gs_grab_init (GSGrab *grab) {
    grab->priv = gs_grab_get_instance_private (grab);

    grab->priv->no_pointer_grab = FALSE;
    grab->priv->hide_cursor = FALSE;
    grab->priv->invisible = gtk_invisible_new ();
    gtk_widget_show (grab->priv->invisible);
}

static void
gs_grab_finalize (GObject *object) {
    GSGrab *grab;

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

    grab = GS_GRAB (object);

    g_return_if_fail (grab->priv != NULL);

    gtk_widget_destroy (grab->priv->invisible);

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

GSGrab *
gs_grab_new (void) {
    if (grab_object) {
        g_object_ref (grab_object);
    } else {
        grab_object = g_object_new (GS_TYPE_GRAB, NULL);
        g_object_add_weak_pointer (grab_object,
                                   (gpointer *) &grab_object);
    }

    return GS_GRAB (grab_object);
}
