/* cc-display-arrangement.c
 *
 * Copyright (C) 2007, 2008, 2017  Red Hat, Inc.
 * Copyright (C) 2013 Intel, Inc.
 *
 * Written by: Benjamin Berg <bberg@redhat.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, 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 <math.h>
#include "cc-display-arrangement.h"
#include "cc-display-config.h"
#include "cc-display-labeler.h"

struct _CcDisplayArrangement
{
  GtkDrawingArea    object;

  CcDisplayConfig  *config;

  cairo_matrix_t    to_widget;
  cairo_matrix_t    to_actual;

  gboolean          drag_active;
  CcDisplayMonitor *selected_output;
  CcDisplayMonitor *prelit_output;
  /* Starting position of cursor inside the monitor. */
  gdouble           drag_anchor_x;
  gdouble           drag_anchor_y;

  guint             major_snap_distance;
};

typedef struct _CcDisplayArrangement CcDisplayArrangement;

enum {
  PROP_0,
  PROP_CONFIG,
  PROP_SELECTED_OUTPUT,
  PROP_LAST
};

typedef enum {
  SNAP_DIR_NONE = 0,
  SNAP_DIR_X    = 1 << 0,
  SNAP_DIR_Y    = 1 << 1,
  SNAP_DIR_BOTH = (SNAP_DIR_X | SNAP_DIR_Y),
} SnapDirection;

typedef struct {
  cairo_matrix_t     to_widget;
  guint              major_snap_distance;
  gdouble            dist_x;
  gdouble            dist_y;
  gint               mon_x;
  gint               mon_y;
  SnapDirection      snapped;
} SnapData;

#define MARGIN_PX  0
#define MARGIN_MON  0.66
#define MAJOR_SNAP_DISTANCE 25
#define MINOR_SNAP_DISTANCE 5
#define MIN_OVERLAP 25

G_DEFINE_TYPE (CcDisplayArrangement, cc_display_arrangement, GTK_TYPE_DRAWING_AREA)

static GParamSpec *props[PROP_LAST];

static void
apply_rotation_to_geometry (CcDisplayMonitor *output,
                            int              *w,
                            int              *h)
{
  CcDisplayRotation rotation;

  rotation = cc_display_monitor_get_rotation (output);
  if ((rotation == CC_DISPLAY_ROTATION_90) || (rotation == CC_DISPLAY_ROTATION_270))
    {
      int tmp;
      tmp = *h;
      *h = *w;
      *w = tmp;
    }
}

/* get_geometry */
static void
get_scaled_geometry (CcDisplayConfig  *config,
                     CcDisplayMonitor *output,
                     double            max_scale,
                     int              *x,
                     int              *y,
                     int              *w,
                     int              *h)
{
  if (cc_display_monitor_is_active (output))
    {
      cc_display_monitor_get_geometry (output, x, y, w, h);
    }
  else
    {
      cc_display_monitor_get_geometry (output, x, y, NULL, NULL);
      CcDisplayMode *mode = cc_display_monitor_get_preferred_mode(output);
      if (!mode)
        mode = CC_DISPLAY_MODE (cc_display_monitor_get_modes(output)->data);
      cc_display_mode_get_resolution (mode, w, h);
    }

  if (cc_display_config_is_layout_logical (config))
    {
      double scale = cc_display_monitor_get_scale (output);

      if (cc_display_config_layout_use_ui_scale (config))
        scale /= ceil (max_scale);

      *w = round (*w / scale);
      *h = round (*h / scale);
    }

  apply_rotation_to_geometry (output, w, h);
}

static void
get_bounding_box (CcDisplayConfig *config,
                  gint            *x1,
                  gint            *y1,
                  gint            *x2,
                  gint            *y2,
                  gint            *max_w,
                  gint            *max_h)
{
  GList *outputs, *l;
  gdouble max_scale;

  g_assert (x1 && y1 && x2 && y2);

  *x1 = *y1 = G_MAXINT;
  *x2 = *y2 = G_MININT;
  *max_w = 0;
  *max_h = 0;
  max_scale = cc_display_config_get_maximum_scaling (config);

  outputs = cc_display_config_get_monitors (config);
  for (l = outputs; l; l = l->next)
    {
      CcDisplayMonitor *output = l->data;
      int x, y, w, h;

      if (!cc_display_monitor_is_useful (output))
        continue;

      get_scaled_geometry (config, output, max_scale, &x, &y, &w, &h);

      *x1 = MIN (*x1, x);
      *y1 = MIN (*y1, y);
      *x2 = MAX (*x2, x + w);
      *y2 = MAX (*y2, y + h);
      *max_w = MAX (*max_w, w);
      *max_h = MAX (*max_h, h);
    }
}

static void
monitor_get_drawing_rect (CcDisplayArrangement *self,
                          CcDisplayMonitor     *output,
                          gint                 *x1,
                          gint                 *y1,
                          gint                 *x2,
                          gint                 *y2)
{
  gdouble x, y;
  gdouble max_scale;

  max_scale = cc_display_config_get_maximum_scaling (self->config);
  get_scaled_geometry (self->config, output, max_scale, x1, y1, x2, y2);

  /* get_scaled_geometry returns the width and height */
  *x2 = *x1 + *x2;
  *y2 = *y1 + *y2;

  x = *x1; y = *y1;
  cairo_matrix_transform_point (&self->to_widget, &x, &y);
  *x1 = round (x);
  *y1 = round (y);

  x = *x2; y = *y2;
  cairo_matrix_transform_point (&self->to_widget, &x, &y);
  *x2 = round (x);
  *y2 = round (y);
}


static void
get_snap_distance (SnapData  *snap_data,
                   gint       mon_x,
                   gint       mon_y,
                   gint       new_x,
                   gint       new_y,
                   gdouble   *dist_x,
                   gdouble   *dist_y)
{
  gdouble local_dist_x, local_dist_y;

  local_dist_x = ABS (mon_x - new_x);
  local_dist_y = ABS (mon_y - new_y);

  cairo_matrix_transform_distance (&snap_data->to_widget, &local_dist_x, &local_dist_y);

  if (dist_x)
    *dist_x = local_dist_x;
  if (dist_y)
    *dist_y = local_dist_y;
}

static void
maybe_update_snap (SnapData       *snap_data,
                   gint            mon_x,
                   gint            mon_y,
                   gint            new_x,
                   gint            new_y,
                   SnapDirection   snapped,
                   SnapDirection   major_axis,
                   gint            minor_unlimited)
{
  SnapDirection update_snap = SNAP_DIR_NONE;
  gdouble dist_x, dist_y;
  gdouble dist;

  get_snap_distance (snap_data, mon_x, mon_y, new_x, new_y, &dist_x, &dist_y);
  dist = MAX (dist_x, dist_y);

  /* Snap by the variable max snap distance on the major axis, ensure the
   * minor axis is below the minimum snapping distance (often just zero). */
  switch (major_axis)
    {
      case SNAP_DIR_X:
        if (dist_x > snap_data->major_snap_distance)
          return;
        if (dist_y > MINOR_SNAP_DISTANCE)
          {
            if (new_y > mon_y && minor_unlimited <= 0)
              return;
            if (new_y < mon_y && minor_unlimited >= 0)
              return;
          }
        break;

      case SNAP_DIR_Y:
        if (dist_y > snap_data->major_snap_distance)
          return;
        if (dist_x > MINOR_SNAP_DISTANCE)
          {
            if (new_x > mon_x && minor_unlimited <= 0)
              return;
            if (new_x < mon_x && minor_unlimited >= 0)
              return;
          }
        break;

      default:
        g_assert_not_reached();
    }

  if (snapped == SNAP_DIR_BOTH)
    {
      if (snap_data->snapped == SNAP_DIR_NONE)
        update_snap = SNAP_DIR_BOTH;

      /* Update, if this is closer on the main axis. */
      if (((major_axis == SNAP_DIR_X) && (dist_x < snap_data->dist_x)) ||
          ((major_axis == SNAP_DIR_Y) && (dist_y < snap_data->dist_y)))
        {
          update_snap = SNAP_DIR_BOTH;
        }

      /* Also update if we were only snapping in one direction earlier and it
       * is better or equally good. */
      if ((snap_data->snapped == SNAP_DIR_X && (dist <= snap_data->dist_x)) ||
          (snap_data->snapped == SNAP_DIR_Y && (dist <= snap_data->dist_y)))
        {
          update_snap = SNAP_DIR_BOTH;
        }

      /* Also allow a minor axis to be added if the first axis remains identical. */
      if (((snap_data->snapped == SNAP_DIR_X) && (major_axis == SNAP_DIR_X) && (new_x == snap_data->mon_x)) ||
          ((snap_data->snapped == SNAP_DIR_Y) && (major_axis == SNAP_DIR_Y) && (new_y == snap_data->mon_y)))
        {
          update_snap = SNAP_DIR_BOTH;
        }
    }
  else if (snapped == SNAP_DIR_X)
    {
      if (dist_x < snap_data->dist_x || (snap_data->snapped & SNAP_DIR_X) == SNAP_DIR_NONE)
        update_snap = SNAP_DIR_X;
    }
  else if (snapped == SNAP_DIR_Y)
    {
      if (dist_y < snap_data->dist_y || (snap_data->snapped & SNAP_DIR_Y) == SNAP_DIR_NONE)
        update_snap = SNAP_DIR_Y;
    }
  else
    {
      g_assert_not_reached ();
    }

  if (update_snap & SNAP_DIR_X)
    {
      snap_data->dist_x = dist_x;
      snap_data->mon_x = new_x;
      snap_data->snapped = snap_data->snapped | SNAP_DIR_X;
    }
  if (update_snap & SNAP_DIR_Y)
    {
      snap_data->dist_y = dist_y;
      snap_data->mon_y = new_y;
      snap_data->snapped = snap_data->snapped | SNAP_DIR_Y;
    }
}

static void
find_best_snapping (CcDisplayConfig   *config,
                    CcDisplayMonitor  *snap_output,
                    SnapData          *snap_data)
{
  GList *outputs, *l;
  gint x1, y1, x2, y2;
  gint w, h;
  double max_scale;

  g_assert (snap_data != NULL);

  max_scale = cc_display_config_get_maximum_scaling (config);
  get_scaled_geometry (config, snap_output, max_scale, &x1, &y1, &w, &h);
  x2 = x1 + w;
  y2 = y1 + h;

#define OVERLAP(_s1, _s2, _t1, _t2) ((_s1) <= (_t2) && (_t1) <= (_s2))

  outputs = cc_display_config_get_monitors (config);
  for (l = outputs; l; l = l->next)
    {
      CcDisplayMonitor *output = l->data;
      gint _x1, _y1, _x2, _y2, _h, _w;
      gint bottom_snap_pos;
      gint top_snap_pos;
      gint left_snap_pos;
      gint right_snap_pos;
      gdouble dist_x, dist_y;
      gdouble max_scale;
      gdouble tmp;

      if (output == snap_output)
        continue;

      if (!cc_display_monitor_is_useful (output))
        continue;

      max_scale = cc_display_config_get_maximum_scaling (config);
      get_scaled_geometry (config, output, max_scale, &_x1, &_y1, &_w, &_h);
      _x2 = _x1 + _w;
      _y2 = _y1 + _h;

      top_snap_pos = _y1 - h;
      bottom_snap_pos = _y2;
      left_snap_pos = _x1 - w;
      right_snap_pos = _x2;

      dist_y = 9999;
      /* overlap on the X axis */
      if (OVERLAP (x1, x2, _x1, _x2))
        {
          get_snap_distance (snap_data, x1, y1, x1, top_snap_pos, NULL, &dist_y);
          get_snap_distance (snap_data, x1, y1, x1, bottom_snap_pos, NULL, &tmp);
          dist_y = MIN(dist_y, tmp);
        }

      dist_x = 9999;
      /* overlap on the Y axis */
      if (OVERLAP (y1, y2, _y1, _y2))
        {
          get_snap_distance (snap_data, x1, y1, left_snap_pos, y1, &dist_x, NULL);
          get_snap_distance (snap_data, x1, y1, right_snap_pos, y1, &tmp, NULL);
          dist_x = MIN(dist_x, tmp);
        }

      /* We only snap horizontally or vertically to an edge of the same monitor */
      if (dist_y < dist_x)
        {
          maybe_update_snap (snap_data, x1, y1, x1, top_snap_pos, SNAP_DIR_Y, SNAP_DIR_Y, 0);
          maybe_update_snap (snap_data, x1, y1, x1, bottom_snap_pos, SNAP_DIR_Y, SNAP_DIR_Y, 0);
        }
      else if (dist_x < 9999)
        {
          maybe_update_snap (snap_data, x1, y1, left_snap_pos, y1, SNAP_DIR_X, SNAP_DIR_X, 0);
          maybe_update_snap (snap_data, x1, y1, right_snap_pos, y1, SNAP_DIR_X, SNAP_DIR_X, 0);
        }

      /* Left/right edge identical on the top */
      maybe_update_snap (snap_data, x1, y1, _x1, top_snap_pos, SNAP_DIR_BOTH, SNAP_DIR_Y, 0);
      maybe_update_snap (snap_data, x1, y1, _x2 - w, top_snap_pos, SNAP_DIR_BOTH, SNAP_DIR_Y, 0);

      /* Left/right edge identical on the bottom */
      maybe_update_snap (snap_data, x1, y1, _x1, bottom_snap_pos, SNAP_DIR_BOTH, SNAP_DIR_Y, 0);
      maybe_update_snap (snap_data, x1, y1, _x2 - w, bottom_snap_pos, SNAP_DIR_BOTH, SNAP_DIR_Y, 0);

      /* Top/bottom edge identical on the left */
      maybe_update_snap (snap_data, x1, y1, left_snap_pos, _y1, SNAP_DIR_BOTH, SNAP_DIR_X, 0);
      maybe_update_snap (snap_data, x1, y1, left_snap_pos, _y2 - h, SNAP_DIR_BOTH, SNAP_DIR_X, 0);

      /* Top/bottom edge identical on the right */
      maybe_update_snap (snap_data, x1, y1, right_snap_pos, _y1, SNAP_DIR_BOTH, SNAP_DIR_X, 0);
      maybe_update_snap (snap_data, x1, y1, right_snap_pos, _y2 - h, SNAP_DIR_BOTH, SNAP_DIR_X, 0);

      /* If snapping is infinite, then add snapping points with minimal overlap
       * to prevent detachment.
       * This is similar to the above but simply re-defines the snapping pos
       * to have only minimal overlap */
      if (snap_data->major_snap_distance == G_MAXUINT)
        {
          /* Hanging over the left/right edge on the top */
          maybe_update_snap (snap_data, x1, y1, _x1 - w + MIN_OVERLAP, top_snap_pos, SNAP_DIR_BOTH, SNAP_DIR_Y, 1);
          maybe_update_snap (snap_data, x1, y1, _x2 - MIN_OVERLAP, top_snap_pos, SNAP_DIR_BOTH, SNAP_DIR_Y, -1);

          /* Left/right edge identical on the bottom */
          maybe_update_snap (snap_data, x1, y1, _x1 - w + MIN_OVERLAP, bottom_snap_pos, SNAP_DIR_BOTH, SNAP_DIR_Y, 1);
          maybe_update_snap (snap_data, x1, y1, _x2 - MIN_OVERLAP, bottom_snap_pos, SNAP_DIR_BOTH, SNAP_DIR_Y, -1);

          /* Top/bottom edge identical on the left */
          maybe_update_snap (snap_data, x1, y1, left_snap_pos, _y1 - h + MIN_OVERLAP, SNAP_DIR_BOTH, SNAP_DIR_X, 1);
          maybe_update_snap (snap_data, x1, y1, left_snap_pos, _y2 - MIN_OVERLAP, SNAP_DIR_BOTH, SNAP_DIR_X, -1);

          /* Top/bottom edge identical on the right */
          maybe_update_snap (snap_data, x1, y1, right_snap_pos, _y1 - h + MIN_OVERLAP, SNAP_DIR_BOTH, SNAP_DIR_X, 1);
          maybe_update_snap (snap_data, x1, y1, right_snap_pos, _y2 - MIN_OVERLAP, SNAP_DIR_BOTH, SNAP_DIR_X, -1);
        }
    }

#undef OVERLAP
}

static void
cc_display_arrangement_update_matrices (CcDisplayArrangement *self)
{
  GtkAllocation allocation;
  gdouble scale, scale_h, scale_w;
  gint x1, y1, x2, y2, max_w, max_h;

  g_assert (self->config);

  /* Do not update the matrices while the user is dragging things around. */
  if (self->drag_active)
    return;

  get_bounding_box (self->config, &x1, &y1, &x2, &y2, &max_w, &max_h);
  gtk_widget_get_allocation (GTK_WIDGET (self), &allocation);

  scale_h = (gdouble) (allocation.width - 2 * MARGIN_PX) / (x2 - x1 + max_w * 2 * MARGIN_MON);
  scale_w = (gdouble) (allocation.height - 2 * MARGIN_PX) / (y2 - y1 + max_h * 2 * MARGIN_MON);

  scale = MIN (scale_h, scale_w);

  cairo_matrix_init_identity (&self->to_widget);
  cairo_matrix_translate (&self->to_widget, allocation.width / 2.0, allocation.height / 2.0);
  cairo_matrix_scale (&self->to_widget, scale, scale);
  cairo_matrix_translate (&self->to_widget, - (x1 + x2) / 2.0, - (y1 + y2) / 2.0);

  self->to_actual = self->to_widget;
  cairo_matrix_invert (&self->to_actual);
}

static CcDisplayMonitor*
cc_display_arrangement_find_monitor_at (CcDisplayArrangement *self,
                                        gint                  x,
                                        gint                  y)
{
  g_autoptr(GList) outputs = NULL;
  GList *l;

  outputs = g_list_copy (cc_display_config_get_monitors (self->config));

  if (self->selected_output)
    outputs = g_list_prepend (outputs, self->selected_output);

  for (l = outputs; l; l = l->next)
    {
      CcDisplayMonitor *output = l->data;
      gint x1, y1, x2, y2;

      if (!cc_display_monitor_is_usable (output))
        continue;

      if (!cc_display_monitor_is_active (output))
        {
          cc_display_monitor_get_disabled_geometry (output, &x1, &y1, &x2, &y2);
        }
      else
        {
          monitor_get_drawing_rect (self, output, &x1, &y1, &x2, &y2);
        }

      if (x >= x1 && x <= x2 && y >= y1 && y <= y2)
        return output;
    }

  return NULL;
}

static void
cc_display_arrangement_update_cursor (CcDisplayArrangement *self,
                                      gboolean              dragable)
{
  g_autoptr(GdkCursor) cursor = NULL;
  GdkWindow *window;

  if (dragable)
    cursor = gdk_cursor_new_for_display (gtk_widget_get_display (GTK_WIDGET (self)), GDK_FLEUR);
  else
    cursor = NULL;

  window = gtk_widget_get_window (GTK_WIDGET (self));

  if (window)
    gdk_window_set_cursor (window, cursor);
}

static void
on_output_changed_cb (CcDisplayArrangement *self,
                      CcDisplayMonitor     *output)
{
  if (cc_display_config_count_useful_monitors (self->config) > 2)
    self->major_snap_distance = MAJOR_SNAP_DISTANCE;
  else
    self->major_snap_distance = G_MAXUINT;

  gtk_widget_queue_draw (GTK_WIDGET (self));
}

static gint
sort_outputs_by_disabled (gconstpointer a, gconstpointer b)
{
    CcDisplayMonitor *ma = CC_DISPLAY_MONITOR ((gpointer) a);
    CcDisplayMonitor *mb = CC_DISPLAY_MONITOR ((gpointer) b);

    if (!cc_display_monitor_is_active (ma) && cc_display_monitor_is_active (mb))
        return -1;


    if (cc_display_monitor_is_active (ma) && !cc_display_monitor_is_active (mb))
        return 1;

    return cc_display_monitor_get_ui_number ((gpointer) a) < cc_display_monitor_get_ui_number ((gpointer) b) ? -1 : 1;
}

static gboolean
cc_display_arrangement_draw (GtkWidget *widget,
                             cairo_t   *cr)
{
  CcDisplayArrangement *self = CC_DISPLAY_ARRANGEMENT (widget);
  GtkStyleContext *context = gtk_widget_get_style_context (widget);
  g_autoptr(GList) outputs = NULL;
  GList *l;
  gint last_disabled_x = 0;

  if (!self->config)
    return FALSE;

  cc_display_arrangement_update_matrices (self);

  gtk_style_context_save (context);
  gtk_style_context_add_class (context, "display-arrangement");

  cairo_save (cr);
  gtk_render_background (context, cr, 0, 0, gtk_widget_get_allocated_width (widget), gtk_widget_get_allocated_height (widget));
  cairo_restore (cr);
  /* Draw in reverse order so that hit detection matches visual. Also pull
   * the selected output to the back. */
  outputs = g_list_copy (cc_display_config_get_monitors (self->config));
  outputs = g_list_sort (outputs, (GCompareFunc) sort_outputs_by_disabled);

  if (cc_display_monitor_is_active (self->selected_output))
    {
      outputs = g_list_remove (outputs, self->selected_output);
      if (self->selected_output != NULL)
        outputs = g_list_prepend (outputs, self->selected_output);
    }

  outputs = g_list_reverse (outputs);

  for (l = outputs; l; l = l->next)
    {
      CcDisplayMonitor *output = l->data;
      GtkStateFlags state = GTK_STATE_FLAG_NORMAL;
      GtkBorder border, padding, margin;
      GdkRGBA bg_rgba;
      gint x1, y1, x2, y2;
      gint w, h;
      gint ui_number, color_index;

      if (!cc_display_monitor_is_usable (output))
        continue;

      gtk_style_context_save (context);
      cairo_save (cr);

      gtk_style_context_add_class (context, "monitor");

      if (output == self->selected_output)
        state |= GTK_STATE_FLAG_SELECTED;
      if (output == self->prelit_output)
        state |= GTK_STATE_FLAG_PRELIGHT;

      gtk_style_context_set_state (context, state);
      if (cc_display_monitor_is_primary (output) || cc_display_config_is_cloning (self->config))
        gtk_style_context_add_class (context, "primary");

      monitor_get_drawing_rect (self, output, &x1, &y1, &x2, &y2);
      w = x2 - x1;
      h = y2 - y1;

      if (!cc_display_monitor_is_active (output))
        {
          h = 50;
          w = h * 1.77;
          cairo_translate (cr, last_disabled_x, 0);

          cc_display_monitor_set_disabled_geometry (output, last_disabled_x, 0, w, h);
          last_disabled_x += w;
        }
        else
        {
          cairo_translate (cr, x1, y1);
        }

      gtk_style_context_get_margin (context, state, &margin);

      cairo_translate (cr, margin.left, margin.top);

      w -= margin.left + margin.right;
      h -= margin.top + margin.bottom;

      cairo_save (cr);

      /* Set in cc-display-panel.c */
      ui_number = cc_display_monitor_get_ui_number (output);
      color_index = ui_number - 1; // ui_numbers start at 1, our color index is 0-based.

      gchar *rgba_str;

      g_signal_emit_by_name (G_OBJECT (widget), "get-output-color", color_index, &rgba_str);

      if (gdk_rgba_parse (&bg_rgba, rgba_str))
        {
          if (!cc_display_monitor_is_active (output))
            {
              bg_rgba.alpha = 0.35;
            }
          gdk_cairo_set_source_rgba (cr, &bg_rgba);
        }

      g_free (rgba_str);

      cairo_rectangle (cr, 0, 0, w, h);
      cairo_fill (cr);
      cairo_restore (cr);

      gtk_render_frame (context, cr, 0, 0, w, h);

      gtk_style_context_get_border (context, state, &border);
      gtk_style_context_get_padding (context, state, &padding);

      w -= border.left + border.right + padding.left + padding.right;
      h -= border.top + border.bottom + padding.top + padding.bottom;

      cairo_translate (cr, border.left + padding.left, border.top + padding.top);

      if (ui_number > 0)
        {
          PangoLayout *layout;
          PangoFontDescription *font = NULL;
          g_autofree gchar *number_str = NULL;
          PangoRectangle extents;
          GdkRGBA color;
          gdouble text_width, text_padding;

          gtk_style_context_add_class (context, "monitor-label");
          gtk_style_context_remove_class (context, "monitor");

          gtk_style_context_get_border (context, state, &border);
          gtk_style_context_get_padding (context, state, &padding);
          gtk_style_context_get_margin (context, state, &margin);

          cairo_translate (cr, margin.left, margin.top);

          number_str = g_strdup_printf ("%d", ui_number);
          gtk_style_context_get (context, state, "font", &font, NULL);
          layout = gtk_widget_create_pango_layout (GTK_WIDGET (self), number_str);
          pango_layout_set_font_description (layout, font);
          pango_font_description_free (font);
          pango_layout_get_extents (layout, NULL, &extents);

          h = (extents.height - extents.y) / PANGO_SCALE;
          text_width = (extents.width - extents.x) / PANGO_SCALE;
          w = MAX (text_width, h - padding.left - padding.right);
          text_padding = w - text_width;

          w += border.left + border.right + padding.left + padding.right;
          h += border.top + border.bottom + padding.top + padding.bottom;

          gtk_render_background (context, cr, 0, 0, w, h);
          gtk_render_frame (context, cr, 0, 0, w, h);

          cairo_translate (cr, border.left + padding.left, border.top + padding.top);
          cairo_translate (cr, extents.x + text_padding / 2, 0);

          gtk_style_context_get_color (context, state, &color);
          gdk_cairo_set_source_rgba (cr, &color);

          gtk_render_layout (context, cr, 0, 0, layout);
          g_object_unref (layout);
        }

      gtk_style_context_restore (context);
      cairo_restore (cr);
    }

  gtk_style_context_restore (context);

  return TRUE;
}

static gboolean
cc_display_arrangement_button_press_event (GtkWidget      *widget,
                                           GdkEventButton *event)
{
  CcDisplayArrangement *self = CC_DISPLAY_ARRANGEMENT (widget);
  CcDisplayMonitor *output;
  gdouble event_x, event_y;
  gint mon_x, mon_y;

  if (!self->config)
    return FALSE;

  /* Only handle normal presses of the left mouse button. */
  if (event->button != 1 || event->type != GDK_BUTTON_PRESS)
    return FALSE;

  g_return_val_if_fail (self->drag_active == FALSE, FALSE);

  output = cc_display_arrangement_find_monitor_at (self, event->x, event->y);
  if (!output)
    return FALSE;

  if (!cc_display_monitor_is_active (output))
    {
      cc_display_arrangement_set_selected_output (self, output);
      return FALSE;
    }

  event_x = event->x;
  event_y = event->y;

  cairo_matrix_transform_point (&self->to_actual, &event_x, &event_y);
  cc_display_monitor_get_geometry (output, &mon_x, &mon_y, NULL, NULL);

  cc_display_arrangement_set_selected_output (self, output);

  if (cc_display_config_count_useful_monitors (self->config) > 1)
    {
      self->drag_active = TRUE;
      self->drag_anchor_x = event_x - mon_x;
      self->drag_anchor_y = event_y - mon_y;
    }

  return TRUE;
}

static gboolean
cc_display_arrangement_button_release_event (GtkWidget      *widget,
                                             GdkEventButton *event)
{
  CcDisplayArrangement *self = CC_DISPLAY_ARRANGEMENT (widget);
  CcDisplayMonitor *output;

  if (!self->config)
    return FALSE;

  /* Only handle left mouse button */
  if (event->button != 1)
    return FALSE;

  if (!self->drag_active)
    return FALSE;

  self->drag_active = FALSE;

  output = cc_display_arrangement_find_monitor_at (self, event->x, event->y);
  cc_display_arrangement_update_cursor (self, output != NULL);

  /* And queue a redraw to recenter everything */
  gtk_widget_queue_draw (widget);

  g_signal_emit_by_name (G_OBJECT (widget), "updated");

  return TRUE;
}

static gboolean
cc_display_arrangement_motion_notify_event (GtkWidget      *widget,
                                            GdkEventMotion *event)
{
  CcDisplayArrangement *self = CC_DISPLAY_ARRANGEMENT (widget);
  gdouble event_x, event_y;
  gint mon_x, mon_y;
  SnapData snap_data;

  if (!self->config)
    return FALSE;

  if (cc_display_config_count_useful_monitors (self->config) <= 1)
    return FALSE;

  if (!self->drag_active)
    {
      CcDisplayMonitor *output;
      output = cc_display_arrangement_find_monitor_at (self, event->x, event->y);

      if (output != NULL && !cc_display_monitor_is_active (output))
        return FALSE;

      cc_display_arrangement_update_cursor (self, output != NULL);
      if (self->prelit_output != output)
        gtk_widget_queue_draw (widget);

      self->prelit_output = output;

      return FALSE;
    }

  g_assert (self->selected_output);

  event_x = event->x;
  event_y = event->y;

  cairo_matrix_transform_point (&self->to_actual, &event_x, &event_y);

  mon_x = round (event_x - self->drag_anchor_x);
  mon_y = round (event_y - self->drag_anchor_y);

  /* The monitor is now at the location as if there was no snapping whatsoever. */
  snap_data.snapped = SNAP_DIR_NONE;
  snap_data.mon_x = mon_x;
  snap_data.mon_y = mon_y;
  snap_data.dist_x = 0;
  snap_data.dist_y = 0;
  snap_data.to_widget = self->to_widget;
  snap_data.major_snap_distance = self->major_snap_distance;

  cc_display_monitor_set_position (self->selected_output, mon_x, mon_y);

  find_best_snapping (self->config, self->selected_output, &snap_data);

  cc_display_monitor_set_position (self->selected_output, snap_data.mon_x, snap_data.mon_y);

  return TRUE;
}

static void
cc_display_arrangement_get_property (GObject    *object,
                                     guint       prop_id,
                                     GValue     *value,
                                     GParamSpec *pspec)
{
  CcDisplayArrangement *self = CC_DISPLAY_ARRANGEMENT (object);

  switch (prop_id)
    {
    case PROP_CONFIG:
      g_value_set_object (value, self->config);
      break;

    case PROP_SELECTED_OUTPUT:
      g_value_set_object (value, self->selected_output);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
cc_display_arrangement_set_property (GObject      *object,
                                     guint         prop_id,
                                     const GValue *value,
                                     GParamSpec   *pspec)
{
  CcDisplayArrangement *obj = CC_DISPLAY_ARRANGEMENT (object);

  switch (prop_id)
    {
    case PROP_CONFIG:
      cc_display_arrangement_set_config (obj, g_value_get_object (value));
      break;

    case PROP_SELECTED_OUTPUT:
      cc_display_arrangement_set_selected_output (obj, g_value_get_object (value));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
cc_display_arrangement_finalize (GObject *object)
{
  CcDisplayArrangement *self = CC_DISPLAY_ARRANGEMENT (object);

  g_clear_object (&self->config);

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

static void
cc_display_arrangement_class_init (CcDisplayArrangementClass *klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

  gobject_class->finalize = cc_display_arrangement_finalize;
  gobject_class->get_property = cc_display_arrangement_get_property;
  gobject_class->set_property = cc_display_arrangement_set_property;

  widget_class->draw = cc_display_arrangement_draw;
  widget_class->button_press_event = cc_display_arrangement_button_press_event;
  widget_class->button_release_event = cc_display_arrangement_button_release_event;
  widget_class->motion_notify_event = cc_display_arrangement_motion_notify_event;

  props[PROP_CONFIG] = g_param_spec_object ("config", "Display Config",
                                            "The display configuration to work with",
                                            CC_TYPE_DISPLAY_CONFIG,
                                            G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

  props[PROP_SELECTED_OUTPUT] = g_param_spec_object ("selected-output", "Selected Output",
                                                     "The output that is currently selected on the configuration",
                                                     CC_TYPE_DISPLAY_MONITOR,
                                                     G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

  g_object_class_install_properties (gobject_class,
                                     PROP_LAST,
                                     props);

  g_signal_new ("updated",
                CC_TYPE_DISPLAY_ARRANGEMENT,
                G_SIGNAL_RUN_LAST,
                0, NULL, NULL, NULL,
                G_TYPE_NONE, 0);

  g_signal_new ("get-output-color",
                CC_TYPE_DISPLAY_ARRANGEMENT,
                G_SIGNAL_RUN_LAST,
                0, NULL, NULL, NULL,
                G_TYPE_STRING, 1, G_TYPE_INT);
}

static void
cc_display_arrangement_init (CcDisplayArrangement *self)
{
  /* XXX: Do we need to listen to touch events here? */
  gtk_widget_add_events (GTK_WIDGET (self),
                         GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK);

  self->major_snap_distance = MAJOR_SNAP_DISTANCE;
}

CcDisplayArrangement*
cc_display_arrangement_new (CcDisplayConfig *config)
{
  return g_object_new (CC_TYPE_DISPLAY_ARRANGEMENT, "config", config, NULL);
}

CcDisplayConfig*
cc_display_arrangement_get_config (CcDisplayArrangement *self)
{
  return self->config;
}

void
cc_display_arrangement_set_config (CcDisplayArrangement *self,
                                   CcDisplayConfig      *config)
{
  const gchar *signals[] = { "rotation", "mode", "primary", "active", "scale", "position-changed", "is-usable" };
  GList *outputs, *l;
  guint i;

  if (self->config)
    {
      outputs = cc_display_config_get_monitors (self->config);
      for (l = outputs; l; l = l->next)
        {
          CcDisplayMonitor *output = l->data;

          g_signal_handlers_disconnect_by_data (output, self);
        }
    }
  g_clear_object (&self->config);

  self->drag_active = FALSE;

  /* Listen to all the signals */
  if (config)
    {
      self->config = g_object_ref (config);

      outputs = cc_display_config_get_monitors (self->config);
      for (l = outputs; l; l = l->next)
        {
          CcDisplayMonitor *output = l->data;

          for (i = 0; i < G_N_ELEMENTS (signals); ++i)
            g_signal_connect_object (output, signals[i], G_CALLBACK (on_output_changed_cb), self, G_CONNECT_SWAPPED);
        }
    }

  cc_display_arrangement_set_selected_output (self, NULL);

  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CONFIG]);
}

CcDisplayMonitor*
cc_display_arrangement_get_selected_output (CcDisplayArrangement *self)
{
  return self->selected_output;
}

void
cc_display_arrangement_set_selected_output (CcDisplayArrangement *self,
                                            CcDisplayMonitor     *output)
{
  g_return_if_fail (self->drag_active == FALSE);

  /* XXX: Could check that it actually belongs to the right config object. */
  self->selected_output = output;

  gtk_widget_queue_draw (GTK_WIDGET (self));

  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTED_OUTPUT]);
}

void
cc_display_config_snap_output (CcDisplayConfig  *config,
                               CcDisplayMonitor *output)
{
  SnapData snap_data;
  gint x, y, w, h;
  gdouble max_scale;

  if (!cc_display_monitor_is_useful (output))
    return;

  if (cc_display_config_count_useful_monitors (config) <= 1)
    return;

  max_scale = cc_display_config_get_maximum_scaling (config);
  get_scaled_geometry (config, output, max_scale, &x, &y, &w, &h);

  snap_data.snapped = SNAP_DIR_NONE;
  snap_data.mon_x = x;
  snap_data.mon_y = y;
  snap_data.dist_x = 0;
  snap_data.dist_y = 0;
  cairo_matrix_init_identity (&snap_data.to_widget);
  snap_data.major_snap_distance = G_MAXUINT;

  find_best_snapping (config, output, &snap_data);

  cc_display_monitor_set_position (output, snap_data.mon_x, snap_data.mon_y);
}
