#include "config.h"

#include <clutter/clutter.h>
#include <meta/window.h>
#include <mtk/mtk.h>

#include "shell-window-preview-layout.h"

typedef struct _ShellWindowPreviewLayoutPrivate ShellWindowPreviewLayoutPrivate;
struct _ShellWindowPreviewLayoutPrivate
{
  ClutterActor *container;
  GHashTable *windows;

  ClutterActorBox bounding_box;
};

enum
{
  PROP_0,

  PROP_BOUNDING_BOX,

  PROP_LAST
};

static GParamSpec *obj_props[PROP_LAST] = { NULL, };

G_DEFINE_TYPE_WITH_PRIVATE (ShellWindowPreviewLayout, shell_window_preview_layout,
                            CLUTTER_TYPE_LAYOUT_MANAGER);

typedef struct _WindowInfo
{
  MetaWindow *window;
  ClutterActor *window_actor;

  gulong size_changed_id;
  gulong position_changed_id;
  gulong window_actor_destroy_id;
  gulong destroy_id;
} WindowInfo;

static void
shell_window_preview_layout_get_property (GObject      *object,
                                          unsigned int  property_id,
                                          GValue       *value,
                                          GParamSpec   *pspec)
{
  ShellWindowPreviewLayout *self = SHELL_WINDOW_PREVIEW_LAYOUT (object);
  ShellWindowPreviewLayoutPrivate *priv;

  priv = shell_window_preview_layout_get_instance_private (self);

  switch (property_id)
    {
    case PROP_BOUNDING_BOX:
      g_value_set_boxed (value, &priv->bounding_box);
      break;

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

static void
shell_window_preview_layout_set_container (ClutterLayoutManager *layout,
                                           ClutterActor         *container)
{
  ShellWindowPreviewLayout *self = SHELL_WINDOW_PREVIEW_LAYOUT (layout);
  ShellWindowPreviewLayoutPrivate *priv;
  ClutterLayoutManagerClass *parent_class;

  priv = shell_window_preview_layout_get_instance_private (self);

  priv->container = container;

  parent_class = CLUTTER_LAYOUT_MANAGER_CLASS (shell_window_preview_layout_parent_class);
  parent_class->set_container (layout, container);
}

static void
shell_window_preview_layout_get_preferred_width (ClutterLayoutManager *layout,
                                                 ClutterActor         *container,
                                                 float                 for_height,
                                                 float                *min_width_p,
                                                 float                *natural_width_p)
{
  ShellWindowPreviewLayout *self = SHELL_WINDOW_PREVIEW_LAYOUT (layout);
  ShellWindowPreviewLayoutPrivate *priv;

  priv = shell_window_preview_layout_get_instance_private (self);

  if (min_width_p)
    *min_width_p = 0;

  if (natural_width_p)
    *natural_width_p = clutter_actor_box_get_width (&priv->bounding_box);
}

static void
shell_window_preview_layout_get_preferred_height (ClutterLayoutManager *layout,
                                                  ClutterActor         *container,
                                                  float                 for_width,
                                                  float                *min_height_p,
                                                  float                *natural_height_p)
{
  ShellWindowPreviewLayout *self = SHELL_WINDOW_PREVIEW_LAYOUT (layout);
  ShellWindowPreviewLayoutPrivate *priv;

  priv = shell_window_preview_layout_get_instance_private (self);

  if (min_height_p)
    *min_height_p = 0;

  if (natural_height_p)
    *natural_height_p = clutter_actor_box_get_height (&priv->bounding_box);
}


static void
shell_window_preview_layout_allocate (ClutterLayoutManager   *layout,
                                      ClutterActor           *container,
                                      const ClutterActorBox  *box)
{
  ShellWindowPreviewLayout *self = SHELL_WINDOW_PREVIEW_LAYOUT (layout);
  ShellWindowPreviewLayoutPrivate *priv;
  float scale_x, scale_y;
  float bounding_box_width, bounding_box_height;
  ClutterActorIter iter;
  ClutterActor *child;

  priv = shell_window_preview_layout_get_instance_private (self);

  bounding_box_width = clutter_actor_box_get_width (&priv->bounding_box);
  bounding_box_height = clutter_actor_box_get_height (&priv->bounding_box);

  if (bounding_box_width == 0)
    scale_x = 1.f;
  else
    scale_x = clutter_actor_box_get_width (box) / bounding_box_width;

  if (bounding_box_height == 0)
    scale_y = 1.f;
  else
    scale_y = clutter_actor_box_get_height (box) / bounding_box_height;

  clutter_actor_iter_init (&iter, container);
  while (clutter_actor_iter_next (&iter, &child))
    {
      ClutterActorBox child_box = { 0, };
      WindowInfo *window_info;

      if (!clutter_actor_is_visible (child))
        continue;

      window_info = g_hash_table_lookup (priv->windows, child);

      if (window_info)
        {
          MtkRectangle buffer_rect;
          float child_nat_width, child_nat_height;

          meta_window_get_buffer_rect (window_info->window, &buffer_rect);

          clutter_actor_box_set_origin (&child_box,
                                        buffer_rect.x - priv->bounding_box.x1,
                                        buffer_rect.y - priv->bounding_box.y1);

          clutter_actor_get_preferred_size (child, NULL, NULL,
                                            &child_nat_width, &child_nat_height);

          clutter_actor_box_set_size (&child_box, child_nat_width, child_nat_height);

          child_box.x1 *= scale_x;
          child_box.x2 *= scale_x;
          child_box.y1 *= scale_y;
          child_box.y2 *= scale_y;

          clutter_actor_allocate (child, &child_box);
        }
      else
        {
          float x, y;

          clutter_actor_get_fixed_position (child, &x, &y);
          clutter_actor_allocate_preferred_size (child, x, y);
        }
    }
}

static void
on_layout_changed (ShellWindowPreviewLayout *self)
{
  ShellWindowPreviewLayoutPrivate *priv;
  GHashTableIter iter;
  gpointer value;
  gboolean first_rect = TRUE;
  MtkRectangle bounding_rect = { 0, };
  ClutterActorBox old_bounding_box;

  priv = shell_window_preview_layout_get_instance_private (self);

  old_bounding_box =
    (ClutterActorBox) CLUTTER_ACTOR_BOX_INIT (priv->bounding_box.x1,
                                              priv->bounding_box.y1,
                                              priv->bounding_box.x2,
                                              priv->bounding_box.y2);

  g_hash_table_iter_init (&iter, priv->windows);
  while (g_hash_table_iter_next (&iter, NULL, &value))
    {
      WindowInfo *window_info = value;
      MtkRectangle frame_rect;

      meta_window_get_frame_rect (window_info->window, &frame_rect);

      if (first_rect)
        {
          bounding_rect = frame_rect;
          first_rect = FALSE;
          continue;
        }

      mtk_rectangle_union (&frame_rect, &bounding_rect, &bounding_rect);
    }

  clutter_actor_box_set_origin (&priv->bounding_box,
                                (float) bounding_rect.x,
                                (float) bounding_rect.y);
  clutter_actor_box_set_size (&priv->bounding_box,
                              (float) bounding_rect.width,
                              (float) bounding_rect.height);

  if (!clutter_actor_box_equal (&priv->bounding_box, &old_bounding_box))
    g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_BOUNDING_BOX]);

  clutter_layout_manager_layout_changed (CLUTTER_LAYOUT_MANAGER (self));
}

static void
on_window_size_position_changed (MetaWindow               *window,
                                 ShellWindowPreviewLayout *self)
{
  on_layout_changed (self);
}

static void
on_window_destroyed (ClutterActor *actor)
{
  clutter_actor_destroy (actor);
}

static void
on_actor_destroyed (ClutterActor *actor,
                    ShellWindowPreviewLayout *self)
{
  ShellWindowPreviewLayoutPrivate *priv;
  WindowInfo *window_info;

  priv = shell_window_preview_layout_get_instance_private (self);

  window_info = g_hash_table_lookup (priv->windows, actor);
  g_assert (window_info != NULL);

  shell_window_preview_layout_remove_window (self, window_info->window);
}

static void
shell_window_preview_layout_dispose (GObject *gobject)
{
  ShellWindowPreviewLayout *self = SHELL_WINDOW_PREVIEW_LAYOUT (gobject);
  ShellWindowPreviewLayoutPrivate *priv;
  GHashTableIter iter;
  gpointer key, value;

  priv = shell_window_preview_layout_get_instance_private (self);

  g_hash_table_iter_init (&iter, priv->windows);
  while (g_hash_table_iter_next (&iter, &key, &value))
    {
      ClutterActor *actor = key;
      WindowInfo *info = value;

      g_clear_signal_handler (&info->size_changed_id, info->window);
      g_clear_signal_handler (&info->position_changed_id, info->window);
      g_clear_signal_handler (&info->window_actor_destroy_id, info->window_actor);
      g_clear_signal_handler (&info->destroy_id, actor);

      clutter_actor_remove_child (priv->container, actor);
    }

  g_hash_table_remove_all (priv->windows);

  G_OBJECT_CLASS (shell_window_preview_layout_parent_class)->dispose (gobject);
}

static void
shell_window_preview_layout_finalize (GObject *gobject)
{
  ShellWindowPreviewLayout *self = SHELL_WINDOW_PREVIEW_LAYOUT (gobject);
  ShellWindowPreviewLayoutPrivate *priv;

  priv = shell_window_preview_layout_get_instance_private (self);

  g_hash_table_destroy (priv->windows);

  G_OBJECT_CLASS (shell_window_preview_layout_parent_class)->finalize (gobject);
}

static void
shell_window_preview_layout_init (ShellWindowPreviewLayout *self)
{
  ShellWindowPreviewLayoutPrivate *priv;

  priv = shell_window_preview_layout_get_instance_private (self);

  priv->windows = g_hash_table_new_full (NULL, NULL, NULL,
                                         (GDestroyNotify) g_free);
}

static void
shell_window_preview_layout_class_init (ShellWindowPreviewLayoutClass *klass)
{
  ClutterLayoutManagerClass *layout_class = CLUTTER_LAYOUT_MANAGER_CLASS (klass);
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);

  layout_class->get_preferred_width = shell_window_preview_layout_get_preferred_width;
  layout_class->get_preferred_height = shell_window_preview_layout_get_preferred_height;
  layout_class->allocate = shell_window_preview_layout_allocate;
  layout_class->set_container = shell_window_preview_layout_set_container;

  gobject_class->dispose = shell_window_preview_layout_dispose;
  gobject_class->finalize = shell_window_preview_layout_finalize;
  gobject_class->get_property = shell_window_preview_layout_get_property;

  /**
   * ShellWindowPreviewLayout:bounding-box:
   */
  obj_props[PROP_BOUNDING_BOX] =
    g_param_spec_boxed ("bounding-box",
                        "Bounding Box",
                        "Bounding Box",
                        CLUTTER_TYPE_ACTOR_BOX,
                        G_PARAM_READABLE |
                        G_PARAM_STATIC_STRINGS);

  g_object_class_install_properties (gobject_class, PROP_LAST, obj_props);
}

/**
 * shell_window_preview_layout_add_window:
 * @self: a #ShellWindowPreviewLayout
 * @window: the #MetaWindow
 *
 * Creates a ClutterActor drawing the texture of @window and adds it
 * to the container. If @window is already part of the preview, this
 * function will do nothing.
 *
 * Returns: (nullable) (transfer none): The newly created actor drawing @window
 */
ClutterActor *
shell_window_preview_layout_add_window (ShellWindowPreviewLayout *self,
                                        MetaWindow               *window)
{
  ShellWindowPreviewLayoutPrivate *priv;
  ClutterActor *window_actor, *actor;
  WindowInfo *window_info;
  GHashTableIter iter;
  gpointer value;

  g_return_val_if_fail (SHELL_IS_WINDOW_PREVIEW_LAYOUT (self), NULL);
  g_return_val_if_fail (META_IS_WINDOW (window), NULL);

  priv = shell_window_preview_layout_get_instance_private (self);

  g_hash_table_iter_init (&iter, priv->windows);
  while (g_hash_table_iter_next (&iter, NULL, &value))
    {
      WindowInfo *info = value;

      if (info->window == window)
        return NULL;
    }

  window_actor = CLUTTER_ACTOR (meta_window_get_compositor_private (window));
  actor = clutter_clone_new (window_actor);

  window_info = g_new0 (WindowInfo, 1);

  window_info->window = window;
  window_info->window_actor = window_actor;
  window_info->size_changed_id =
    g_signal_connect (window, "size-changed",
                      G_CALLBACK (on_window_size_position_changed), self);
  window_info->position_changed_id =
    g_signal_connect (window, "position-changed",
                      G_CALLBACK (on_window_size_position_changed), self);
  window_info->window_actor_destroy_id =
    g_signal_connect_swapped (window_actor, "destroy",
                              G_CALLBACK (on_window_destroyed), actor);
  window_info->destroy_id =
    g_signal_connect (actor, "destroy",
                      G_CALLBACK (on_actor_destroyed), self);

  g_hash_table_insert (priv->windows, actor, window_info);

  clutter_actor_add_child (priv->container, actor);

  on_layout_changed (self);

  return actor;
}

/**
 * shell_window_preview_layout_remove_window:
 * @self: a #ShellWindowPreviewLayout
 * @window: the #MetaWindow
 *
 * Removes a MetaWindow @window from the preview which has been added
 * previously using shell_window_preview_layout_add_window().
 * If @window is not part of preview, this function will do nothing.
 */
void
shell_window_preview_layout_remove_window (ShellWindowPreviewLayout *self,
                                           MetaWindow               *window)
{
  ShellWindowPreviewLayoutPrivate *priv;
  ClutterActor *actor;
  WindowInfo *window_info = NULL;
  GHashTableIter iter;
  gpointer key, value;

  g_return_if_fail (SHELL_IS_WINDOW_PREVIEW_LAYOUT (self));
  g_return_if_fail (META_IS_WINDOW (window));

  priv = shell_window_preview_layout_get_instance_private (self);

  g_hash_table_iter_init (&iter, priv->windows);
  while (g_hash_table_iter_next (&iter, &key, &value))
    {
      WindowInfo *info = value;

      if (info->window == window)
        {
          actor = CLUTTER_ACTOR (key);
          window_info = info;
          break;
        }
    }

  if (window_info == NULL)
    return;

  g_clear_signal_handler (&window_info->size_changed_id, window);
  g_clear_signal_handler (&window_info->position_changed_id, window);
  g_clear_signal_handler (&window_info->window_actor_destroy_id, window_info->window_actor);
  g_clear_signal_handler (&window_info->destroy_id, actor);

  g_hash_table_remove (priv->windows, actor);

  clutter_actor_remove_child (priv->container, actor);

  on_layout_changed (self);
}

/**
 * shell_window_preview_layout_get_windows:
 * @self: a #ShellWindowPreviewLayout
 *
 * Gets an array of all MetaWindows that were added to the layout
 * using shell_window_preview_layout_add_window(), ordered by the
 * insertion order.
 *
 * Returns: (transfer container) (element-type Meta.Window): The list of windows
 */
GList *
shell_window_preview_layout_get_windows (ShellWindowPreviewLayout *self)
{
  ShellWindowPreviewLayoutPrivate *priv;
  GList *windows = NULL;
  GHashTableIter iter;
  gpointer value;

  g_return_val_if_fail (SHELL_IS_WINDOW_PREVIEW_LAYOUT (self), NULL);

  priv = shell_window_preview_layout_get_instance_private (self);

  g_hash_table_iter_init (&iter, priv->windows);
  while (g_hash_table_iter_next (&iter, NULL, &value))
    {
      WindowInfo *window_info = value;

      windows = g_list_prepend (windows, window_info->window);
    }

  return windows;
}
