/*
 * Copyright (c) 2008-2009  Christian Hammond
 * Copyright (c) 2008-2009  David Trowbridge
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "config.h"
#include <glib/gi18n-lib.h>

#include "window.h"
#include "object-tree.h"

#include "gtkstack.h"
#include "gtkmain.h"
#include "gtkinvisible.h"

typedef struct
{
  gint x;
  gint y;
  gboolean found;
  gboolean first;
  GtkWidget *res_widget;
} FindWidgetData;

static void
find_widget (GtkWidget      *widget,
             FindWidgetData *data)
{
  GtkAllocation new_allocation;
  gint x_offset = 0;
  gint y_offset = 0;

  gtk_widget_get_allocation (widget, &new_allocation);

  if (data->found || !gtk_widget_get_mapped (widget))
    return;

  /* Note that in the following code, we only count the
   * position as being inside a WINDOW widget if it is inside
   * widget->window; points that are outside of widget->window
   * but within the allocation are not counted. This is consistent
   * with the way we highlight drag targets.
   */
  if (gtk_widget_get_has_window (widget))
    {
      new_allocation.x = 0;
      new_allocation.y = 0;
    }

  if (gtk_widget_get_parent (widget) && !data->first)
    {
      GdkWindow *window;

      window = gtk_widget_get_window (widget);
      while (window != gtk_widget_get_window (gtk_widget_get_parent (widget)))
        {
          gint tx, ty, twidth, theight;

          if (window == NULL)
            return;

          twidth = gdk_window_get_width (window);
          theight = gdk_window_get_height (window);

          if (new_allocation.x < 0)
            {
              new_allocation.width += new_allocation.x;
              new_allocation.x = 0;
            }
          if (new_allocation.y < 0)
            {
              new_allocation.height += new_allocation.y;
              new_allocation.y = 0;
            }
          if (new_allocation.x + new_allocation.width > twidth)
            new_allocation.width = twidth - new_allocation.x;
          if (new_allocation.y + new_allocation.height > theight)
            new_allocation.height = theight - new_allocation.y;

          gdk_window_get_position (window, &tx, &ty);
          new_allocation.x += tx;
          x_offset += tx;
          new_allocation.y += ty;
          y_offset += ty;

          window = gdk_window_get_parent (window);
        }
    }

  if ((data->x >= new_allocation.x) && (data->y >= new_allocation.y) &&
      (data->x < new_allocation.x + new_allocation.width) &&
      (data->y < new_allocation.y + new_allocation.height))
    {
      /* First, check if the drag is in a valid drop site in
       * one of our children 
       */
      if (GTK_IS_CONTAINER (widget))
        {
          FindWidgetData new_data = *data;

          new_data.x -= x_offset;
          new_data.y -= y_offset;
          new_data.found = FALSE;
          new_data.first = FALSE;

          gtk_container_forall (GTK_CONTAINER (widget),
                                (GtkCallback)find_widget,
                                &new_data);

          data->found = new_data.found;
          if (data->found)
            data->res_widget = new_data.res_widget;
        }

      /* If not, and this widget is registered as a drop site, check to
       * emit "drag_motion" to check if we are actually in
       * a drop site.
       */
      if (!data->found)
        {
          data->found = TRUE;
          data->res_widget = widget;
        }
    }
}

static GtkWidget *
find_widget_at_pointer (GdkDevice *device)
{
  GtkWidget *widget = NULL;
  GdkWindow *pointer_window;
  gint x, y;
  FindWidgetData data;

  pointer_window = gdk_device_get_window_at_position (device, NULL, NULL);

  if (pointer_window)
    {
      gpointer widget_ptr;

      gdk_window_get_user_data (pointer_window, &widget_ptr);
      widget = widget_ptr;
    }

  if (widget)
    {
      gdk_window_get_device_position (gtk_widget_get_window (widget),
                                      device, &x, &y, NULL);

      data.x = x;
      data.y = y;
      data.found = FALSE;
      data.first = TRUE;

      find_widget (widget, &data);
      if (data.found)
        return data.res_widget;

      return widget;
    }

  return NULL;
}

static gboolean draw_flash (GtkWidget          *widget,
                            cairo_t            *cr,
                            GtkInspectorWindow *iw);

static void
clear_flash (GtkInspectorWindow *iw)
{
  if (iw->flash_widget)
    {
      gtk_widget_queue_draw (iw->flash_widget);
      g_signal_handlers_disconnect_by_func (iw->flash_widget, draw_flash, iw);
      g_signal_handlers_disconnect_by_func (iw->flash_widget, clear_flash, iw);
      iw->flash_widget = NULL;
    }
}

static void
start_flash (GtkInspectorWindow *iw,
             GtkWidget          *widget)
{
  clear_flash (iw);

  iw->flash_count = 1;
  iw->flash_widget = widget;
  g_signal_connect_after (widget, "draw", G_CALLBACK (draw_flash), iw);
  g_signal_connect_swapped (widget, "unmap", G_CALLBACK (clear_flash), iw);
  gtk_widget_queue_draw (widget);
}

static void
select_widget (GtkInspectorWindow *iw,
               GtkWidget          *widget)
{
  GtkInspectorObjectTree *wt = GTK_INSPECTOR_OBJECT_TREE (iw->object_tree);

  iw->selected_widget = widget;

  if (!gtk_inspector_object_tree_select_object (wt, G_OBJECT (widget)))
    {
      gtk_inspector_object_tree_scan (wt, gtk_widget_get_toplevel (widget));
      gtk_inspector_object_tree_select_object (wt, G_OBJECT (widget));
    }
}

static void
on_inspect_widget (GtkWidget          *button,
                   GdkEvent           *event,
                   GtkInspectorWindow *iw)
{
  GtkWidget *widget;

  gdk_window_raise (gtk_widget_get_window (GTK_WIDGET (iw)));

  clear_flash (iw);

  widget = find_widget_at_pointer (gdk_event_get_device (event));

  if (widget)
    select_widget (iw, widget);
}

static void
on_highlight_widget (GtkWidget          *button,
                     GdkEvent           *event,
                     GtkInspectorWindow *iw)
{
  GtkWidget *widget;

  widget = find_widget_at_pointer (gdk_event_get_device (event));

  if (widget == NULL)
    {
      /* This window isn't in-process. Ignore it. */
      return;
    }

  if (gtk_widget_get_toplevel (widget) == GTK_WIDGET (iw))
    {
      /* Don't hilight things in the inspector window */
      return;
    }

  if (iw->flash_widget == widget)
    {
      /* Already selected */
      return;
    }

  clear_flash (iw);
  start_flash (iw, widget);
}

static void
deemphasize_window (GtkWidget *window)
{
  GdkScreen *screen;

  screen = gtk_widget_get_screen (window);
  if (gdk_screen_is_composited (screen) &&
      gtk_widget_get_visual (window) == gdk_screen_get_rgba_visual (screen))
    {
      cairo_rectangle_int_t rect;
      cairo_region_t *region;

      gtk_widget_set_opacity (window, 0.3);
      rect.x = rect.y = rect.width = rect.height = 0;
      region = cairo_region_create_rectangle (&rect);
      gtk_widget_input_shape_combine_region (window, region);
      cairo_region_destroy (region);
    }
  else
    gdk_window_lower (gtk_widget_get_window (window));
}

static void
reemphasize_window (GtkWidget *window)
{
  GdkScreen *screen;

  screen = gtk_widget_get_screen (window);
  if (gdk_screen_is_composited (screen) &&
      gtk_widget_get_visual (window) == gdk_screen_get_rgba_visual (screen))
    {
      gtk_widget_set_opacity (window, 1.0);
      gtk_widget_input_shape_combine_region (window, NULL);
    }
  else
    gdk_window_raise (gtk_widget_get_window (window));
}

static gboolean
property_query_event (GtkWidget *widget,
                      GdkEvent  *event,
                      gpointer   data)
{
  GtkInspectorWindow *iw = (GtkInspectorWindow *)data;
  GdkDevice *device;

  if (event->type == GDK_BUTTON_RELEASE)
    {
      device = gdk_event_get_device (event);

      g_signal_handlers_disconnect_by_func (widget, property_query_event, data);
      gtk_grab_remove (widget);
      if (iw->grabbed)
        gdk_device_ungrab (device, GDK_CURRENT_TIME);
      reemphasize_window (GTK_WIDGET (iw));

      on_inspect_widget (widget, event, data);
    }
  else if (event->type == GDK_MOTION_NOTIFY)
    {
      on_highlight_widget (widget, event, data);
    }
  else if (event->type == GDK_KEY_PRESS)
    {
      GdkEventKey *ke = (GdkEventKey*)event;

      if (ke->keyval == GDK_KEY_Escape)
        {
          g_signal_handlers_disconnect_by_func (widget, property_query_event, data);
          gtk_grab_remove (widget);
          device = gdk_device_get_associated_device (gdk_event_get_device (event));
          if (iw->grabbed)
            gdk_device_ungrab (device, GDK_CURRENT_TIME);
          reemphasize_window (GTK_WIDGET (iw));

          clear_flash (iw);
        }
    }

  return TRUE;
}

void
gtk_inspector_on_inspect (GtkWidget          *button,
                          GtkInspectorWindow *iw)
{
  GdkDisplay *display;
  GdkDevice *device;
  GdkCursor *cursor;
  GdkGrabStatus status;

  if (!iw->invisible)
    {
      iw->invisible = gtk_invisible_new_for_screen (gdk_screen_get_default ());
      gtk_widget_add_events (iw->invisible,
                             GDK_POINTER_MOTION_MASK |
                             GDK_BUTTON_PRESS_MASK |
                             GDK_BUTTON_RELEASE_MASK |
                             GDK_KEY_PRESS_MASK |
                             GDK_KEY_RELEASE_MASK);
      gtk_widget_realize (iw->invisible);
      gtk_widget_show (iw->invisible);
    }

  display = gdk_display_get_default ();
  cursor = gdk_cursor_new_from_name (display, "crosshair");
  device = gdk_device_manager_get_client_pointer (gdk_display_get_device_manager (display));
  status = gdk_device_grab (device,
                            gtk_widget_get_window (iw->invisible),
                            GDK_OWNERSHIP_NONE, TRUE,
                            GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK,
                            cursor, GDK_CURRENT_TIME);
  g_object_unref (cursor);
  iw->grabbed = status == GDK_GRAB_SUCCESS;

  g_signal_connect (iw->invisible, "event", G_CALLBACK (property_query_event), iw);

  gtk_grab_add (GTK_WIDGET (iw->invisible));
  deemphasize_window (GTK_WIDGET (iw));
}

static gboolean
draw_flash (GtkWidget          *widget,
            cairo_t            *cr,
            GtkInspectorWindow *iw)
{
  GtkAllocation alloc;

  if (iw && iw->flash_count % 2 == 0)
    return FALSE;

  if (GTK_IS_WINDOW (widget))
    {
      GtkWidget *child = gtk_bin_get_child (GTK_BIN (widget));
      /* We don't want to draw the drag highlight around the
       * CSD window decorations
       */
      if (child == NULL)
        return FALSE;

      gtk_widget_get_allocation (child, &alloc);
    }
  else
    {
      alloc.x = 0;
      alloc.y = 0;
      alloc.width = gtk_widget_get_allocated_width (widget);
      alloc.height = gtk_widget_get_allocated_height (widget);
    }

  cairo_set_source_rgba (cr, 0.0, 0.0, 1.0, 0.2);
  cairo_rectangle (cr,
                   alloc.x + 0.5, alloc.y + 0.5,
                   alloc.width - 1, alloc.height - 1);
  cairo_fill (cr);

  return FALSE;
}

static gboolean
on_flash_timeout (GtkInspectorWindow *iw)
{
  gtk_widget_queue_draw (iw->flash_widget);

  iw->flash_count++;

  if (iw->flash_count == 6)
    {
      g_signal_handlers_disconnect_by_func (iw->flash_widget, draw_flash, iw);
      g_signal_handlers_disconnect_by_func (iw->flash_widget, clear_flash, iw);
      iw->flash_widget = NULL;
      iw->flash_cnx = 0;

      return G_SOURCE_REMOVE;
    }

  return G_SOURCE_CONTINUE;
}

void
gtk_inspector_flash_widget (GtkInspectorWindow *iw,
                            GtkWidget          *widget)
{
  if (!gtk_widget_get_visible (widget) || !gtk_widget_get_mapped (widget))
    return;

  if (iw->flash_cnx != 0)
    {
      g_source_remove (iw->flash_cnx);
      iw->flash_cnx = 0;
    }

  start_flash (iw, widget);
  iw->flash_cnx = g_timeout_add (150, (GSourceFunc) on_flash_timeout, iw);
}

void
gtk_inspector_start_highlight (GtkWidget *widget)
{
  g_signal_connect_after (widget, "draw", G_CALLBACK (draw_flash), NULL);
  gtk_widget_queue_draw (widget);
}

void
gtk_inspector_stop_highlight (GtkWidget *widget)
{
  g_signal_handlers_disconnect_by_func (widget, draw_flash, NULL);
  g_signal_handlers_disconnect_by_func (widget, clear_flash, NULL);
  gtk_widget_queue_draw (widget);
}

void
gtk_inspector_window_select_widget_under_pointer (GtkInspectorWindow *iw)
{
  GdkDisplay *display;
  GdkDeviceManager *dm;
  GdkDevice *device;
  GtkWidget *widget;

  display = gdk_display_get_default ();
  dm = gdk_display_get_device_manager (display);
  device = gdk_device_manager_get_client_pointer (dm);

  widget = find_widget_at_pointer (device);

  if (widget)
    select_widget (iw, widget);
}

/* vim: set et sw=2 ts=2: */
