/* gsd-locate-pointer.c
 *
 * Copyright (C) 2008 Carlos Garnacho  <carlos@imendio.com>
 *
 * 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 <gtk/gtk.h>
#include "gsd-timeline.h"
#include "gsd-locate-pointer.h"

#include <gdk/gdkkeysyms.h>
#include <gdk/gdkx.h>
#include <X11/keysym.h>

#define ANIMATION_LENGTH 750
#define WINDOW_SIZE 101
#define N_CIRCLES 4

/* All circles are supposed to be moving when progress
 * reaches 0.5, and each of them are supposed to long
 * for half of the progress, hence the need of 0.5 to
 * get the circles interval, and the multiplication
 * by 2 to know a circle progress */
#define CIRCLES_PROGRESS_INTERVAL (0.5 / N_CIRCLES)
#define CIRCLE_PROGRESS(p) (MIN (1., ((gdouble) (p) * 2.)))

typedef struct GsdLocatePointerData GsdLocatePointerData;

struct GsdLocatePointerData
{
  GsdTimeline *timeline;
  GtkWidget *widget; 
  GdkWindow *window;

  gdouble progress;
};

static GsdLocatePointerData *data = NULL;

static void
locate_pointer_paint (GsdLocatePointerData *data,
                      cairo_t              *cr,
                      gboolean              composite)
{
  GdkRGBA color;
  gdouble progress, circle_progress;
  gint width, height, i;
  GtkStyleContext *context;

  progress = data->progress;

  width = gdk_window_get_width (data->window);
  height = gdk_window_get_height (data->window);
  context = gtk_widget_get_style_context (data->widget);
  gtk_style_context_get_background_color (context, GTK_STATE_FLAG_SELECTED, &color);

  cairo_set_source_rgba (cr, 1., 1., 1., 0.);
  cairo_set_operator (cr, CAIRO_OPERATOR_SOURCE);
  cairo_paint (cr);

  for (i = 0; i <= N_CIRCLES; i++)
    {
      if (progress < 0.)
        break;

      circle_progress = MIN (1., (progress * 2));
      progress -= CIRCLES_PROGRESS_INTERVAL;

      if (circle_progress >= 1.)
        continue;

      if (composite)
        {
          cairo_set_source_rgba (cr,
                                 color.red,
                                 color.green,
                                 color.blue,
                                 1 - circle_progress);
          cairo_arc (cr,
                     width / 2,
                     height / 2,
                     circle_progress * width / 2,
                     0, 2 * G_PI);

          cairo_fill (cr);
        }
      else
        {
          cairo_set_source_rgb (cr, 0., 0., 0.);
          cairo_set_line_width (cr, 3.);
          cairo_arc (cr,
                     width / 2,
                     height / 2,
                     circle_progress * width / 2,
                     0, 2 * G_PI);
          cairo_stroke (cr);

          cairo_set_source_rgb (cr, 1., 1., 1.);
          cairo_set_line_width (cr, 1.);
          cairo_arc (cr,
                     width / 2,
                     height / 2,
                     circle_progress * width / 2,
                     0, 2 * G_PI);
          cairo_stroke (cr);
        }
    }
}

static gboolean
locate_pointer_draw (GtkWidget      *widget,
                     cairo_t        *cr,
                     gpointer        user_data)
{
  GsdLocatePointerData *data = (GsdLocatePointerData *) user_data;

  if (gtk_cairo_should_draw_window (cr, data->window))
    locate_pointer_paint (data, cr, gtk_widget_is_composited (data->widget));

  return TRUE;
}

static void
update_shape (GsdLocatePointerData *data)
{
  cairo_t *cr;
  cairo_region_t *region;
  cairo_surface_t *mask;

  mask = cairo_image_surface_create (CAIRO_FORMAT_A1, WINDOW_SIZE, WINDOW_SIZE);
  cr = cairo_create (mask);

  locate_pointer_paint (data, cr, FALSE);

  region = gdk_cairo_region_create_from_surface (mask);
  gdk_window_shape_combine_region (data->window, region, 0, 0);

  cairo_region_destroy (region);
  cairo_destroy (cr);
  cairo_surface_destroy (mask);
}

static void
timeline_frame_cb (GsdTimeline *timeline,
                   gdouble      progress,
                   gpointer     user_data)
{
  GsdLocatePointerData *data = (GsdLocatePointerData *) user_data;
  GdkScreen *screen;
  GdkDeviceManager *device_manager;
  gint cursor_x, cursor_y;

  if (gtk_widget_is_composited (data->widget))
    {
      gdk_window_invalidate_rect (data->window, NULL, FALSE);
      data->progress = progress;
    }
  else if (progress >= data->progress + CIRCLES_PROGRESS_INTERVAL)
    {
      /* only invalidate window each circle interval */
      update_shape (data);
      gdk_window_invalidate_rect (data->window, NULL, FALSE);
      data->progress += CIRCLES_PROGRESS_INTERVAL;
    }

  screen = gdk_window_get_screen (data->window);
  device_manager = gdk_display_get_device_manager (gdk_window_get_display (data->window));
  gdk_window_get_device_position (gdk_screen_get_root_window (screen),
                                  gdk_device_manager_get_client_pointer (device_manager),
                                  &cursor_x, &cursor_y, NULL);
  gdk_window_move (data->window,
                   cursor_x - WINDOW_SIZE / 2,
                   cursor_y - WINDOW_SIZE / 2);
}

static void
set_transparent_shape (GdkWindow *window)
{
  cairo_region_t *region;

  region = cairo_region_create ();
  gdk_window_shape_combine_region (data->window, region, 0, 0);
  cairo_region_destroy (region);
}

static void
unset_transparent_shape (GdkWindow *window)
{
  gdk_window_shape_combine_region (data->window, NULL, 0, 0);
}

static void
composited_changed (GtkWidget            *widget,
                    GsdLocatePointerData *data)
{
  if (!gtk_widget_is_composited (widget))
    set_transparent_shape (data->window);
  else
    unset_transparent_shape (data->window);
}

static void
timeline_finished_cb (GsdTimeline *timeline,
                      gpointer     user_data)
{
  GsdLocatePointerData *data = (GsdLocatePointerData *) user_data;

  /* set transparent shape and hide window */
  if (!gtk_widget_is_composited (data->widget))
    set_transparent_shape (data->window);

  gtk_widget_hide (data->widget);
  gdk_window_hide (data->window);
}

static void
create_window (GsdLocatePointerData *data,
               GdkScreen            *screen)
{
  GdkVisual *visual;
  GdkWindowAttr attributes;
  gint attributes_mask;

  visual = gdk_screen_get_rgba_visual (screen);

  if (visual == NULL)
    visual = gdk_screen_get_system_visual (screen);

  attributes_mask = GDK_WA_X | GDK_WA_Y;

  if (visual != NULL)
    attributes_mask = attributes_mask | GDK_WA_VISUAL;

  attributes.window_type = GDK_WINDOW_TEMP;
  attributes.wclass = GDK_INPUT_OUTPUT;
  attributes.visual = visual;
  attributes.event_mask = GDK_VISIBILITY_NOTIFY_MASK | GDK_EXPOSURE_MASK;
  attributes.width = 1;
  attributes.height = 1;

  data->window = gdk_window_new (gdk_screen_get_root_window (screen),
                                 &attributes,
                                 attributes_mask);

  gdk_window_set_user_data (data->window, data->widget);
}

static GsdLocatePointerData *
gsd_locate_pointer_data_new (GdkScreen *screen)
{
  GsdLocatePointerData *data;

  data = g_new0 (GsdLocatePointerData, 1);

  /* this widget will never be shown, it's
   * mainly used to get signals/events from
   */
  data->widget = gtk_invisible_new ();
  gtk_widget_realize (data->widget);

  g_signal_connect (G_OBJECT (data->widget), "draw",
                    G_CALLBACK (locate_pointer_draw),
                    data);

  data->timeline = gsd_timeline_new (ANIMATION_LENGTH);
  g_signal_connect (data->timeline, "frame",
                    G_CALLBACK (timeline_frame_cb), data);
  g_signal_connect (data->timeline, "finished",
                    G_CALLBACK (timeline_finished_cb), data);

  create_window (data, screen);

  return data;
}

static void
move_locate_pointer_window (GsdLocatePointerData *data,
                            GdkScreen            *screen)
{
  GdkDeviceManager *device_manager;
  cairo_region_t *region;
  gint cursor_x, cursor_y;

  device_manager = gdk_display_get_device_manager (gdk_window_get_display (data->window));
  gdk_window_get_device_position (gdk_screen_get_root_window (screen),
                                  gdk_device_manager_get_client_pointer (device_manager),
                                  &cursor_x, &cursor_y, NULL);

  gdk_window_move_resize (data->window,
                          cursor_x - WINDOW_SIZE / 2,
                          cursor_y - WINDOW_SIZE / 2,
                          WINDOW_SIZE, WINDOW_SIZE);

  /* allow events to happen through the window */
  region = cairo_region_create ();
  gdk_window_input_shape_combine_region (data->window, region, 0, 0);
  cairo_region_destroy (region);
}

void
gsd_locate_pointer (GdkScreen *screen)
{
  if (!data)
    data = gsd_locate_pointer_data_new (screen);

  gsd_timeline_pause (data->timeline);
  gsd_timeline_rewind (data->timeline);

  /* Create again the window if it is not for the current screen */
  if (gdk_screen_get_number (screen) != gdk_screen_get_number (gdk_window_get_screen (data->window)))
    {
      gdk_window_set_user_data (data->window, NULL);
      gdk_window_destroy (data->window);

      create_window (data, screen);
    }

  data->progress = 0.;

  g_signal_connect (data->widget, "composited-changed",
                    G_CALLBACK (composited_changed), data);

  move_locate_pointer_window (data, screen);
  composited_changed (data->widget, data);
  gdk_window_show (data->window);
  gtk_widget_show (data->widget);

  gsd_timeline_start (data->timeline);
}


#define KEYBOARD_GROUP_SHIFT 13
#define KEYBOARD_GROUP_MASK ((1 << 13) | (1 << 14))

/* Owen magic */
static GdkFilterReturn
filter (GdkXEvent *xevent,
        GdkEvent  *event,
        gpointer   data)
{
  XEvent *xev = (XEvent *) xevent;
  guint keyval;
  gint group;

  GdkScreen *screen = (GdkScreen *)data;

  if (xev->type == ButtonPress)
    {
      XAllowEvents (xev->xbutton.display,
                    ReplayPointer,
                    xev->xbutton.time);
      XUngrabButton (xev->xbutton.display,
                     AnyButton,
                     AnyModifier,
                     xev->xbutton.window);
      XUngrabKeyboard (xev->xbutton.display,
                       xev->xbutton.time);
    }

  if (xev->type == KeyPress || xev->type == KeyRelease)
    {
      /* get the keysym */
      group = (xev->xkey.state & KEYBOARD_GROUP_MASK) >> KEYBOARD_GROUP_SHIFT;
      gdk_keymap_translate_keyboard_state (gdk_keymap_get_default (),
                                           xev->xkey.keycode,
                                           xev->xkey.state,
                                           group,
                                           &keyval,
                                           NULL, NULL, NULL);
      if (keyval == GDK_KEY_Control_L || keyval == GDK_KEY_Control_R)
        {
          if (xev->type == KeyPress)
            {
              XAllowEvents (xev->xkey.display,
                            SyncKeyboard,
                            xev->xkey.time);
              XGrabButton (xev->xkey.display,
                           AnyButton,
                           AnyModifier,
                           xev->xkey.window,
                           False,
                           ButtonPressMask,
                           GrabModeSync,
                           GrabModeAsync,
                           None,
                           None);
            }
          else
            {
              XUngrabButton (xev->xkey.display,
                             AnyButton,
                             AnyModifier,
                             xev->xkey.window);
              XAllowEvents (xev->xkey.display,
                            AsyncKeyboard,
                            xev->xkey.time);
              gsd_locate_pointer (screen);
            }
        }
      else
        {
          XAllowEvents (xev->xkey.display,
                        ReplayKeyboard,
                        xev->xkey.time);
          XUngrabButton (xev->xkey.display,
                         AnyButton,
                         AnyModifier,
                         xev->xkey.window);
          XUngrabKeyboard (xev->xkey.display,
                           xev->xkey.time);
        }
    }

  return GDK_FILTER_CONTINUE;
}

static void
set_locate_pointer (void)
{
  GdkKeymapKey *keys;
  GdkDisplay *display;
  GdkScreen *screen;
  Window xroot;
  int n_keys;
  gboolean has_entries;
  static const guint keyvals[] = { GDK_KEY_Control_L, GDK_KEY_Control_R };
  unsigned i, j;

  display = gdk_display_get_default ();
  screen = gdk_screen_get_default ();
  xroot = gdk_x11_window_get_xid (gdk_screen_get_root_window (screen));

  for (j = 0 ; j < G_N_ELEMENTS (keyvals) ; j++)
    {
      has_entries = gdk_keymap_get_entries_for_keyval (gdk_keymap_get_default (),
                                                       keyvals[j],
                                                       &keys,
                                                       &n_keys);
      if (has_entries)
        {
          for (i = 0; i < n_keys; i++)
            {
              gdk_x11_display_error_trap_push (display);

              XGrabKey (GDK_DISPLAY_XDISPLAY (display),
                        keys[i].keycode,
                        0,
                        xroot,
                        False,
                        GrabModeAsync,
                        GrabModeSync);
              XGrabKey (GDK_DISPLAY_XDISPLAY (display),
                        keys[i].keycode,
                        LockMask,
                        xroot,
                        False,
                        GrabModeAsync,
                        GrabModeSync);
              XGrabKey (GDK_DISPLAY_XDISPLAY (display),
                        keys[i].keycode,
                        Mod2Mask,
                        xroot,
                        False,
                        GrabModeAsync,
                        GrabModeSync);
              XGrabKey (GDK_DISPLAY_XDISPLAY (display),
                        keys[i].keycode,
                        Mod4Mask,
                        xroot,
                        False,
                        GrabModeAsync,
                        GrabModeSync);

              gdk_x11_display_error_trap_pop_ignored (display);
            }

          g_free (keys);

          gdk_window_add_filter (gdk_screen_get_root_window (screen),
                                 filter,
                                 screen);
        }
    }
}


int
main (int argc, char *argv[])
{
  gdk_disable_multidevice ();

  gtk_init (&argc, &argv);

  set_locate_pointer ();

  gtk_main ();

  return 0;
}

