#include <stdio.h>
#include <stdlib.h>

#include <gmodule.h>
#include <cogl/cogl.h>
#include <clutter/clutter.h>

/* layout actor, by Lucas Rocha */

#define MY_TYPE_THING                (my_thing_get_type ())
#define MY_THING(obj)                (G_TYPE_CHECK_INSTANCE_CAST ((obj), MY_TYPE_THING, MyThing))
#define MY_IS_THING(obj)             (G_TYPE_CHECK_INSTANCE_TYPE ((obj), MY_TYPE_THING))
#define MY_THING_CLASS(klass)        (G_TYPE_CHECK_CLASS_CAST ((klass),  MY_TYPE_THING, MyThingClass))
#define MY_IS_THING_CLASS(klass)     (G_TYPE_CHECK_CLASS_TYPE ((klass),  MY_TYPE_THING))
#define MY_THING_GET_CLASS(obj)      (G_TYPE_INSTANCE_GET_CLASS ((obj),  MY_TYPE_THING, MyThingClass))

typedef struct _MyThing             MyThing;
typedef struct _MyThingPrivate      MyThingPrivate;
typedef struct _MyThingClass        MyThingClass;

struct _MyThing
{
  ClutterActor parent_instance;

  MyThingPrivate *priv;
};

struct _MyThingClass
{
  ClutterActorClass parent_class;
};

enum
{
  PROP_0,

  PROP_SPACING,
  PROP_PADDING,
  PROP_USE_TRANSFORMED_BOX
};

static void clutter_container_iface_init (ClutterContainerIface *iface);

G_DEFINE_TYPE_WITH_CODE (MyThing,
                         my_thing,
                         CLUTTER_TYPE_ACTOR,
                         G_IMPLEMENT_INTERFACE (CLUTTER_TYPE_CONTAINER,
                                                clutter_container_iface_init));

#define MY_THING_GET_PRIVATE(obj)    \
(G_TYPE_INSTANCE_GET_PRIVATE ((obj), MY_TYPE_THING, MyThingPrivate))

struct _MyThingPrivate
{
  GList  *children;
  
  gfloat  spacing;
  gfloat  padding;

  guint   use_transformed_box : 1;
};

/* Add, remove, foreach, copied from ClutterGroup code. */

static void
my_thing_real_add (ClutterContainer *container,
                   ClutterActor     *actor)
{
  MyThing *group = MY_THING (container);
  MyThingPrivate *priv = group->priv;

  g_object_ref (actor);

  priv->children = g_list_append (priv->children, actor);
  clutter_actor_set_parent (actor, CLUTTER_ACTOR (group));

  g_signal_emit_by_name (container, "actor-added", actor);

  /* queue relayout to allocate new item */
  clutter_actor_queue_relayout (CLUTTER_ACTOR (group));

  g_object_unref (actor);
}

static void
my_thing_real_remove (ClutterContainer *container,
                      ClutterActor     *actor)
{
  MyThing *group = MY_THING (container);
  MyThingPrivate *priv = group->priv;

  g_object_ref (actor);

  priv->children = g_list_remove (priv->children, actor);
  clutter_actor_unparent (actor);

  /* At this point, the actor passed to the "actor-removed" signal
   * handlers is not parented anymore to the container but since we
   * are holding a reference on it, it's still valid
   */
  g_signal_emit_by_name (container, "actor-removed", actor);

  /* queue relayout to re-allocate children without the
     removed item */
  clutter_actor_queue_relayout (CLUTTER_ACTOR (group));

  g_object_unref (actor);
}

static void
my_thing_real_foreach (ClutterContainer *container,
                       ClutterCallback   callback,
                       gpointer          user_data)
{
  MyThingPrivate *priv = MY_THING (container)->priv;
  GList *l;

  for (l = priv->children; l; l = l->next)
    (* callback) (CLUTTER_ACTOR (l->data), user_data);
}

static void
clutter_container_iface_init (ClutterContainerIface *iface)
{
  iface->add = my_thing_real_add;
  iface->remove = my_thing_real_remove;
  iface->foreach = my_thing_real_foreach;
}

static void
my_thing_set_property (GObject      *gobject,
                       guint         prop_id,
                       const GValue *value,
                       GParamSpec   *pspec)
{
  MyThingPrivate *priv = MY_THING (gobject)->priv;
  gboolean needs_relayout = TRUE;

  switch (prop_id)
    {
    case PROP_SPACING:
      priv->spacing = g_value_get_float (value);
      break;

    case PROP_PADDING:
      priv->padding = g_value_get_float (value);
      break;

    case PROP_USE_TRANSFORMED_BOX:
      priv->use_transformed_box = g_value_get_boolean (value);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
      needs_relayout = FALSE;
      break;
    }

  /* setting spacing or padding queues a relayout 
     because they are supposed to change the internal
     allocation of children */
  if (needs_relayout)
    clutter_actor_queue_relayout (CLUTTER_ACTOR (gobject));
}

static void
my_thing_get_property (GObject    *gobject,
                       guint       prop_id,
                       GValue     *value,
                       GParamSpec *pspec)
{
  MyThingPrivate *priv = MY_THING (gobject)->priv;

  switch (prop_id)
    {
    case PROP_SPACING:
      g_value_set_float (value, priv->spacing);
      break;

    case PROP_PADDING:
      g_value_set_float (value, priv->padding);
      break;

    case PROP_USE_TRANSFORMED_BOX:
      g_value_set_boolean (value, priv->use_transformed_box);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
      break;
    }
}

static void
my_thing_finalize (GObject *gobject)
{
  G_OBJECT_CLASS (my_thing_parent_class)->finalize (gobject);
}

static void
my_thing_dispose (GObject *gobject)
{
  MyThing *self = MY_THING (gobject);
  MyThingPrivate *priv = self->priv;

  if (priv->children)
    {
      g_list_foreach (priv->children, (GFunc) clutter_actor_destroy, NULL);
      priv->children = NULL;
    }

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

static void
my_thing_get_preferred_width (ClutterActor *self,
                              gfloat   for_height,
                              gfloat  *min_width_p,
                              gfloat  *natural_width_p)
{
  MyThingPrivate *priv;
  GList *l;
  gfloat min_left, min_right;
  gfloat natural_left, natural_right;

  priv = MY_THING (self)->priv;

  min_left = 0;
  min_right = 0;
  natural_left = 0;
  natural_right = 0;

  for (l = priv->children; l != NULL; l = l->next)
    {
      ClutterActor *child;
      gfloat child_x, child_min, child_natural;

      child = l->data;

      child_x = clutter_actor_get_x (child);

      clutter_actor_get_preferred_size (child,
                                        &child_min, NULL,
                                        &child_natural, NULL);

      if (l == priv->children)
        {
          /* First child */
          min_left = child_x;
          natural_left = child_x;
          min_right = min_left + child_min;
          natural_right = natural_left + child_natural;
        }
      else
        {
          /* Union of extents with previous children */
          if (child_x < min_left)
            min_left = child_x;

          if (child_x < natural_left)
            natural_left = child_x;

          if (child_x + child_min > min_right)
            min_right = child_x + child_min;

          if (child_x + child_natural > natural_right)
            natural_right = child_x + child_natural;
        }
    }

  if (min_left < 0)
    min_left = 0;

  if (natural_left < 0)
    natural_left = 0;

  if (min_right < 0)
    min_right = 0;

  if (natural_right < 0)
    natural_right = 0;

  g_assert (min_right >= min_left);
  g_assert (natural_right >= natural_left);

  if (min_width_p)
    *min_width_p = min_right - min_left;

  if (natural_width_p)
    *natural_width_p = natural_right - min_left;
}

static void
my_thing_get_preferred_height (ClutterActor *self,
                               gfloat   for_width,
                               gfloat  *min_height_p,
                               gfloat  *natural_height_p)
{
  MyThingPrivate *priv;
  GList *l;
  gfloat min_top, min_bottom;
  gfloat natural_top, natural_bottom;

  priv = MY_THING (self)->priv;

  min_top = 0;
  min_bottom = 0;
  natural_top = 0;
  natural_bottom = 0;

  for (l = priv->children; l != NULL; l = l->next)
    {
      ClutterActor *child;
      gfloat child_y, child_min, child_natural;

      child = l->data;

      child_y = clutter_actor_get_y (child);

      clutter_actor_get_preferred_size (child,
                                        NULL, &child_min,
                                        NULL, &child_natural);

      if (l == priv->children)
        {
          /* First child */
          min_top = child_y;
          natural_top = child_y;
          min_bottom = min_top + child_min;
          natural_bottom = natural_top + child_natural;
        }
      else
        {
          /* Union of extents with previous children */
          if (child_y < min_top)
            min_top = child_y;

          if (child_y < natural_top)
            natural_top = child_y;

          if (child_y + child_min > min_bottom)
            min_bottom = child_y + child_min;

          if (child_y + child_natural > natural_bottom)
            natural_bottom = child_y + child_natural;
        }
    }

  if (min_top < 0)
    min_top = 0;

  if (natural_top < 0)
    natural_top = 0;

  if (min_bottom < 0)
    min_bottom = 0;

  if (natural_bottom < 0)
    natural_bottom = 0;

  g_assert (min_bottom >= min_top);
  g_assert (natural_bottom >= natural_top);

  if (min_height_p)
    *min_height_p = min_bottom - min_top;

  if (natural_height_p)
    *natural_height_p = natural_bottom - min_top;
}

static void
my_thing_allocate (ClutterActor           *self,
                   const ClutterActorBox  *box,
                   ClutterAllocationFlags  flags)
{
  MyThingPrivate *priv;
  gfloat current_x, current_y, max_row_height;
  GList *l;

  /* chain up to set actor->allocation */
  CLUTTER_ACTOR_CLASS (my_thing_parent_class)->allocate (self, box, flags);

  priv = MY_THING (self)->priv;

  current_x = priv->padding;
  current_y = priv->padding;
  max_row_height = 0;

  /* The allocation logic here is to horizontally place children 
   * side-by-side and reflow into a new row when we run out of 
   * space 
   */
  for (l = priv->children; l != NULL; l = l->next)
    {
      ClutterActor *child;
      gfloat natural_width, natural_height;
      ClutterActorBox child_box;
 
      child = l->data;

      clutter_actor_get_preferred_size (child,
                                        NULL, NULL,
                                        &natural_width, &natural_height);

      /* if it fits in the current row, keep it there; otherwise
       * reflow into another row
       */
      if (current_x + natural_width > box->x2 - box->x1 - priv->padding)
        {
          current_x = priv->padding;
          current_y += max_row_height + priv->spacing;
          max_row_height = 0;
        }

      child_box.x1 = current_x;
      child_box.y1 = current_y;
      child_box.x2 = child_box.x1 + natural_width;
      child_box.y2 = child_box.y1 + natural_height;

      clutter_actor_allocate (child, &child_box, flags);

      /* if we take into account the transformation of the children
       * then we first check if it's transformed; then we get the
       * onscreen coordinates of the two points of the bounding box
       * of the actor (origin(x, y) and (origin + size)(x,y)) and
       * we update the coordinates and area given to the next child
       */
      if (priv->use_transformed_box)
        {
          if (clutter_actor_is_scaled (child) ||
              clutter_actor_is_rotated (child))
            {
              ClutterVertex v1 = { 0, }, v2 = { 0, };
              ClutterActorBox transformed_box = { 0, };

              /* origin */
              if (!(flags & CLUTTER_ABSOLUTE_ORIGIN_CHANGED))
                {
                  v1.x = 0;
                  v1.y = 0;
                }
              else
                {
                  v1.x = box->x1;
                  v1.y = box->y1;
                }

              clutter_actor_apply_transform_to_point (child, &v1, &v2);
              transformed_box.x1 = v2.x;
              transformed_box.y1 = v2.y;

              /* size */
              v1.x = natural_width;
              v1.y = natural_height;
              clutter_actor_apply_transform_to_point (child, &v1, &v2);
              transformed_box.x2 = v2.x;
              transformed_box.y2 = v2.y;

              natural_width = transformed_box.x2 - transformed_box.x1;
              natural_height = transformed_box.y2 - transformed_box.y1;
            }
        }

      /* Record the maximum child height on current row to know
       * what's the increment that should be used for the next  
       * row 
       */
      if (natural_height > max_row_height)
        max_row_height = natural_height;

      current_x += natural_width + priv->spacing;
    }
}

static void
my_thing_paint (ClutterActor *actor)
{
  MyThing *self = MY_THING (actor);
  GList *c;

  cogl_push_matrix();

  /* paint all visible children */
  for (c = self->priv->children;
       c != NULL;
       c = c->next)
    {
      ClutterActor *child = c->data;

      g_assert (child != NULL);

      clutter_actor_paint (child);
    }

  cogl_pop_matrix();
}

#define MIN_SIZE 24
#define MAX_SIZE 64

static void
my_thing_class_init (MyThingClass *klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
  ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass);

  gobject_class->set_property = my_thing_set_property;
  gobject_class->get_property = my_thing_get_property;
  gobject_class->dispose      = my_thing_dispose;
  gobject_class->finalize     = my_thing_finalize;

  actor_class->get_preferred_width  = my_thing_get_preferred_width;
  actor_class->get_preferred_height = my_thing_get_preferred_height;
  actor_class->allocate             = my_thing_allocate;
  actor_class->paint                = my_thing_paint;

  g_object_class_install_property (gobject_class,
                                   PROP_SPACING,
                                   g_param_spec_float ("spacing",
                                                       "Spacing",
                                                       "Spacing of the thing",
                                                       0, G_MAXFLOAT,
                                                       0,
                                                       G_PARAM_READWRITE));

  g_object_class_install_property (gobject_class,
                                   PROP_PADDING,
                                   g_param_spec_float ("padding",
                                                       "Padding",
                                                       "Padding around the thing",
                                                       0, G_MAXFLOAT,
                                                       0,
                                                       G_PARAM_READWRITE));

  g_object_class_install_property (gobject_class,
                                   PROP_USE_TRANSFORMED_BOX,
                                   g_param_spec_boolean ("use-transformed-box",
                                                         "Use Transformed Box",
                                                         "Use transformed box when allocating",
                                                         FALSE,
                                                         G_PARAM_READWRITE));

  g_type_class_add_private (klass, sizeof (MyThingPrivate));
}

static void
my_thing_init (MyThing *thing)
{
  thing->priv = MY_THING_GET_PRIVATE (thing);
}

ClutterActor *
my_thing_new (gfloat padding,
              gfloat spacing)
{
  return g_object_new (MY_TYPE_THING,
                       "padding", padding,
                       "spacing", spacing,
                       NULL);
}

/* test code */

static ClutterActor *box              = NULL;
static ClutterActor *icon             = NULL;
static ClutterTimeline *main_timeline = NULL;
static ClutterBehaviour *behaviour    = NULL;

static ClutterColor bg_color;

static void
toggle_property_value (ClutterActor *actor,
                       const gchar  *property_name)
{
  gboolean value;

  g_object_get (G_OBJECT (actor),
                property_name, &value,
                NULL);

  value = !value;

  g_object_set (G_OBJECT (box),
                property_name, value,
                NULL);
}

static void
increase_property_value (ClutterActor *actor, 
                         const char   *property_name)
{
  gfloat value;

  g_object_get (G_OBJECT (actor),
                property_name, &value,
                NULL);

  value = value + 10.0;

  g_object_set (G_OBJECT (box),
                property_name, value,
                NULL);
}

static void
decrease_property_value (ClutterActor *actor, 
                         const char   *property_name)
{
  gfloat value;

  g_object_get (G_OBJECT (actor),
                property_name, &value,
                NULL);

  value = MAX (0, value - 10.0);

  g_object_set (G_OBJECT (box),
                property_name, value,
                NULL);
}

static ClutterActor *
create_item (void)
{
  ClutterActor *clone = clutter_clone_new (icon);
  
  gint32 size = g_random_int_range (MIN_SIZE, MAX_SIZE);
  
  clutter_actor_set_size (clone, size, size);

  clutter_behaviour_apply (behaviour, clone);

  return clone;
}

static gboolean
keypress_cb (ClutterActor *actor,
	     ClutterEvent *event,
	     gpointer      data)
{
  switch (clutter_event_get_key_symbol (event))
    {
    case CLUTTER_KEY_q:
      clutter_main_quit ();
      break;

    case CLUTTER_KEY_a:
      {
        if (icon != NULL)
          {
            ClutterActor *clone = create_item (); 

            /* Add one item to container */
            clutter_container_add_actor (CLUTTER_CONTAINER (box), clone);
          }
        break;
      }

    case CLUTTER_KEY_d:
      {
        GList *children = 
          clutter_container_get_children (CLUTTER_CONTAINER (box));

        if (children)
          {
            GList *last = g_list_last (children);

            /* Remove last item on container */
            clutter_container_remove_actor (CLUTTER_CONTAINER (box), 
                                            CLUTTER_ACTOR (last->data));
          }
        break;
      }

    case CLUTTER_KEY_w:
      {
        decrease_property_value (box, "padding");
        break;
      }

    case CLUTTER_KEY_e:
      {
        increase_property_value (box, "padding");
        break;
      }

    case CLUTTER_KEY_r:
      {
        decrease_property_value (box, "spacing");
        break;
      }

    case CLUTTER_KEY_s:
      {
        toggle_property_value (box, "use-transformed-box");
        break;
      }

    case CLUTTER_KEY_t:
      {
        increase_property_value (box, "spacing");
        break;
      }

    case CLUTTER_KEY_z:
      {
        if (clutter_timeline_is_playing (main_timeline))
          clutter_timeline_pause (main_timeline);
        else
          clutter_timeline_start (main_timeline);

        break;
      }

    default:
      break;
    }

  return FALSE;
}

static void
relayout_on_frame (ClutterTimeline *timeline)
{
  gboolean use_transformed_box;

  /* if we care about transformations updating the layout, we need to inform
   * the layout that a transformation is happening; this is either done by
   * attaching a notification on the transformation properties or by simply
   * queuing a relayout on each frame of the timeline used to drive the
   * behaviour. for simplicity's sake, we used the latter
   */

  g_object_get (G_OBJECT (box),
                "use-transformed-box", &use_transformed_box,
                NULL);

  if (use_transformed_box)
    clutter_actor_queue_relayout (box);
}

G_MODULE_EXPORT int
test_layout_main (int argc, char *argv[])
{
  ClutterActor *stage, *instructions;
  ClutterAlpha *alpha;
  gint i, size;
  GError *error = NULL;

  clutter_init (&argc, &argv);

  stage = clutter_stage_get_default ();
  clutter_actor_set_size (stage, 800, 600);

  clutter_color_from_string (&bg_color, "Red");

  main_timeline = clutter_timeline_new (2000);
  clutter_timeline_set_loop (main_timeline, TRUE);
  g_signal_connect (main_timeline, "new-frame",
                    G_CALLBACK (relayout_on_frame),
                    NULL);

  alpha = clutter_alpha_new_full (main_timeline, CLUTTER_LINEAR);
  behaviour = clutter_behaviour_scale_new (alpha, 1.0, 1.0, 2.0, 2.0);

  box = my_thing_new (10, 10);

  clutter_actor_set_position (box, 20, 20);
  clutter_actor_set_size (box, 350, -1);

  icon = clutter_texture_new_from_file (TESTS_DATADIR
                                        G_DIR_SEPARATOR_S
                                        "redhand.png",
                                        &error);
  if (error)
    g_error ("Unable to load 'redhand.png': %s", error->message);

  size = g_random_int_range (MIN_SIZE, MAX_SIZE);
  clutter_actor_set_size (icon, size, size);
  clutter_behaviour_apply (behaviour, icon);
  clutter_container_add_actor (CLUTTER_CONTAINER (box), icon);

  for (i = 1; i < 33; i++)
    {
      ClutterActor *clone = create_item (); 

      clutter_container_add_actor (CLUTTER_CONTAINER (box), clone);
    }

  clutter_container_add_actor (CLUTTER_CONTAINER (stage), box);

  instructions = clutter_text_new_with_text ("Sans 14",
                                              "<b>Instructions:</b>\n"
                                              "a - add a new item\n"
                                              "d - remove last item\n"
                                              "z - start/pause behaviour\n"
                                              "w - decrease padding\n"
                                              "e - increase padding\n"
                                              "r - decrease spacing\n"
                                              "t - increase spacing\n"
                                              "s - use transformed box\n"
                                              "q - quit");

  clutter_text_set_use_markup (CLUTTER_TEXT (instructions), TRUE);
  clutter_actor_set_position (instructions, 450, 10);
  clutter_container_add_actor (CLUTTER_CONTAINER (stage), instructions);

  g_signal_connect (stage, "key-release-event",
		    G_CALLBACK (keypress_cb),
		    NULL);

  clutter_actor_show (stage);

  clutter_main ();

  g_object_unref (main_timeline);
  g_object_unref (behaviour);

  return EXIT_SUCCESS;
}
