#include <glib-object.h>
#include <gio/gio.h>
#include <clutter/clutter.h>

/* {{{ MenuItemModel */

/* This is our "model" of a Menu item; it has a "label" property, and
 * a "selected" state property. The user is supposed to operate on the
 * model instance, and change its state.
 */

#define EXAMPLE_TYPE_MENU_ITEM_MODEL (example_menu_item_model_get_type ())

G_DECLARE_FINAL_TYPE (ExampleMenuItemModel, example_menu_item_model, EXAMPLE, MENU_ITEM_MODEL, GObject)

struct _ExampleMenuItemModel
{
  GObject parent_instance;

  char *label;

  gboolean selected;
};

struct _ExampleMenuItemModelClass
{
  GObjectClass parent_class;
};

enum {
  MENU_ITEM_MODEL_PROP_LABEL = 1,
  MENU_ITEM_MODEL_PROP_SELECTED,
  MENU_ITEM_MODEL_N_PROPS
};

static GParamSpec *menu_item_model_props[MENU_ITEM_MODEL_N_PROPS] = { NULL, };

G_DEFINE_TYPE (ExampleMenuItemModel, example_menu_item_model, G_TYPE_OBJECT)

static void
example_menu_item_model_finalize (GObject *gobject)
{
  ExampleMenuItemModel *self = (ExampleMenuItemModel *) gobject;

  free (self->label);

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

static void
example_menu_item_model_set_property (GObject      *gobject,
                                      guint         prop_id,
                                      const GValue *value,
                                      GParamSpec   *pspec)
{
  ExampleMenuItemModel *self = (ExampleMenuItemModel *) gobject;

  switch (prop_id)
    {
    case MENU_ITEM_MODEL_PROP_LABEL:
      free (self->label);
      self->label = g_value_dup_string (value);
      break;

    case MENU_ITEM_MODEL_PROP_SELECTED:
      self->selected = g_value_get_boolean (value);
      break;
    }
}

static void
example_menu_item_model_get_property (GObject    *gobject,
                                      guint       prop_id,
                                      GValue     *value,
                                      GParamSpec *pspec)
{
  ExampleMenuItemModel *self = (ExampleMenuItemModel *) gobject;

  switch (prop_id)
    {
    case MENU_ITEM_MODEL_PROP_LABEL:
      g_value_set_string (value, self->label);
      break;

    case MENU_ITEM_MODEL_PROP_SELECTED:
      g_value_set_boolean (value, self->selected);
      break;
    }
}

static void
example_menu_item_model_class_init (ExampleMenuItemModelClass *klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);

  gobject_class->set_property = example_menu_item_model_set_property;
  gobject_class->get_property = example_menu_item_model_get_property;
  gobject_class->finalize = example_menu_item_model_finalize;

  menu_item_model_props[MENU_ITEM_MODEL_PROP_LABEL] =
    g_param_spec_string ("label", NULL, NULL,
                         NULL,
                         G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
  menu_item_model_props[MENU_ITEM_MODEL_PROP_SELECTED] =
    g_param_spec_boolean ("selected", NULL, NULL,
                          FALSE,
                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
  g_object_class_install_properties (gobject_class, MENU_ITEM_MODEL_N_PROPS, menu_item_model_props);
}

static void
example_menu_item_model_init (ExampleMenuItemModel *self)
{
}
/* }}} */

/* {{{ MenuItemView */

/* This is our "view" of a Menu item; it changes state depending on whether
 * the "selected" property is set. The "view" reflects the state of the
 * "model" instance, though it has no direct connection to it.
 */
#define EXAMPLE_TYPE_MENU_ITEM_VIEW (example_menu_item_view_get_type ())

G_DECLARE_FINAL_TYPE (ExampleMenuItemView, example_menu_item_view, EXAMPLE, MENU_ITEM_VIEW, ClutterText)

struct _ExampleMenuItemView
{
  ClutterText parent_instance;

  gboolean is_selected;
};

struct _ExampleMenuItemViewClass
{
  ClutterTextClass parent_class;
};

G_DEFINE_TYPE (ExampleMenuItemView, example_menu_item_view, CLUTTER_TYPE_TEXT)

enum {
  MENU_ITEM_VIEW_PROP_SELECTED = 1,
  MENU_ITEM_VIEW_N_PROPS
};

static GParamSpec *menu_item_view_props[MENU_ITEM_VIEW_N_PROPS] = { NULL, };

static void
example_menu_item_view_set_selected (ExampleMenuItemView *self,
                                     gboolean             selected)
{
  selected = !!selected;
  if (self->is_selected == selected)
    return;

  self->is_selected = selected;

  if (self->is_selected)
    clutter_text_set_color (CLUTTER_TEXT (self), CLUTTER_COLOR_LightSkyBlue);
  else
    clutter_text_set_color (CLUTTER_TEXT (self), CLUTTER_COLOR_White);

  g_object_notify_by_pspec (G_OBJECT (self), menu_item_view_props[MENU_ITEM_VIEW_PROP_SELECTED]);
}

static void
example_menu_item_view_set_property (GObject      *gobject,
                                     guint         prop_id,
                                     const GValue *value,
                                     GParamSpec   *pspec)
{
  switch (prop_id)
    {
    case MENU_ITEM_VIEW_PROP_SELECTED:
      example_menu_item_view_set_selected (EXAMPLE_MENU_ITEM_VIEW (gobject),
                                           g_value_get_boolean (value));
      break;
    }
}

static void
example_menu_item_view_get_property (GObject    *gobject,
                                     guint       prop_id,
                                     GValue     *value,
                                     GParamSpec *pspec)
{
  switch (prop_id)
    {
    case MENU_ITEM_VIEW_PROP_SELECTED:
      g_value_set_boolean (value, EXAMPLE_MENU_ITEM_VIEW (gobject)->is_selected);
      break;
    }
}

static void
example_menu_item_view_class_init (ExampleMenuItemViewClass *klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);

  gobject_class->set_property = example_menu_item_view_set_property;
  gobject_class->get_property = example_menu_item_view_get_property;

  menu_item_view_props[MENU_ITEM_VIEW_PROP_SELECTED] =
    g_param_spec_boolean ("selected", NULL, NULL,
                          FALSE,
                          G_PARAM_READWRITE |
                          G_PARAM_STATIC_STRINGS);

  g_object_class_install_properties (gobject_class, MENU_ITEM_VIEW_N_PROPS, menu_item_view_props);
}

static void
example_menu_item_view__transition_stopped (ClutterActor *actor,
                                            const char   *transition,
                                            gboolean      is_finished)
{
  clutter_actor_set_scale (actor, 1.0, 1.0);
  clutter_actor_set_opacity (actor, 255);
}

static void
example_menu_item_view_init (ExampleMenuItemView *self)
{
  ClutterText *text = CLUTTER_TEXT (self);
  ClutterActor *actor = CLUTTER_ACTOR (self);
  ClutterTransition *scalex_trans, *scaley_trans, *fade_trans;
  ClutterTransition *group;

  clutter_text_set_font_name (text, "Sans Bold 24px");
  clutter_text_set_color (text, CLUTTER_COLOR_White);

  clutter_actor_set_margin_left (actor, 12);
  clutter_actor_set_margin_right (actor, 12);

  clutter_actor_set_pivot_point (actor, 0.5, 0.5);

  scalex_trans = clutter_property_transition_new ("scale-x");
  clutter_transition_set_from (scalex_trans, G_TYPE_FLOAT, 1.0);
  clutter_transition_set_to (scalex_trans, G_TYPE_FLOAT, 3.0);

  scaley_trans = clutter_property_transition_new ("scale-y");
  clutter_transition_set_from (scaley_trans, G_TYPE_FLOAT, 1.0);
  clutter_transition_set_to (scaley_trans, G_TYPE_FLOAT, 3.0);

  fade_trans = clutter_property_transition_new ("opacity");
  clutter_transition_set_to (fade_trans, G_TYPE_UINT, 0);

  group = clutter_transition_group_new ();
  clutter_transition_group_add_transition (CLUTTER_TRANSITION_GROUP (group), scalex_trans);
  clutter_transition_group_add_transition (CLUTTER_TRANSITION_GROUP (group), scaley_trans);
  clutter_transition_group_add_transition (CLUTTER_TRANSITION_GROUP (group), fade_trans);
  clutter_timeline_set_duration (CLUTTER_TIMELINE (group), 250);
  clutter_timeline_set_progress_mode (CLUTTER_TIMELINE (group), CLUTTER_EASE_OUT);

  clutter_actor_add_transition (actor, "activateTransition", group);
  g_object_unref (group);

  clutter_timeline_stop (CLUTTER_TIMELINE (group));

  g_signal_connect (actor, "transition-stopped",
                    G_CALLBACK (example_menu_item_view__transition_stopped),
                    group);
}

static void
example_menu_item_view_activate (ExampleMenuItemView *self)
{
  ClutterTransition *t;

  t = clutter_actor_get_transition (CLUTTER_ACTOR (self), "activateTransition");
  clutter_timeline_start (CLUTTER_TIMELINE (t));
}

/* }}} */

/* {{{ Menu */

/* This is our container actor, which binds the GListStore with the
 * ExampleMenuItemModel instances to the ExampleMenuItemView actors
 */

#define EXAMPLE_TYPE_MENU (example_menu_get_type ())

G_DECLARE_FINAL_TYPE (ExampleMenu, example_menu, EXAMPLE, MENU, ClutterActor)

struct _ExampleMenu
{
  ClutterActor parent_instance;

  int current_idx;
};

struct _ExampleMenuClass
{
  ClutterActorClass parent_class;
};

G_DEFINE_TYPE (ExampleMenu, example_menu, CLUTTER_TYPE_ACTOR)

static void
example_menu_class_init (ExampleMenuClass *klass)
{
}

static void
example_menu_init (ExampleMenu *self)
{
  ClutterActor *actor = CLUTTER_ACTOR (self);
  ClutterLayoutManager *layout;

  layout = clutter_box_layout_new ();
  clutter_box_layout_set_orientation (CLUTTER_BOX_LAYOUT (layout), CLUTTER_ORIENTATION_VERTICAL);
  clutter_box_layout_set_spacing (CLUTTER_BOX_LAYOUT (layout), 12);

  clutter_actor_set_layout_manager (actor, layout);
  clutter_actor_set_background_color (actor, CLUTTER_COLOR_Black);

  self->current_idx = -1;
}

static ClutterActor *
example_menu_select_item (ExampleMenu *self,
                          int          idx)
{
  ClutterActor *item;

  /* Any change in the view is reflected into the model */

  if (idx == self->current_idx)
    return clutter_actor_get_child_at_index (CLUTTER_ACTOR (self), self->current_idx);

  item = clutter_actor_get_child_at_index (CLUTTER_ACTOR (self), self->current_idx);
  if (item != NULL)
    example_menu_item_view_set_selected ((ExampleMenuItemView *) item, FALSE);

  if (idx < 0)
    idx = clutter_actor_get_n_children (CLUTTER_ACTOR (self)) - 1;
  else if (idx >= clutter_actor_get_n_children (CLUTTER_ACTOR (self)))
    idx = 0;

  self->current_idx = idx;

  item = clutter_actor_get_child_at_index (CLUTTER_ACTOR (self), self->current_idx);
  if (item != NULL)
    example_menu_item_view_set_selected ((ExampleMenuItemView *) item, TRUE);

  return item;
}

static ClutterActor *
example_menu_select_next (ExampleMenu *self)
{
  return example_menu_select_item (self, self->current_idx + 1);
}

static ClutterActor *
example_menu_select_prev (ExampleMenu *self)
{
  return example_menu_select_item (self, self->current_idx - 1);
}

static void
example_menu_activate_item (ExampleMenu *self)
{
  ClutterActor *child;

  child = clutter_actor_get_child_at_index (CLUTTER_ACTOR (self),
                                            self->current_idx);
  if (child == NULL)
    return;

  example_menu_item_view_activate ((ExampleMenuItemView *) child);
}

/* }}} */

/* {{{ main */
static gboolean
on_key_press (ClutterActor *stage,
              ClutterEvent *event)
{
  ClutterActor *scroll = clutter_actor_get_first_child (stage);
  ClutterActor *menu = clutter_actor_get_first_child (scroll);
  ClutterActor *item = NULL;
  guint key = clutter_event_get_key_symbol (event);
  ClutterPoint p;

  switch (key)
    {
    case CLUTTER_KEY_q:
      clutter_main_quit ();
      break;

    case CLUTTER_KEY_Up:
      item = example_menu_select_prev ((ExampleMenu *) menu);
      clutter_actor_get_position (item, &p.x, &p.y);
      break;

    case CLUTTER_KEY_Down:
      item = example_menu_select_next ((ExampleMenu *) menu);
      clutter_actor_get_position (item, &p.x, &p.y);
      break;

    case CLUTTER_KEY_Return:
    case CLUTTER_KEY_KP_Enter:
      example_menu_activate_item ((ExampleMenu *) menu);
      break;
    }

  if (item != NULL)
    clutter_scroll_actor_scroll_to_point (CLUTTER_SCROLL_ACTOR (scroll), &p);

  return CLUTTER_EVENT_PROPAGATE;
}

static void
on_model_item_selection (GObject    *model_item,
                         GParamSpec *pspec,
                         gpointer    data)
{
  char *label = NULL;
  gboolean is_selected = FALSE;

  g_object_get (model_item, "label", &label, "selected", &is_selected, NULL);

  if (is_selected)
    g_print ("Item '%s' selected!\n", label);

  free (label);
}

static ClutterActor *
create_menu_actor (void)
{
  /* Our store of menu item models */
  GListStore *model = g_list_store_new (EXAMPLE_TYPE_MENU_ITEM_MODEL);
  ClutterActor *menu = g_object_new (EXAMPLE_TYPE_MENU, NULL);
  int i;

  /* Populate the model */
  for (i = 0; i < 12; i++)
    {
      char *label = g_strdup_printf ("Option %02d", i + 1);

      ExampleMenuItemModel *item = g_object_new (EXAMPLE_TYPE_MENU_ITEM_MODEL,
                                                 "label", label,
                                                 NULL);

      g_list_store_append (model, item);

      g_signal_connect (item, "notify::selected",
                        G_CALLBACK (on_model_item_selection),
                        NULL);

      g_object_unref (item);
      free (label);
    }

  /* Bind the list of menu item models to the menu actor; this will
   * create ClutterActor views of each item in the model, and add them
   * to the menu actor
   */
  clutter_actor_bind_model_with_properties (menu, G_LIST_MODEL (model),
                                            EXAMPLE_TYPE_MENU_ITEM_VIEW,
                                            "label", "text", G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE,
                                            "selected", "selected", G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE,
                                            NULL);

  /* We don't need a pointer to the model any more, so we transfer ownership
   * to the menu actor; this means that the model will go away when the menu
   * actor is destroyed
   */
  g_object_unref (model);

  /* Select the first item in the menu */
  example_menu_select_item ((ExampleMenu *) menu, 0);

  return menu;
}

/* The scrolling container for the menu */
static ClutterActor *
create_scroll_actor (void)
{
  ClutterActor *menu = clutter_scroll_actor_new ();
  clutter_actor_set_name (menu, "scroll");
  clutter_scroll_actor_set_scroll_mode (CLUTTER_SCROLL_ACTOR (menu),
                                        CLUTTER_SCROLL_VERTICALLY);
  clutter_actor_set_easing_duration (menu, 250);
  clutter_actor_add_child (menu, create_menu_actor ());

  return menu;
}

int
main (int argc, char *argv[])
{
  ClutterActor *stage, *menu;

  if (clutter_init (&argc, &argv) != CLUTTER_INIT_SUCCESS)
    return 1;

  stage = clutter_stage_new ();
  clutter_stage_set_title (CLUTTER_STAGE (stage), "Actor Model");
  clutter_stage_set_user_resizable (CLUTTER_STAGE (stage), TRUE);
  g_signal_connect (stage, "destroy", G_CALLBACK (clutter_main_quit), NULL);
  g_signal_connect (stage, "key-press-event", G_CALLBACK (on_key_press), NULL);
  clutter_actor_show (stage);

#define PADDING 18.f

  menu = create_scroll_actor ();
  clutter_actor_set_position (menu, 0, PADDING);
  clutter_actor_add_constraint (menu, clutter_align_constraint_new (stage, CLUTTER_ALIGN_X_AXIS, 0.5));
  clutter_actor_add_constraint (menu, clutter_bind_constraint_new (stage, CLUTTER_BIND_HEIGHT, -PADDING * 2));
  clutter_actor_add_child (stage, menu);

  clutter_main ();

  return 0;
}
/* }}} */
