/* To compile:
 * gcc `pkg-config --cflags --libs gtk+-2.0 libwnck-1.0 xdamage` -DWNCK_I_KNOW_THIS_IS_UNSTABLE test-thumbnail.c -o test-thumbnail
 */

#include <libwnck/libwnck.h>
#include <gtk/gtk.h>
#include <gdk/gdkx.h>
#include <X11/extensions/Xdamage.h>
#include <X11/extensions/Xrender.h>
#include <X11/extensions/shape.h>
#include <X11/extensions/Xfixes.h>
#include <stdlib.h>

static int render_error_base, render_event_base;
static int damage_error_base, damage_event_base;
static int fixes_error_base, fixes_event_base;

typedef struct
{
  GtkWidget *drawing_area;
  GdkPixmap *buffer;

  GdkGC *gc;

  Picture buffer_picture;

  Picture root_picture;

  int damage_disabled_count;
  Damage damage;

  /* if None, the assumption is repaint everything */
  XserverRegion damage_region;

  guint timeout;
} ThumbData;

static void
paint_buffer (ThumbData *td)
{
  GdkScreen *screen;
  GdkColor color;
  int screen_w, screen_h;
  int buffer_w, buffer_h;
  XTransform transform;
  double scale_x, scale_y;

  screen = gtk_widget_get_screen (td->drawing_area);

  screen_w = gdk_screen_get_width (screen);
  screen_h = gdk_screen_get_height (screen);
  buffer_w = td->drawing_area->allocation.width;
  buffer_h = td->drawing_area->allocation.height;
  
  gdk_gc_set_subwindow (td->gc, GDK_INCLUDE_INFERIORS);
  
  color.red = 0xffff;
  color.green = 0;
  color.blue = 0;
  gdk_gc_set_rgb_fg_color (td->gc, &color);

  gdk_draw_rectangle (td->buffer, td->gc, TRUE,
                      0, 0,
                      buffer_w,
                      buffer_h);

  scale_x = (double) screen_w / (double) buffer_w;
  scale_y = (double) screen_h / (double) buffer_h;
  
  transform.matrix[0][0] = XDoubleToFixed (scale_x);
  transform.matrix[0][1] = XDoubleToFixed (0.0);
  transform.matrix[0][2] = XDoubleToFixed (0.0);
  transform.matrix[1][0] = XDoubleToFixed (0.0);
  transform.matrix[1][1] = XDoubleToFixed (scale_y);
  transform.matrix[1][2] = XDoubleToFixed (0.0);
  transform.matrix[2][0] = XDoubleToFixed (0.0);
  transform.matrix[2][1] = XDoubleToFixed (0.0);
  transform.matrix[2][2] = XDoubleToFixed (1.0);

  XRenderSetPictureTransform (gdk_display,
                              td->root_picture,
                              &transform);
  XRenderSetPictureFilter (gdk_display,
                           td->root_picture,
                           "good", /* could be: nearest, bilinear, fast, best */
                           NULL, 0);

  /* With X11R6.8, setting this clip hoses us, because instead of
   * making the clipped-out regions of the source "transparent",
   * they are black and overwrite the stuff that was in the
   * buffer before. keithp says this is not the intended behavior.
   */
#if 0
  /* damage_region == None will unset any clip */
  XFixesSetPictureClipRegion (gdk_display,
                              td->root_picture,
                              0, 0, td->damage_region);
#endif
      
  XRenderComposite (gdk_display, PictOpSrc,
                    td->root_picture, None,
                    td->buffer_picture,
                    0, 0, 0, 0, 0, 0,
                    buffer_w,
                    buffer_h);

  /* Change damage region to empty region (remember
   * damage_region of None means complete damage, not
   * no damage)
   */
  if (td->damage_region == None)
    td->damage_region = XFixesCreateRegion (gdk_display, NULL, 0);
  else
    XFixesSetRegion (gdk_display, td->damage_region, NULL, 0);
}


static void
disable_damage (ThumbData *td)
{
  td->damage_disabled_count += 1;

  if (td->damage != None)
    {
      XDamageDestroy (gdk_display, td->damage);
      td->damage = None;
    }

  if (td->damage_region)
    {
      XFixesDestroyRegion (gdk_display, td->damage_region);
      td->damage_region = None;
    }
}

static void
enable_damage (ThumbData *td)
{
  GdkScreen *screen;

  screen = gtk_widget_get_screen (td->drawing_area);
  
  /* it's OK to enable_damage() if it was never disabled */
  if (td->damage_disabled_count > 0)
    td->damage_disabled_count -= 1;

  if (td->damage_disabled_count == 0 &&
      td->damage == None &&
      GTK_WIDGET_REALIZED (td->drawing_area))
    {      
      td->damage = XDamageCreate (gdk_display,
                                  GDK_DRAWABLE_XID (gdk_screen_get_root_window (screen)),
                                  XDamageReportBoundingBox);

      
    }
}

static void
destroy_buffer (ThumbData *td)
{
  if (td->buffer)
    {
      g_object_unref (td->buffer);
      td->buffer = NULL;
    }

  if (td->gc)
    {
      g_object_unref (td->gc);
      td->gc = NULL;
    }

  if (td->buffer_picture != None)
    {
      XRenderFreePicture (gdk_display, td->buffer_picture);
      td->buffer_picture = None;
    }

  /* we have to repaint, so damage region is the whole thing */
  if (td->damage_region)
    {
      XFixesDestroyRegion (gdk_display, td->damage_region);
      td->damage_region = None;
    }
}

static void
update_buffer (ThumbData *td)
{
  int w, h;
  GdkScreen *screen;
      
  if (!GTK_WIDGET_REALIZED (td->drawing_area))
    return;
  
  screen = gtk_widget_get_screen (td->drawing_area);

  /* We create the per-screen resources here just
   * because we want to wait for realization
   */
  if (td->root_picture == None)
    {
      XRenderPictFormat *format;
      XRenderPictureAttributes pa;

      format = XRenderFindVisualFormat (gdk_display,
                                        DefaultVisual (gdk_display,
                                                       gdk_screen_get_number (screen)));

      pa.subwindow_mode = IncludeInferiors;
      td->root_picture =
        XRenderCreatePicture (gdk_display,
                              GDK_DRAWABLE_XID (gdk_screen_get_root_window (screen)),
                              format,
                              CPSubwindowMode,
                              &pa);
    }
  
  if (td->buffer)
    {
      gdk_drawable_get_size (GDK_DRAWABLE (td->buffer),
                             &w, &h);

      if (w != td->drawing_area->allocation.width ||
          h != td->drawing_area->allocation.height)
        destroy_buffer (td);
    }

  /* this also resets the damage region if it was nuked */
  enable_damage (td);
  
  if (td->buffer == NULL)
    {
      XRenderPictFormat *format;
        
      td->buffer = gdk_pixmap_new (td->drawing_area->window,
                                   td->drawing_area->allocation.width,
                                   td->drawing_area->allocation.height,
                                   -1);

      td->gc = gdk_gc_new (td->buffer);      

      format = XRenderFindVisualFormat (gdk_display,
                                        DefaultVisual (gdk_display,
                                                       gdk_screen_get_number (screen)));
      
      td->buffer_picture = XRenderCreatePicture (gdk_display,
                                                 GDK_DRAWABLE_XID (td->buffer),
                                                 format,
                                                 0, NULL);
      paint_buffer (td);
    }
}


static gboolean
timeout_handler (gpointer data)
{
  ThumbData *td = data;
  gtk_widget_queue_draw (td->drawing_area);
  td->timeout = 0;
  return FALSE;
}


static GdkFilterReturn
thumb_data_filter_func (GdkXEvent  *gdkxevent,
                        GdkEvent   *event,
                        gpointer    data)
{
  ThumbData *td = data;
  XEvent *xevent = gdkxevent;

  /***
   * NOTE: there's a problem which is that in response to damage
   * we repaint and create more damage; eventually in current
   * RENDER implementation, no damage is created since the pixels
   * are not really changed; however, we end up repainting a bunch
   * of times and it's far from efficient. So we should short-circuit
   * things ourselves and not thumbnail the thumbnailer.
   *
   * Of course in a true thumbnailing pager, we need to be a little
   * more sophisticated overall; we probably want to thumbnail each
   * window, but not the desktop background; also, we need to keep the
   * last thumbnail of currently-unmapped windows not on the current
   * workspace. So most likely we have a pixmap corresponding to each
   * workspace, and we thumbnail the windows on that workspace only
   * while said workspace is active.
   * 
   */

  
  if (xevent->type == (damage_event_base + XDamageNotify))
    {
      XDamageNotifyEvent *devent = (XDamageNotifyEvent*) xevent;
#if 0
      g_print ("Damage drawable 0x%lx level %d more %d area %d,%d %dx%d\n",
               (unsigned long) devent->drawable,
               devent->level,
               devent->more,
               devent->area.x,
               devent->area.y,
               devent->area.width,
               devent->area.height);
#endif
      if (td->damage != None &&
          devent->damage == td->damage)
        {
          XserverRegion our_region;
          XserverRegion new_damage;
          int x, y;

          new_damage = XFixesCreateRegion (gdk_display,
                                           NULL, 0);
          
          XDamageSubtract (gdk_display, td->damage, None,
                           new_damage);

          /* We want to ignore self-inflicted damage, or things get
           * really slow, though the recursion is sort of cool
           * looking.
           */
          our_region = XFixesCreateRegionFromWindow (gdk_display,
                                                     GDK_DRAWABLE_XID (td->drawing_area->window),
                                                     WindowRegionClip);

#if 1
          /* make region root-relative; this round trip
           * sucks and would be nice to eliminate.
           */
          gdk_window_get_origin (td->drawing_area->window,
                                 &x, &y);

          XFixesTranslateRegion (gdk_display,
                                 our_region, x, y);
#endif

#if 1
          XFixesSubtractRegion (gdk_display,
                                new_damage,
                                new_damage,
                                our_region);
#endif
          if (td->damage_region != None)
            {
              XFixesUnionRegion (gdk_display,
                                 td->damage_region,
                                 new_damage,
                                 td->damage_region);
              XFixesDestroyRegion (gdk_display,
                                   new_damage);
          
            }
          else
            {
              td->damage_region = new_damage;
            }
          
          XFixesDestroyRegion (gdk_display,
                               our_region);

          if (td->timeout == 0)
            {
              // 20ms is 50fps, which should be plenty fast enough
              td->timeout = g_timeout_add (20, timeout_handler, td);
            }
        }
    }
  
  return GDK_FILTER_CONTINUE;
}

static ThumbData*
thumb_data_new (void)
{
  ThumbData *td;

  td = g_new0 (ThumbData, 1);

  td->drawing_area = gtk_drawing_area_new ();
  td->timeout = 0;

  gdk_window_add_filter (NULL, thumb_data_filter_func, td);
  
  return td;
}


static void
thumb_data_free (ThumbData *td)
{
  destroy_buffer (td);

  if (td->root_picture != None)
    XRenderFreePicture (gdk_display, td->root_picture);

  disable_damage (td);
  
  gdk_window_remove_filter (NULL, thumb_data_filter_func, td);
  
  g_free (td);
}

static gboolean
expose_event_handler (GtkWidget      *thumb,
                      GdkEventExpose *expose,
                      void           *data)
{
    
  ThumbData *td = data;
  GdkPixmap *old_buffer;

  old_buffer = td->buffer;

  update_buffer (td);
  g_assert (td->buffer != NULL);

  if (old_buffer == td->buffer)
    paint_buffer (td);

  gdk_draw_drawable (td->drawing_area->window,
                     td->gc,
                     td->buffer,
                     expose->area.x, expose->area.y,
                     expose->area.x, expose->area.y,
                     expose->area.width, expose->area.height);
  
  return FALSE;
}

static void
create_thumbnail_window (WnckScreen *screen)
{
  GtkWidget *win;
  ThumbData *td;
  
  win = gtk_window_new (GTK_WINDOW_TOPLEVEL);

  gtk_window_stick (GTK_WINDOW (win));
#if 0
  wnck_gtk_window_set_dock_type (GTK_WINDOW (win));
#endif
  
  gtk_window_set_title (GTK_WINDOW (win), "Thumbnailer");

  /* very very random */
  gtk_window_move (GTK_WINDOW (win), 0, 0);
  
  /* quit on window close */
  g_signal_connect (G_OBJECT (win), "destroy",
                    G_CALLBACK (gtk_main_quit),
                    NULL);

  td = thumb_data_new ();
  
  g_signal_connect (G_OBJECT (td->drawing_area), "expose_event",
                    G_CALLBACK (expose_event_handler),
                    td);
  
  gtk_container_add (GTK_CONTAINER (win), td->drawing_area);

  g_object_set_data_full (G_OBJECT (td->drawing_area),
                          "thumb-data",
                          td, (GDestroyNotify) thumb_data_free);
  
  gtk_widget_show_all (win);
}

int
main (int argc, char **argv)
{
  WnckScreen *screen;
  
  gtk_init (&argc, &argv);

  render_error_base = 0;
  render_event_base = 0;

  if (!XRenderQueryExtension (gdk_display,
                              &render_event_base,
                              &render_error_base))
    {
      g_printerr ("No Render extension found\n");
      exit (1);
    }

  damage_error_base = 0;
  damage_event_base = 0;

  if (!XDamageQueryExtension (gdk_display,
                              &damage_event_base,
                              &damage_error_base))
    {
      g_printerr ("No Damage extension found\n");
      exit (1);
    }

  fixes_error_base = 0;
  fixes_event_base = 0;

  if (!XFixesQueryExtension (gdk_display,
                             &fixes_event_base,
                             &fixes_error_base))
    {
      g_printerr ("No Fixes extension found\n");
      exit (1);
    }
  
  screen = wnck_screen_get_default ();

  wnck_screen_force_update (screen);

  create_thumbnail_window (screen);
  
  gtk_main ();
  
  return 0;
}
