/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
/*
 * Copyright 2009 Canonical Ltd.
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of either or both of the following licenses:
 *
 * 1) the GNU Lesser General Public License version 3, as published by the
 * Free Software Foundation; and/or
 * 2) the GNU Lesser General Public License version 2.1, as published by
 * the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranties of
 * MERCHANTABILITY, SATISFACTORY QUALITY or FITNESS FOR A PARTICULAR
 * PURPOSE.  See the applicable version of the GNU Lesser General Public
 * License for more details.
 *
 * You should have received a copy of both the GNU Lesser General Public
 * License version 3 and version 2.1 along with this program.  If not, see
 * <http://www.gnu.org/licenses/>
 *
 * Authored by: Gordon Allott <gord.allott@canonical.com>
 *
 */
#if HAVE_CONFIG_H
#include <config.h>
#endif
#include "ctk-menu.h"
#include <math.h>
#include <glib.h>
#include "ctk-gfx-private.h"
#include "ctk-arb-asm-private.h"

static void clutter_container_iface_init (ClutterContainerIface *iface);

G_DEFINE_TYPE_WITH_CODE (CtkMenu, ctk_menu, CTK_TYPE_ACTOR,
                         G_IMPLEMENT_INTERFACE (CLUTTER_TYPE_CONTAINER,
                                                clutter_container_iface_init));

#define CTK_MENU_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj),\
  CTK_TYPE_MENU, \
  CtkMenuPrivate))

static const ClutterColor default_color        = { 0, 0, 0, 255 };

struct _CtkMenuPrivate
{
  GList  *children;
  ClutterActor *special_child;
  gint16  spacing;
  gint stage_handle_id;

  CtkActor *attached_actor;
  gint attached_actor_handle_id;

  ClutterActor *background;

  gboolean detect_clicks;
  gboolean swallow_clicks;

  ClutterTimeline *transform_timeline;
  gboolean is_transforming;
  guint transform_queue_handler;
  gfloat previous_height;
  gfloat previous_width;
  gfloat previous_x, previous_y;

  gboolean close_on_leave;
  guint    close_on_leave_timeout;
  ClutterStage  *attached_stage;

  /* background */
  guint blurred_background_texture;
  gboolean refresh_background_texture;
};

enum
{
  PROP_0,

  PROP_SPACING,
  PROP_BACKGROUND,
  PROP_NUM_ITEMS,
  PROP_CLOSE_ON_LEAVE
};

enum
{
  CLOSED,

  LAST_SIGNAL
};
/* Globals */
static guint _menu_signals[LAST_SIGNAL] = { 0 };

/* Forwards */
static void ctk_menu_remove           (ClutterContainer *container,
                                       ClutterActor *actor);
static void ctk_menu_add              (ClutterContainer *container,
                                       ClutterActor     *actor);

static void ctk_menu_foreach          (ClutterContainer *container,
                                       ClutterCallback   callback,
                                       gpointer          user_data);
static void ctk_menu_foreach_with_internals (ClutterContainer *container,
                                             ClutterCallback   callback,
                                             gpointer          user_data);
static void ctk_menu_raise            (ClutterContainer *container,
                                       ClutterActor     *actor,
                                       ClutterActor     *sibling);
static void ctk_menu_lower            (ClutterContainer *container,
                                       ClutterActor     *actor,
                                       ClutterActor     *sibling);
static void ctk_menu_sort_depth_order (ClutterContainer *container);
static void ctk_menu_on_realize (ClutterActor *actor);
gboolean ctk_menu_on_button_press  (ClutterActor *stage, ClutterEvent *event,
                                    ClutterActor *actor);

static void
clutter_container_iface_init (ClutterContainerIface *iface)
{
  iface->add              = ctk_menu_add;
  iface->remove           = ctk_menu_remove;
  iface->foreach          = ctk_menu_foreach;
  iface->raise            = ctk_menu_raise;
  iface->lower            = ctk_menu_lower;
  iface->sort_depth_order = ctk_menu_sort_depth_order;
  iface->foreach_with_internals = ctk_menu_foreach_with_internals;
}

void
ctk_menu_finalize (GObject *object)
{
  G_OBJECT_CLASS (ctk_menu_parent_class)->finalize (object);

  CtkMenu *menu = CTK_MENU (object);
  CtkMenuPrivate *priv =  CTK_MENU_GET_PRIVATE (menu);
  g_return_if_fail (priv);
  if (priv->blurred_background_texture)
    {
      CHECKGL (glDeleteTextures (1, &priv->blurred_background_texture));
    }
  if (menu->priv->transform_queue_handler)
    {
      g_source_remove (menu->priv->transform_queue_handler);
      menu->priv->transform_queue_handler = 0;
    }
  if (menu->priv->stage_handle_id)
    {
      g_signal_handler_disconnect (menu->priv->attached_stage,
                                   menu->priv->stage_handle_id);
      menu->priv->stage_handle_id = 0;
    }
  if (menu->priv->close_on_leave_timeout)
    {
      g_source_remove (menu->priv->close_on_leave_timeout);
      menu->priv->close_on_leave_timeout = 0;
    }
  if (menu->priv->attached_actor_handle_id)
    {
      g_signal_handler_disconnect (menu->priv->attached_actor,
                                   menu->priv->attached_actor_handle_id);
      menu->priv->attached_actor_handle_id = 0;
    }
}

static void
ctk_menu_init (CtkMenu *menu)
{
  CtkMenuPrivate *priv;
  priv = menu->priv = CTK_MENU_GET_PRIVATE (menu);

  priv->children = NULL;
  priv->special_child = NULL;
  priv->spacing = 2;
  priv->attached_actor = NULL;
  priv->attached_actor_handle_id = 0;
  priv->background = NULL;
  priv->detect_clicks = TRUE;
  priv->swallow_clicks = FALSE;
  priv->transform_timeline = NULL;
  priv->previous_width = 0;
  priv->previous_height = 0;
  priv->is_transforming = FALSE;
  priv->previous_y = 0;
  priv->close_on_leave = FALSE;
  priv->close_on_leave_timeout = 0;

  g_signal_connect (CLUTTER_ACTOR (menu),
                    "realize",
                    G_CALLBACK(ctk_menu_on_realize),
                    NULL);

  priv->blurred_background_texture = 0;
  priv->refresh_background_texture = FALSE;
  CHECKGL (glGenTextures (1, &priv->blurred_background_texture));
  CHECKGL (glActiveTextureARB(GL_TEXTURE0) );
  CHECKGL (glBindTexture (GL_TEXTURE_2D, priv->blurred_background_texture));
  CHECKGL (glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL));
  CHECKGL (glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));
  CHECKGL (glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR));
  CHECKGL (glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP));
  CHECKGL (glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP));
}

gboolean
ctk_menu_on_button_press (ClutterActor *stage, ClutterEvent *event,
                          ClutterActor *actor)
{
  CtkMenu        *menu;
  CtkMenuPrivate *priv;

  g_return_val_if_fail (CLUTTER_IS_ACTOR (actor), FALSE);
  g_return_val_if_fail (CTK_IS_MENU (actor), FALSE);

  menu = CTK_MENU (actor);
  priv = menu->priv;

  if (!priv->detect_clicks)
    return FALSE;

  if (!priv->detect_clicks)
  {
    return FALSE;
  }

  if (!CLUTTER_ACTOR_IS_VISIBLE(actor))
  {
    return FALSE;
  }

  ClutterButtonEvent *button_event = (ClutterButtonEvent*)event;
  gfloat x, y;
  ClutterActorBox box;
  ClutterActor *picked_actor;

  clutter_actor_get_transformed_position (actor, &x, &y);
  ctk_actor_get_stored_allocation (CTK_ACTOR (actor), &box);

  gboolean is_in_actor = FALSE;
  if (button_event->x > x &&
      button_event->x < (x + clutter_actor_box_get_width (&box)) &&
      button_event->y > y &&
      button_event->y < (y + clutter_actor_box_get_height (&box)))
      {
        is_in_actor = TRUE;
      }

  // check to see if we are also in our attached_actor
  if (priv->attached_actor != NULL)
    {
      picked_actor = clutter_stage_get_actor_at_pos (CLUTTER_STAGE (stage),
                                                     CLUTTER_PICK_REACTIVE,
                                                     button_event->x,
                                                     button_event->y);
      if (picked_actor == CLUTTER_ACTOR (priv->attached_actor))
        {
          is_in_actor = TRUE;
        }
    }

  if (!is_in_actor)
  {
    if (event->type == CLUTTER_BUTTON_RELEASE)
      {
        clutter_actor_destroy (actor);
      }

    return priv->swallow_clicks;
  }

  return FALSE;
}

gboolean
ctk_menu_on_mouse_motion (ClutterActor *stage, ClutterMotionEvent *event,
                          ClutterActor *actor)
{
  gfloat x, y;
  ClutterActorBox box;

  CtkMenuPrivate *priv = CTK_MENU (actor)->priv;
  CtkMenu        *menu = CTK_MENU (actor);
  CtkActor       *attached_actor = CTK_ACTOR (priv->attached_actor);

  g_return_val_if_fail (CLUTTER_IS_ACTOR (actor), FALSE);
  if (attached_actor)
    g_return_val_if_fail (CTK_IS_ACTOR (attached_actor), FALSE);

  if (!priv->close_on_leave) return FALSE;

  // we want to monitor if we are in/out of our menu or our attached actor
  // if we are, close the menu

  gboolean is_in_actor = FALSE;

  clutter_actor_get_transformed_position (actor, &x, &y);
  ctk_actor_get_stored_allocation (CTK_ACTOR (actor), &box);
  if (event->x > x &&
      event->x < (x + clutter_actor_box_get_width (&box)) &&
      event->y > y &&
      event->y < (y + clutter_actor_box_get_height (&box)))
      {
        is_in_actor = TRUE;
      }

  if (priv->attached_actor)
    {
      clutter_actor_get_transformed_position (CLUTTER_ACTOR (attached_actor), &x, &y);
      ctk_actor_get_stored_allocation (attached_actor, &box);
      if (event->x > x &&
          event->x < (x + clutter_actor_box_get_width (&box)) &&
          event->y > y &&
          event->y < (y + clutter_actor_box_get_height (&box)))
          {
            is_in_actor |= TRUE;
          }
    }

  if (is_in_actor && priv->close_on_leave_timeout != 0)
    {
      // we are in our actor and we have previously set a timeout, so unset it
      g_source_remove (priv->close_on_leave_timeout);
      priv->close_on_leave_timeout = 0;
    }
  else if (!is_in_actor && priv->close_on_leave_timeout == 0)
    {
      // we are outside out actor and we haven't set a timeout on it, so set one
      priv->close_on_leave_timeout = g_timeout_add (400,
                                                    (GSourceFunc)clutter_actor_destroy,
                                                    menu);
    }

  return FALSE;
}

gboolean
handle_stage_events (ClutterActor *stage, ClutterEvent *event,
                     ClutterActor *actor)
{
  gboolean return_val = FALSE;

  if (!CLUTTER_IS_ACTOR (actor))
    {
      g_signal_handlers_disconnect_by_func (stage,
                                            handle_stage_events,
                                            actor);
      return FALSE;
    }

  switch (event->type)
  {
    case CLUTTER_MOTION:
      return_val = ctk_menu_on_mouse_motion (stage, (ClutterMotionEvent *)(event), actor);
      break;
    case CLUTTER_BUTTON_RELEASE:
    case CLUTTER_BUTTON_PRESS:
      return_val = ctk_menu_on_button_press (stage, event, actor);
      break;
    default:
      break;
  }

  return return_val;
}


static void
ctk_menu_on_realize (ClutterActor *actor)
{
  CtkMenuPrivate *priv = CTK_MENU (actor)->priv;

  if (priv->detect_clicks)
    {
      priv->attached_stage = CLUTTER_STAGE (clutter_actor_get_stage (actor));
      priv->stage_handle_id = g_signal_connect (clutter_actor_get_stage (actor),
                                                "captured-event",
                                                G_CALLBACK(handle_stage_events),
                                                actor);
    }
}


static void
ctk_menu_paint (ClutterActor *actor)
{
  CtkActor *ctk_actor = CTK_ACTOR (actor);
  CtkMenu         *menu = CTK_MENU (actor);
  CtkMenuPrivate  *priv = menu->priv;
  GSList          *effects   = NULL;
  ClutterActorBox box;

  cogl_flush ();
  if (priv->refresh_background_texture)
    {
      ClutterActor* stage         = NULL;
      gfloat stage_width          = 0.0f;
      gfloat stage_height         = 0.0f;
      float x, y, w, h;
      int w_int, h_int;
      ClutterVertex vtx[4];

      ctk_get_actor_screen_position (CTK_ACTOR(actor), &x, &y, &w, &h, vtx);
      y -= 1.0f;

      /* It is best to use integer coordinates to compute the memory allocation size below.
       * Since the value of w and h we get from ctk_get_actor_screen_position cannot be
       * assumed to be integer, it is not safe to use them to compute a memory allocation
       * that must be precise. We use w_int and h_int instead and also mojor them by +1 for safety.
       */
      w_int = w + 1;
      h_int = h + 1;

      stage = clutter_actor_get_stage(actor);
      clutter_actor_get_size (CLUTTER_ACTOR(stage), &stage_width, &stage_height);

      CHECKGL( glPixelStorei(GL_PACK_ALIGNMENT, 1) );
      CHECKGL( glPixelStorei(GL_PACK_ROW_LENGTH, 0) );
      CHECKGL( glPixelStorei(GL_PACK_IMAGE_HEIGHT, 0) );
      CHECKGL( glPixelStorei(GL_PACK_SKIP_PIXELS, 0) );
      CHECKGL( glPixelStorei(GL_PACK_SKIP_ROWS, 0) );

      /* It does not seem rigth to use floating point values to compute a memory allocation size! */
      char* pixels = g_malloc(w_int * h_int * 4);
      glReadPixels(x, stage_height - y - h, w, h, GL_RGBA, GL_UNSIGNED_BYTE, pixels);

      CHECKGL( glPixelStorei(GL_UNPACK_ALIGNMENT, 1) );
      CHECKGL( glPixelStorei(GL_UNPACK_ROW_LENGTH, 0) );
      CHECKGL( glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0) );
      CHECKGL( glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0) );
      CHECKGL( glPixelStorei(GL_UNPACK_SKIP_ROWS, 0) );

      CHECKGL (glActiveTextureARB(GL_TEXTURE0) );
      CHECKGL( glBindTexture(GL_TEXTURE_2D, priv->blurred_background_texture) );
      CHECKGL (glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels));

      g_free(pixels);
      priv->refresh_background_texture = FALSE;

      // Disable Scissoring
      CHECKGL (glDisable (GL_SCISSOR_TEST));

      CtkEffectContext* fxctx = ctk_effect_context_get_default_for_actor (actor);

      /* Get the current render target */
      CtkRenderTarget* top_rt = ctk_effect_context_peek_render_target(fxctx);

      /* reserve 2 render targets */
      CtkRenderTarget* rt0 = ctk_effect_context_grab_render_target (fxctx);
      CtkRenderTarget* rt1 = ctk_effect_context_grab_render_target (fxctx);

      /* resize the render targets */
      ctk_render_target_resize (rt0, w, h);
      ctk_render_target_resize (rt1, w, h);

      ctk_render_target_bind (rt0);
      CHECKGL( glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT) );

      /* Copy the background texture into the first render target */
      /* Inverse texture coordinates to put the texture in the right coordinates for the rendering:
          use (0, h, w, -h) instead of (0, 0, w, h)
      */
      ctk_render_quad_asm(priv->blurred_background_texture, w, h, g_shTexture_asm,
                         w, h, 0, h, w, -h,
                         1.0f, 1.0f, 1.0f, 1.0f);

      /* Blur the render targets */
      int iPass = 0;
      for (iPass = 0; iPass < 2; iPass++)
    	  {
          ctk_render_target_bind(rt1);
          CHECKGL( glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT) );
    	  	opengl_blur_pass (ctk_render_target_get_color_buffer_ogl_id(rt0),
            ctk_render_target_get_width(rt0),
            ctk_render_target_get_height(rt0),
            0.0f,
            TRUE,
            w,
            h,
            TRUE);

          ctk_render_target_bind(rt0);
          CHECKGL( glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT) );
          opengl_blur_pass (ctk_render_target_get_color_buffer_ogl_id(rt1),
            ctk_render_target_get_width(rt1),
            ctk_render_target_get_height(rt1),
    		    0.0f,
            FALSE,
            w,
   			    h,
            FALSE);
    		}

      /* Copy the blurred texture back to priv->blurred_background_texture */
      ctk_copy_render_target_to_cached_texture_asm(fxctx, rt0, priv->blurred_background_texture);

      /* Release the render target we use */
      ctk_effect_context_release_render_target (fxctx, rt0);
      ctk_effect_context_release_render_target (fxctx, rt1);

      /* Restore the previous render target */
      if(top_rt)
        {
          ctk_render_target_bind(top_rt);
          CHECKGL( glViewport(0, 0, ctk_render_target_get_width(top_rt), ctk_render_target_get_height(top_rt)) );
          CHECKGL( glScissor(0, 0, ctk_render_target_get_width(top_rt), ctk_render_target_get_height(top_rt)) );
        }
      else
        {
          /* There is no render target on the stack. Set back the regular frame buffer */
          ctk_render_target_unbind(); /* unbind whatever render target is binded */
          CHECKGL( glViewport(0, 0, stage_width, stage_height) );
          CHECKGL( glScissor(0, 0, stage_width, stage_height) );
        }
    }

  if (!ctk_actor_get_effects_painting (ctk_actor)
      && (effects = ctk_actor_get_effects (ctk_actor)))
    {
      GSList *e;

      ctk_actor_set_effects_painting (ctk_actor, TRUE);

      for (e = effects; e; e = e->next)
        {
          gboolean last_effect = e->next ? FALSE : TRUE;
          ctk_effect_paint (e->data, ctk_menu_paint, last_effect);
        }

      ctk_actor_set_effects_painting (ctk_actor, FALSE);
    }
  else
    {
      GList *c;
      clutter_actor_get_allocation_box (actor, &box);

      if (priv->background != NULL)
        {
          clutter_actor_paint (priv->background);
        }

      /* Now draw the children */
      for (c = priv->children; c; c = c->next)
        {
          ClutterActor *child = c->data;
          if (CLUTTER_ACTOR_IS_VISIBLE (child))
            clutter_actor_paint (child);
        }
    }
}

static void
ctk_menu_pick (ClutterActor       *actor,
               const ClutterColor *color)
{
  /* Chain up so we get a bounding box pained (if we are reactive) */
  CLUTTER_ACTOR_CLASS (ctk_menu_parent_class)->pick (actor, color);

  /* Just forward to the paint call which in turn will trigger
   * the child actors also getting 'picked'.
   */
  CtkMenuPrivate *priv;
  GList *c;

  g_return_if_fail (CTK_IS_MENU (actor));
  priv = CTK_MENU (actor)->priv;

  for (c = priv->children; c; c = c->next)
    {
      ClutterActor *child = c->data;
      ctk_actor_set_effects_painting (CTK_ACTOR (actor), TRUE);
      clutter_actor_paint (child);
      ctk_actor_set_effects_painting (CTK_ACTOR (actor), FALSE);
    }

}


static void
ctk_menu_set_property (GObject      *object,
                      guint         prop_id,
                      const GValue *value,
                      GParamSpec   *pspec)
{
  CtkMenu *menu = CTK_MENU (object);

  switch (prop_id)
    {
    case PROP_SPACING:
      ctk_menu_set_spacing (menu, g_value_get_int (value));
      break;

    case PROP_BACKGROUND:
      ctk_menu_set_background (menu, CLUTTER_ACTOR (g_value_get_object (value)));
      break;

    case PROP_CLOSE_ON_LEAVE:
      ctk_menu_set_close_on_leave (menu, g_value_get_boolean (value));
      break;

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

static void
ctk_menu_get_property (GObject    *object,
                      guint       prop_id,
                      GValue     *value,
                      GParamSpec *pspec)
{
  CtkMenu *menu = CTK_MENU (object);

  switch (prop_id)
    {
    case PROP_SPACING:
      g_value_set_int (value, ctk_menu_get_spacing (menu));
      break;

    case PROP_BACKGROUND:
      g_value_set_object (value, ctk_menu_get_background (menu));
      break;

    case PROP_NUM_ITEMS:
      g_value_set_int (value, ctk_menu_get_num_items (menu));
      break;

    case PROP_CLOSE_ON_LEAVE:
      g_value_set_boolean (value, ctk_menu_get_close_on_leave (menu));
      break;

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


static void
ctk_menu_get_preferred_width (ClutterActor *actor,
                             gfloat   for_height,
                             gfloat  *minimum_width,
                             gfloat  *natural_width)
{
  CtkMenu        *menu = CTK_MENU (actor);
  CtkMenuPrivate *priv = menu->priv;
  ClutterActor   *child;
  GList          *children;
  GList          *c;
  gint           nvis_children;
  CtkPadding     padding;

  ctk_actor_get_padding (CTK_ACTOR (actor), &padding);

  if (minimum_width)
    *minimum_width = 0;
  if (natural_width)
    *natural_width = 0;
  nvis_children = 0;

  gfloat pmin_width = 0;
  gfloat pnat_width = 0;

  children = priv->children;

  for (c = children; c; c = c->next)
    {
      gfloat cmin_width = 0;
      gfloat cnat_width = 0;

      child = c->data;
      clutter_actor_get_preferred_width (CLUTTER_ACTOR (child), for_height,
                                         &cmin_width, &cnat_width);

      pmin_width = MAX (pmin_width, cmin_width);
      pnat_width = MAX (pnat_width, cnat_width);
    }
  pmin_width += padding.left + padding.right;
  pnat_width += padding.left + padding.right;

  if (minimum_width)
    *minimum_width = pmin_width;
  if (natural_width)
    *natural_width = pnat_width;
}

static void
ctk_menu_get_preferred_height (ClutterActor *actor,
                              gfloat   for_width,
                              gfloat  *minimum_height,
                              gfloat  *natural_height)
{
  CtkMenu        *menu = CTK_MENU (actor);
  CtkMenuPrivate *priv = menu->priv;
  ClutterActor   *child;
  GList          *children;
  GList          *c;
  gint           nvis_children;
  gfloat         spacing;
  CtkPadding     padding;

  ctk_actor_get_padding (CTK_ACTOR (actor), &padding);
  spacing = (gfloat) (priv->spacing);

  if (minimum_height)
    *minimum_height = 0;
  if (natural_height)
    *natural_height = 0;
  nvis_children = 0;

  children = priv->children;

  gfloat all_child_height = 0;
  gfloat height = 0;

  for (c = children; c; c = c->next)
    {
      child = c->data;
      height = 0.0f;
      clutter_actor_get_preferred_height (CLUTTER_ACTOR (child),
                                          for_width, &height, NULL);

      all_child_height += height + spacing;
    }

  if (minimum_height)
    *minimum_height = all_child_height + padding.top + padding.bottom;
  if (natural_height)
    *natural_height = all_child_height + padding.top + padding.bottom;

}


static void
ctk_menu_allocate (ClutterActor          *actor,
                  const ClutterActorBox *box,
                  ClutterAllocationFlags flags)
{
  CtkMenuPrivate    *priv = CTK_MENU (actor)->priv;
  CtkPadding        padding;
  GList             *c;
  gint              n_children = 0;
  gfloat            width;
  gfloat            height;
  gfloat            spacing;
  gfloat            stage_height;
  gfloat            actor_y;
  gfloat            special_y, special_height;
  ClutterActorBox   *actor_box;
  gfloat             attached_actor_height;
  gfloat            delta_width, delta_height;
  gfloat            delta_y1, delta_y2;

  special_y = special_height = 0;
  actor_box = clutter_actor_box_copy (box);
  ctk_actor_get_padding (CTK_ACTOR (actor), &padding);
  width = (box->x2 - box->x1) - padding.left - padding.right;
  height = (box->y2 - box->y1) - padding.top - padding.bottom;
  spacing = (gfloat) (priv->spacing);
  clutter_actor_box_get_size (actor_box, &delta_width, &delta_height);

  for (c = priv->children; c; c = c->next)
    {
      n_children++;
    }

  /* Work out the size of the children first */
  gfloat x, y;
  y = padding.top;
  for (c = priv->children; c; c = c->next)
    {
      ClutterActor *child = c->data;
      gfloat min_width;
      gfloat min_height;
      gfloat child_height;

      clutter_actor_get_preferred_width (child,
                                         box->y2 - box->y1,
                                         &min_width, NULL);
      clutter_actor_get_preferred_height (child,
                                          box->x2 - box->x1,
                                          &min_height, NULL);

      clutter_actor_set_size (child, width, min_height);
      child_height = clutter_actor_get_height (child);

      // if we have a "special" widget, we need to move ourselfs so that the
      // special widget is aligned with the origin (as best as possible)
      if (priv->special_child == child)
        {
          special_y = y;
          special_height = child_height;
        }

      y += child_height + spacing;
    }

  /* Now we do the positioning */

  // modify the position of the menu so that the special child is aligned
  // with the origin
  gfloat pad = 0;
  if (CLUTTER_IS_ACTOR (priv->attached_actor))
    {
      attached_actor_height = clutter_actor_get_height (
                                          CLUTTER_ACTOR (priv->attached_actor));
      pad = (attached_actor_height - special_height) * 0.5;
    }
  actor_box->y1 = actor_box->y1 - special_y + pad;
  actor_box->y2 = actor_box->y2 - special_y + pad;

  // its only at this point that we know if we are too big to fit on the screen
  // if we are, we need to move
  stage_height = clutter_actor_get_height (clutter_actor_get_stage (actor));
  actor_y = floor(actor_box->y1);
  if (actor_y + floor (clutter_actor_box_get_height (actor_box)) > stage_height)
    {
      float mod_pos = actor_y + clutter_actor_box_get_height (box) - stage_height;
      actor_box->y1 = actor_box->y1 - mod_pos;
      actor_box->y2 = actor_box->y2 - mod_pos;
    }

  // check top edge
  if (actor_y < 12)
    {
      float mod_pos = 12 - actor_y;
      actor_box->y1 = actor_box->y1 + mod_pos;
      actor_box->y2 = actor_box->y2 + mod_pos;
    }

  CLUTTER_ACTOR_CLASS (ctk_menu_parent_class)->allocate (actor, actor_box, flags);

  // if is_transforming is true, we need to calculate a new width/height
  delta_y1 = 0;
  delta_y2 = clutter_actor_box_get_height (actor_box);
  if (priv->is_transforming)
    {
      gfloat delta = clutter_timeline_get_progress (priv->transform_timeline);
      gfloat full_height, full_width;
      clutter_actor_box_get_size (actor_box, &full_width, &full_height);
      delta_width = priv->previous_width + ((full_width - priv->previous_width) * delta);

      delta_y1 = (priv->previous_y - actor_box->y1) * delta;
      delta_y1 = (priv->previous_y - actor_box->y1) - delta_y1;
      delta_y2 = (priv->previous_y - actor_box->y1) + priv->previous_height;
      delta_y2 = delta_y2 + ((full_height - delta_y2) * delta);
    }

  if (priv->background)
    {
      //background actor is always the 'size' of our menu
      ClutterActorBox child_box;
      child_box.x1 = 0;
      child_box.y1 = floor (delta_y1);//actor_box->y1 + delta_height;
      child_box.x2 = floor (child_box.x1 + delta_width);
      child_box.y2 = floor (delta_y2); //child_box.y1 + delta_height;
      clutter_actor_allocate (priv->background, &child_box, flags);
    }


  // Allocate each of the child items
  x = padding.left;
  y = padding.top;

  for (c = priv->children; c; c = c->next)
    {
      ClutterActor *child = c->data;
      ClutterActorBox   child_box;
      gfloat       child_width;
      gfloat       child_height;

      clutter_actor_get_allocation_box (child, &child_box);
      child_width = clutter_actor_get_width (child);
      child_height = clutter_actor_get_height (child);

      child_box.x1 = floor (x);
      child_box.x2 = floor (x + child_width);
      child_box.y1 = floor (y);
      child_box.y2 = floor (y + child_height);
      y += child_height + spacing;

      clutter_actor_allocate (child, &child_box, flags);

      // we need to clip our children if they are outside our child box
      if ((child_box.y2 < delta_y1) || (child_box.y1 > delta_y2))
        {
          clutter_actor_hide (child);
        }
      else
        {
          if (!CLUTTER_ACTOR_IS_VISIBLE (child))
            {
              clutter_actor_show (child);
            }
          gfloat offx, offy = 0;
          gfloat cwidth = delta_width - padding.right;
          gfloat cheight = child_height + spacing;

          if ((child_box.y2 > delta_y1) && (child_box.y1 < delta_y1))
            {
              //need a clip on the top item
              offx = 0;
              offy = child_height - (child_box.y2 - delta_y1);
              cheight = child_height;


            }
          else if ((child_box.y1 < delta_y2) && (child_box.y2 > delta_y2))
            {
              //need a clip on the bottom item
              offx = 0;
              offy = 0;
              cheight = delta_y2 - child_box.y1;
            }
          clutter_actor_set_clip (child,
                                  floor (offx),
                                  floor (offy),
                                  floor (cwidth),
                                  floor (cheight));
        }

    }
}

static void
ctk_menu_class_init (CtkMenuClass *klass)
{
  GObjectClass* object_class = G_OBJECT_CLASS (klass);
  ClutterActorClass *act_class = CLUTTER_ACTOR_CLASS (klass);
  GParamSpec        *pspec;

  object_class->finalize = ctk_menu_finalize;
  object_class->set_property = ctk_menu_set_property;
  object_class->get_property = ctk_menu_get_property;

  act_class->paint                = ctk_menu_paint;
  act_class->get_preferred_width  = ctk_menu_get_preferred_width;
  act_class->get_preferred_height = ctk_menu_get_preferred_height;
  act_class->allocate             = ctk_menu_allocate;
  act_class->pick                 = ctk_menu_pick;

  pspec = g_param_spec_int ("spacing", "spacing",
                            "The amount of space between the children",
                            0, G_MAXINT, 0, G_PARAM_READWRITE);
  g_object_class_install_property (object_class, PROP_SPACING, pspec);


  pspec = g_param_spec_object ("background", "background",
                               "The background for the menu as a ClutterActor",
                               CLUTTER_TYPE_ACTOR, G_PARAM_READWRITE);
  g_object_class_install_property (object_class, PROP_BACKGROUND, pspec);

  pspec = g_param_spec_int ("num-items",
                            "num-items",
                            "Number of menu-items",
                            0,
                            G_MAXINT,
                            0,
                            G_PARAM_READABLE);
  g_object_class_install_property (object_class, PROP_NUM_ITEMS, pspec);

  pspec = g_param_spec_boolean ("close-on-leave", "close-on-leave",
                                "If set to true, will close the menu when you mouse off it",
                                FALSE, G_PARAM_READWRITE);
  g_object_class_install_property (object_class, PROP_CLOSE_ON_LEAVE, pspec);

  _menu_signals[CLOSED] =
    g_signal_new ("closed",
                  G_OBJECT_CLASS_TYPE (object_class),
                  G_SIGNAL_RUN_LAST,
                  G_STRUCT_OFFSET (CtkMenuClass, closed),
                  NULL, NULL,
                  g_cclosure_marshal_VOID__VOID,
                  G_TYPE_NONE, 0);


  g_type_class_add_private (klass, sizeof (CtkMenuPrivate));
}

static void
ctk_menu_add (ClutterContainer *container,
              ClutterActor     *actor)
{
  g_return_if_fail (CTK_IS_ACTOR (actor));

  ctk_menu_append (CTK_MENU (container), actor, FALSE);
}

static void
ctk_menu_foreach (ClutterContainer *container,
                 ClutterCallback   callback,
                 gpointer          user_data)
{
  CtkMenuPrivate *priv;
  GList *c;

  g_return_if_fail (CTK_IS_MENU (container));
  priv = CTK_MENU (container)->priv;

  for (c = priv->children; c; c = c->next)
    {
      ClutterActor *child = c->data;
      (* callback) (child, user_data);
    }
}

static void
ctk_menu_foreach_with_internals (ClutterContainer *container,
                                ClutterCallback   callback,
                                gpointer          user_data)
{
  CtkMenuPrivate *priv;
  GList *c;

  g_return_if_fail (CTK_IS_MENU (container));
  priv = CTK_MENU (container)->priv;

  for (c = priv->children; c; c = c->next)
    {
      ClutterActor *child = c->data;
      (* callback) (child, user_data);
    }
  if (CLUTTER_IS_ACTOR (priv->background))
    {
      (* callback) (priv->background, user_data);
    }
}

static void
ctk_menu_raise (ClutterContainer *container,
               ClutterActor     *actor,
               ClutterActor     *sibling)
{
  /* Null op */
}

static void
ctk_menu_lower (ClutterContainer *container,
               ClutterActor     *actor,
               ClutterActor     *sibling)
{
  /* Null op */
}

static void
ctk_menu_sort_depth_order (ClutterContainer *container)
{
  /* Null op */
}

/* Private methods */
static void
transform_new_frame (ClutterTimeline *timeline, gint msecs, CtkMenu *menu)
{
  g_return_if_fail (CLUTTER_IS_TIMELINE (timeline));

  if (!CTK_IS_MENU (menu))
    {
      clutter_timeline_stop (timeline);
      g_object_unref (timeline);
      return;
    }

  clutter_actor_queue_relayout (CLUTTER_ACTOR (menu));
}

static void
transform_completed (ClutterTimeline *timeline, CtkMenu *menu)
{
  g_return_if_fail (CTK_IS_MENU (menu));
  g_return_if_fail (CLUTTER_IS_TIMELINE (timeline));
  menu->priv->is_transforming = FALSE;
}

static gboolean
ctk_menu_do_transform_anim (CtkMenu *menu)
{
  g_return_val_if_fail (CTK_IS_MENU (menu), FALSE);
  if (CLUTTER_IS_TIMELINE (menu->priv->transform_timeline))
    {
      clutter_timeline_stop (menu->priv->transform_timeline);
    }
  else
    {
      menu->priv->transform_timeline = clutter_timeline_new (1); //replace with 150
      g_signal_connect (menu->priv->transform_timeline, "new-frame",
                        (GCallback)(transform_new_frame), menu);
      g_signal_connect (menu->priv->transform_timeline, "completed",
                        (GCallback)(transform_completed), menu);
    }

  clutter_timeline_start (menu->priv->transform_timeline);
  menu->priv->transform_queue_handler = 0;
  menu->priv->is_transforming = FALSE;
  return FALSE;
}

static void
ctk_menu_queue_transform (CtkMenu *menu)
{
  ClutterActorBox box;
  // queues up a transform from one size to another
  g_return_if_fail (CTK_IS_MENU (menu));
  if (menu->priv->transform_queue_handler != 0)
    {
      return;
    }

  ctk_actor_get_stored_allocation (CTK_ACTOR (menu), &box);
  clutter_actor_box_get_size (&box,
                              &menu->priv->previous_width,
                              &menu->priv->previous_height);
  clutter_actor_get_position (CLUTTER_ACTOR (menu),
                              &menu->priv->previous_x,
                              &menu->priv->previous_y);

  menu->priv->transform_queue_handler = g_idle_add ((GSourceFunc)(ctk_menu_do_transform_anim),
                                                    menu);

 }

static void
ctk_menu_stick_to_actor (CtkMenu *menu, CtkActor *actor)
{
  g_return_if_fail (CTK_IS_MENU (menu));
  g_return_if_fail (CTK_IS_ACTOR (actor));

  ClutterActorBox box;
  gfloat x, y, w, h;

  ctk_actor_get_stored_allocation (actor, &box);
  w = clutter_actor_box_get_width (&box);
  h = clutter_actor_box_get_height (&box);
  clutter_actor_get_position (CLUTTER_ACTOR (actor), &x, &y);

  clutter_actor_set_position (CLUTTER_ACTOR (menu),
                              x + w, y + (h/2.0f));
}

static void
ctk_menu_notify_on_attached_allocation (GObject* _sender,
                                        GParamSpec* pspec,
                                        CtkMenu *menu)
{
  g_return_if_fail (CTK_IS_ACTOR (_sender));

  ctk_menu_stick_to_actor (menu, CTK_ACTOR (_sender));
}


/* constructors */
CtkMenu *
ctk_menu_new (void)
{
  CtkMenu *menu;

  ClutterColor color = {0x52, 0x51, 0x4d, 0xff};
  ClutterActor *base_background = clutter_rectangle_new_with_color (&color);

  menu = g_object_new (CTK_TYPE_MENU,
                       NULL);

  ctk_menu_set_background (menu, base_background);
  return menu;
}

CtkMenu *
ctk_menu_new_with_background (ClutterActor *background)
{
  CtkMenu *menu;
  menu = g_object_new (CTK_TYPE_MENU,
                       NULL);

  ctk_menu_set_background (menu, background);
  return menu;
}

void
ctk_menu_append (CtkMenu *self, ClutterActor *item, gboolean is_special)
{
  CtkMenuPrivate *priv;

  g_return_if_fail (CTK_IS_MENU (self));
  g_return_if_fail (CLUTTER_IS_ACTOR (item));
  g_return_if_fail (clutter_actor_get_parent (item) == NULL);
  priv = self->priv;

  g_object_ref (item);

  priv->children = g_list_append (priv->children, item);
  clutter_actor_set_parent (item, CLUTTER_ACTOR (self));

  if (is_special)
    {
      priv->special_child = item;
    }
  if (g_list_length (priv->children) > 1)
    {
      ctk_menu_queue_transform (self);
    }

  /* queue a relayout, to get the correct positioning inside
   * the ::actor-added signal handlers
   */
  clutter_actor_queue_relayout (CLUTTER_ACTOR (self));

}

void
ctk_menu_prepend (CtkMenu *self, ClutterActor *item, gboolean is_special)
{
  CtkMenuPrivate *priv;

  g_return_if_fail (CTK_IS_MENU (self));
  g_return_if_fail (CLUTTER_IS_ACTOR (item));
  g_return_if_fail (clutter_actor_get_parent (item) == NULL);
  priv = self->priv;

  g_object_ref (item);

  priv->children = g_list_prepend (priv->children, item);
  clutter_actor_set_parent (item, CLUTTER_ACTOR (self));
  if (is_special)
    {
      priv->special_child = item;
    }
  if (g_list_length (priv->children) > 1)
    {
      ctk_menu_queue_transform (self);
    }


  /* queue a relayout, to get the correct positioning inside
   * the ::actor-added signal handlers
   */
  clutter_actor_queue_relayout (CLUTTER_ACTOR (self));

  g_object_unref (item);
}

static void
ctk_menu_remove (ClutterContainer *container, ClutterActor *actor)
{
  CtkMenu *menu = CTK_MENU (container);
  CtkMenuPrivate *priv = menu->priv;
  g_return_if_fail (CTK_IS_MENU (menu));
  g_return_if_fail (CLUTTER_IS_ACTOR (actor));
  g_return_if_fail (clutter_actor_get_parent (actor) == CLUTTER_ACTOR (menu));

  priv->children = g_list_remove (priv->children, actor);
  clutter_actor_unparent (actor);
  clutter_actor_queue_relayout (CLUTTER_ACTOR (menu));
  g_signal_emit_by_name (menu, "actor-removed", actor);

  if (priv->special_child == actor)
    {
      priv->special_child = NULL;
    }
}



void
ctk_menu_remove_all (CtkMenu *self)
{
  CtkMenuPrivate *priv = self->priv;
  g_return_if_fail (CTK_IS_MENU (self));

  while (priv->children)
    {
      ClutterActor *child = CLUTTER_ACTOR (priv->children->data);
      if (CLUTTER_IS_ACTOR (child))
        {
          ctk_menu_remove (CLUTTER_CONTAINER (self), child);
        }
    }
}


void
ctk_menu_set_spacing (CtkMenu *menu,
                      gint spacing)
{
  g_return_if_fail (CTK_IS_MENU (menu));

  if (spacing != menu->priv->spacing)
    {
      menu->priv->spacing = spacing;

      g_object_notify (G_OBJECT (menu), "spacing");

      clutter_actor_queue_relayout (CLUTTER_ACTOR (menu));
    }
}

gint
ctk_menu_get_spacing (CtkMenu *menu)
{
  g_return_val_if_fail (CTK_IS_MENU (menu), 0);
  return menu->priv->spacing;
}

gint
ctk_menu_get_num_items (CtkMenu *menu)
{
  g_return_val_if_fail (CTK_IS_MENU (menu), 0);

  if (!menu->priv->children)
    return 0;

  return (gint) g_list_length (menu->priv->children);
}

void
ctk_menu_attach_to_actor (CtkMenu *menu, CtkActor *actor)
{
  g_return_if_fail (CTK_IS_ACTOR (actor));
  g_return_if_fail (CTK_IS_MENU (menu));

  if (menu->priv->attached_actor_handle_id &&
      CTK_IS_ACTOR (menu->priv->attached_actor))
  {
    g_signal_handler_disconnect (menu->priv->attached_actor,
                                 menu->priv->attached_actor_handle_id);
  }

  menu->priv->attached_actor = actor;
  menu->priv->attached_actor_handle_id = g_signal_connect (
                            actor, "notify::allocation",
                            G_CALLBACK (ctk_menu_notify_on_attached_allocation),
                            menu);

  ctk_menu_stick_to_actor (menu, actor);
}

CtkActor *
ctk_menu_get_attached_actor (CtkMenu *menu)
{
  g_return_val_if_fail (CTK_IS_MENU (menu), NULL);
  g_return_val_if_fail (CTK_IS_ACTOR (menu->priv->attached_actor), NULL);

  return menu->priv->attached_actor;
}


void
ctk_menu_set_background (CtkMenu *menu, ClutterActor *actor)
{
  g_return_if_fail (CTK_IS_MENU (menu));
  g_return_if_fail (CLUTTER_IS_ACTOR (actor));
  CtkMenuPrivate *priv = menu->priv;

  if (CLUTTER_IS_ACTOR (priv->background))
  {
    clutter_actor_unparent (priv->background);
    priv->background = NULL;
  }

  clutter_actor_set_parent (actor, CLUTTER_ACTOR (menu));
  priv->background = actor;
  clutter_actor_queue_relayout (CLUTTER_ACTOR (menu));
}

ClutterActor *
ctk_menu_get_background (CtkMenu *menu)
{
  g_return_val_if_fail (CTK_IS_MENU (menu), NULL);
  return menu->priv->background;
}

void
ctk_menu_set_detect_clicks (CtkMenu *menu, gboolean value)
{
  g_return_if_fail (CTK_IS_MENU (menu));
  CtkMenuPrivate *priv = menu->priv;
  ClutterActor *actor = CLUTTER_ACTOR (menu);

  priv->detect_clicks = value;
  if (clutter_actor_get_stage (actor) == NULL)
    return;

  if (!priv->detect_clicks && priv->stage_handle_id > 0)
    {
      // false detect clicks but we have a stage_handler, so disconnect it
      g_signal_handler_disconnect (menu->priv->attached_stage,
                                   priv->stage_handle_id);
      priv->stage_handle_id = 0;
    }

  if (priv->detect_clicks && !priv->stage_handle_id)
    {
      // true detect_clicks but no stage handler is set, so set one up
      priv->attached_stage = CLUTTER_STAGE (clutter_actor_get_stage (actor));
      priv->stage_handle_id = g_signal_connect (clutter_actor_get_stage (actor),
                                                "captured-event",
                                                G_CALLBACK (handle_stage_events),
                                                actor);
    }
}

void
ctk_menu_set_swallow_clicks (CtkMenu *menu, gboolean value)
{
  CtkMenuPrivate *priv;

  g_return_if_fail (CTK_IS_MENU (menu));

  priv = menu->priv;
  priv->swallow_clicks = value;
}

void
ctk_menu_refresh_background_texture (CtkMenu *self)
{
  CtkMenuPrivate *priv;

  g_return_if_fail (CTK_IS_MENU (self));

  priv = CTK_MENU_GET_PRIVATE(self);
  priv->refresh_background_texture = TRUE;
}

guint
ctk_menu_get_framebuffer_background (CtkMenu *self)
{
  CtkMenuPrivate *priv;

  g_return_val_if_fail (CTK_IS_MENU (self), 0);

  priv = CTK_MENU_GET_PRIVATE(self);
  return priv->blurred_background_texture;
}

gboolean
ctk_menu_get_close_on_leave (CtkMenu *self)
{
  g_return_val_if_fail (CTK_IS_MENU (self), FALSE);
  return self->priv->close_on_leave;
}

void
ctk_menu_set_close_on_leave (CtkMenu *self, gboolean value)
{
  g_return_if_fail (CTK_IS_MENU (self));
  self->priv->close_on_leave = value;
}


