/*
 * 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: Neil Jagdish Patel <neil.patel@canonical.com>
 *
 */
/**
 * SECTION:ctk-icon-view
 * @short_description: A widget that organises its children into a grid of icons
 * @include: ctk-icon-view.h
 *
 * CtkIconView can hold multiple widgets and organises them into a grid that fills
 * the space available.
 */

#if HAVE_CONFIG_H
#include <config.h>
#endif

#include <clutter-gtk/clutter-gtk.h>
#include <clutk/ctk-focusable.h>

#include "ctk-icon-view.h"
#include "math.h"

static void clutter_container_iface_init (ClutterContainerIface *iface);

G_DEFINE_TYPE_WITH_CODE (CtkIconView,
                         ctk_icon_view,
                         CTK_TYPE_ACTOR,
                         G_IMPLEMENT_INTERFACE (CLUTTER_TYPE_CONTAINER,
                                                clutter_container_iface_init));

#define CTK_ICON_VIEW_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj),\
  CTK_TYPE_ICON_VIEW, \
  CtkIconViewPrivate))

struct _CtkIconViewPrivate
{
  GList  *children;
  gint16  spacing;

  guint   n_cols;
};

enum
{
  PROP_0,

  PROP_SPACING
};

/* Globals */

/* Forwards */
static void ctk_icon_view_add              (ClutterContainer *container,
    ClutterActor     *actor);
static void ctk_icon_view_remove           (ClutterContainer *container,
    ClutterActor *actor);
static void ctk_icon_view_foreach          (ClutterContainer *container,
    ClutterCallback   callback,
    gpointer          user_data);
static void ctk_icon_view_raise            (ClutterContainer *container,
    ClutterActor     *actor,
    ClutterActor     *sibling);
static void ctk_icon_view_lower            (ClutterContainer *container,
    ClutterActor     *actor,
    ClutterActor     *sibling);
static void ctk_icon_view_sort_depth_order (ClutterContainer *container);

/* GObject stuff */
static void
ctk_icon_view_finalize (GObject *object)
{
  CtkIconViewPrivate *priv = CTK_ICON_VIEW (object)->priv;
  GList *c;

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

      if (CLUTTER_IS_ACTOR (child))
        clutter_actor_unparent (child);
    }
  g_list_free (priv->children);
  priv->children = NULL;

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

static void
clutter_container_iface_init (ClutterContainerIface *iface)
{
  iface->add              = ctk_icon_view_add;
  iface->remove           = ctk_icon_view_remove;
  iface->foreach          = ctk_icon_view_foreach;
  iface->raise            = ctk_icon_view_raise;
  iface->lower            = ctk_icon_view_lower;
  iface->sort_depth_order = ctk_icon_view_sort_depth_order;
}

static void
ctk_icon_view_paint (ClutterActor *actor)
{
  CtkActor *ctk_actor = CTK_ACTOR (actor);
  GSList   *effects   = NULL;

  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_icon_view_paint, last_effect);
        }

      ctk_actor_set_effects_painting (ctk_actor, FALSE);
    }
  else
    {
      GList *c;

      CLUTTER_ACTOR_CLASS (ctk_icon_view_parent_class)->paint (actor);


      for (c = CTK_ICON_VIEW (actor)->priv->children; c; c = c->next)
        {
          if (CLUTTER_IS_ACTOR (c->data) && CLUTTER_ACTOR_IS_VISIBLE (c->data))
            clutter_actor_paint (c->data);
        }
    }
  ctk_actor_set_damaged (ctk_actor, FALSE);
}

static void
ctk_icon_view_pick (ClutterActor       *actor,
                    const ClutterColor *color)
{

  /* Chain up so we get a bounding icon_view pained (if we are reactive) */
  CLUTTER_ACTOR_CLASS (ctk_icon_view_parent_class)->pick (actor, color);

  /* Just forward to the paint call which in turn will trigger
   * the child actors also getting 'picked'.
   */
  if (CLUTTER_ACTOR_IS_VISIBLE (actor))
    {
      ctk_actor_set_effects_painting (CTK_ACTOR (actor), TRUE);
      ctk_icon_view_paint (actor);
      ctk_actor_set_effects_painting (CTK_ACTOR (actor), FALSE);
    }
}

static void
ctk_icon_view_set_property (GObject      *object,
                            guint         prop_id,
                            const GValue *value,
                            GParamSpec   *pspec)
{
  CtkIconView *icon_view = CTK_ICON_VIEW (object);

  switch (prop_id)
    {
    case PROP_SPACING:
      ctk_icon_view_set_spacing (icon_view, g_value_get_int (value));
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
    }
}

static void
ctk_icon_view_get_property (GObject    *object,
                            guint       prop_id,
                            GValue     *value,
                            GParamSpec *pspec)
{
  CtkIconView *icon_view = CTK_ICON_VIEW (object);

  switch (prop_id)
    {
    case PROP_SPACING:
      g_value_set_int (value, ctk_icon_view_get_spacing (icon_view));
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
    }
}

static void
ctk_icon_view_get_preferred_width (ClutterActor *actor,
                                   gfloat   for_height,
                                   gfloat  *minimum_width,
                                   gfloat  *natural_width)
{
  CtkPadding     padding;

  ctk_actor_get_padding (CTK_ACTOR (actor), &padding);

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


  if (minimum_width)
    *minimum_width += padding.left + padding.right;

  if (natural_width)
    *natural_width += padding.left + padding.right;
}

static void
ctk_icon_view_get_preferred_height (ClutterActor *actor,
                                    gfloat   for_width,
                                    gfloat  *minimum_height,
                                    gfloat  *natural_height)
{
  CtkIconView        *icon_view = CTK_ICON_VIEW (actor);
  CtkIconViewPrivate *priv = icon_view->priv;
  CtkPadding padding;
  gfloat     real_width;
  gfloat     spacing;
  gfloat     min_height = 0;
  gfloat     nat_height = 0;
  gfloat     max_child_width = 0;
  GList     *c;
  gint       n_cols;
  gfloat     cur_row_height = 0;
  gfloat     x;
  gfloat     y;
  gint            i = 0;

  ctk_actor_get_padding (CTK_ACTOR (actor), &padding);

  if (for_width == -1)
    for_width = 824;

  real_width = for_width - padding.left - padding.right;
  spacing = (gfloat) (priv->spacing);

  /* Find out our widest child */
  for (c = priv->children; c; c = c->next)
    {
      ClutterActor *child = c->data;
      gfloat   nwidth = 0;

      if (!CLUTTER_IS_ACTOR (child))
        continue;

      clutter_actor_get_preferred_width (child, -1, NULL, &nwidth);

      max_child_width = MAX (max_child_width, nwidth);
    }

  /* Figure out the layout */
  n_cols = floor (real_width/ max_child_width);
  n_cols = (real_width - ((n_cols-1) * spacing))/max_child_width;
  priv->n_cols = n_cols;

  x = padding.left;
  y = padding.top;
  i = 0;

  for (c = priv->children; c; c = c->next)
    {
      ClutterActor    *child = c->data;
      gfloat      nheight = 0;

      if (!CLUTTER_IS_ACTOR (child))
        continue;

      clutter_actor_get_preferred_height (child, max_child_width,NULL, &nheight);

      cur_row_height = MAX (cur_row_height, nheight);
      x += max_child_width + spacing;
      i += 1;

      if (i == n_cols)
        {
          x = padding.left;
          y += cur_row_height + spacing;
          cur_row_height = 0;
          i = 0;
        }
    }

  if (i == 0)
    y -= spacing;

  min_height = nat_height = y + cur_row_height + padding.bottom;

  if (minimum_height)
    *minimum_height = min_height;

  if (natural_height)
    *natural_height = nat_height;
}

static void
ctk_icon_view_allocate (ClutterActor          *actor,
                        const ClutterActorBox *box,
                        ClutterAllocationFlags flags)
{
  CtkIconView        *icon_view = CTK_ICON_VIEW (actor);
  CtkIconViewPrivate *priv = icon_view->priv;
  CtkPadding padding;
  gfloat     real_width;
  gfloat     spacing;
  gfloat     max_child_width = 0;
  GList     *c;
  gint       n_cols;
  gfloat     cur_row_height = 0;
  gfloat     x;
  gfloat     y;
  gint       i = 0;
  gint       row=0;

  CLUTTER_ACTOR_CLASS (ctk_icon_view_parent_class)->allocate (actor,
      box,
      flags);

  ctk_actor_get_padding (CTK_ACTOR (actor), &padding);

  real_width = box->x2 - box->x1 - padding.left - padding.right;
  spacing = (gfloat) (priv->spacing);

  /* Find out our widest child */
  for (c = priv->children; c; c = c->next)
    {
      ClutterActor *child = c->data;
      gfloat   nwidth = 0;

      if (!CLUTTER_IS_ACTOR (child))
        continue;

      clutter_actor_get_preferred_width (child, -1, NULL, &nwidth);

      max_child_width = MAX (max_child_width, nwidth);
      i++;
    }

  /* Figure out the layout */
  n_cols = floor (real_width/ max_child_width);
  n_cols = (real_width - ((n_cols-1) * spacing))/max_child_width;
  priv->n_cols = n_cols;

  x = padding.left;
  y = padding.top;

  gfloat row_heights[(gint)ceil (i/n_cols)+1];

  i = 0;
  for (c = priv->children; c; c = c->next)
    {
      gfloat nheight =0;

      if (!CLUTTER_IS_ACTOR (c->data))
        continue;

      clutter_actor_get_preferred_height (c->data, max_child_width, NULL,
                                          &nheight);

      row_heights[row] = MAX (row_heights[row], nheight);
      i++;
      if (i == n_cols)
        {
          row += 1;
          i = 0;
        }
    }

  i = 0;
  row = 0;

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

      if (!CLUTTER_IS_ACTOR (child))
        continue;

      box_ = g_object_get_data (G_OBJECT (child), "ctk-icon-view-child-box");

      child_box.x1 = floor (x);
      child_box.x2 = floor (x + max_child_width);
      child_box.y1 = floor (y);
      child_box.y2 = floor (y + row_heights[row]);
      clutter_actor_allocate (child, &child_box, flags);

      if (box_)
        {
          box_->x1 = floor (child_box.x1); box_->x2 = floor (child_box.x2);
          box_->y1 = floor (child_box.y1); box_->y2 = floor (child_box.y2);
        }

      cur_row_height = MAX (cur_row_height, row_heights[row]);
      x += max_child_width + spacing;
      i += 1;

      if (i == n_cols)
        {
          x = padding.left;
          y += cur_row_height + spacing;
          cur_row_height = 0;
          i = 0;
          row += 1;
        }
    }
}

static void
ctk_icon_view_focus_in (ClutterActor *actor)
{
  CtkIconViewPrivate *priv;
  GList *c;

  g_return_if_fail (CTK_IS_ICON_VIEW (actor));
  priv = CTK_ICON_VIEW (actor)->priv;

  CLUTTER_ACTOR_CLASS (ctk_icon_view_parent_class)->key_focus_in (actor);

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

      if (CTK_IS_FOCUSABLE (child))
        {
          ClutterActorBox *box;

          box = g_object_get_data (G_OBJECT (child),
                                   "ctk-icon-view-child-box");

          ctk_focusable_set_focused (CTK_FOCUSABLE (child), TRUE);
          g_signal_emit_by_name (actor, "child-focus-changed", box);
          break;
        }
    }
}

static gboolean
ctk_icon_view_key_press_event (ClutterActor *actor, ClutterKeyEvent *event)
{
  CtkIconViewPrivate *priv;
  CtkFocusEventType   type;
  CtkFocusDirection   direction;
  GList              *c;
  ClutterActor       *parent;
  ClutterActor       *new_focused = NULL;
  ClutterActor       *focused_child = NULL;
  gint                focused_child_pos = 0;
  gint                offset = 0;
  gint                n_children;

  g_return_val_if_fail (CTK_IS_ICON_VIEW (actor), FALSE);
  priv = CTK_ICON_VIEW (actor)->priv;

  parent = clutter_actor_get_parent (actor);

  direction = CTK_FOCUS_DIR_NONE;
  type = ctk_focusable_get_event_type (event, &direction);

  if (type != CTK_FOCUS_EVENT_DIRECTION)
    return FALSE;

  if (direction == CTK_FOCUS_DIR_NONE)
    return FALSE;

  if (direction == CTK_FOCUS_DIR_NEXT || direction == CTK_FOCUS_DIR_PREV)
    return clutter_actor_event (parent, (ClutterEvent *)event, FALSE);

  switch (direction)
    {
    case CTK_FOCUS_DIR_UP:
      offset -= priv->n_cols;
      break;
    case CTK_FOCUS_DIR_RIGHT:
      offset += 1;
      break;
    case CTK_FOCUS_DIR_DOWN:
      offset += priv->n_cols;
      break;
    case CTK_FOCUS_DIR_LEFT:
      offset -= 1;
    default:
      break;
    }

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

      if (CTK_IS_FOCUSABLE (child)
          && ctk_focusable_get_focused (CTK_FOCUSABLE (child)))
        {
          focused_child = child;
          break;
        }
      focused_child_pos++;
    }

  if (focused_child == NULL)
    {
      focused_child = g_list_nth_data (priv->children, 0);
      focused_child_pos = 0;
      offset = 0;

      if (focused_child == NULL)
        return FALSE;
    }

  n_children = g_list_length (priv->children);
  focused_child_pos += offset;

  /*
  if (focused_child_pos >= n_children)
    focused_child_pos = n_children - 1;
  if (focused_child_pos < 0)
    focused_child_pos = 0;
  */
  if (focused_child_pos >= n_children || focused_child_pos < 0)
    {
      return clutter_actor_event (parent, (ClutterEvent *)event, FALSE);
    }

  new_focused = g_list_nth_data (priv->children, focused_child_pos);
  ClutterActorBox *box;

  box = g_object_get_data (G_OBJECT (new_focused),
                           "ctk-icon-view-child-box");
  ctk_focusable_set_focused (CTK_FOCUSABLE (new_focused), TRUE);
  g_signal_emit_by_name (actor, "child-focus-changed", box);

  return TRUE;
}

static void
ctk_icon_view_class_init (CtkIconViewClass *klass)
{
  GObjectClass      *obj_class = G_OBJECT_CLASS (klass);
  ClutterActorClass *act_class = CLUTTER_ACTOR_CLASS (klass);
  GParamSpec        *pspec;

  obj_class->finalize     = ctk_icon_view_finalize;
  obj_class->set_property = ctk_icon_view_set_property;
  obj_class->get_property = ctk_icon_view_get_property;

  act_class->paint                = ctk_icon_view_paint;
  act_class->get_preferred_width  = ctk_icon_view_get_preferred_width;
  act_class->get_preferred_height = ctk_icon_view_get_preferred_height;
  act_class->allocate             = ctk_icon_view_allocate;
  act_class->pick                 = ctk_icon_view_pick;
  act_class->key_press_event      = ctk_icon_view_key_press_event;
  act_class->key_focus_in         = ctk_icon_view_focus_in;

  /* Install properties */
  pspec = g_param_spec_int ("spacing", "spacing",
                            "The amount of space between the children",
                            0, G_MAXINT, 18,
                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
  g_object_class_install_property (obj_class, PROP_SPACING, pspec);

  /* Install private struct */
  g_type_class_add_private (obj_class, sizeof (CtkIconViewPrivate));
}

static void
ctk_icon_view_init (CtkIconView *icon_view)
{
  CtkIconViewPrivate *priv;

  priv = icon_view->priv = CTK_ICON_VIEW_GET_PRIVATE (icon_view);

  priv->children = NULL;
  priv->spacing = 0;
}

ClutterActor *
ctk_icon_view_new (void)
{
  return g_object_new (CTK_TYPE_ICON_VIEW,
                       "request-mode", CLUTTER_REQUEST_HEIGHT_FOR_WIDTH,
                       NULL);
}

/*
 * Private methods
 */

static void
ctk_icon_view_add (ClutterContainer *container,
                   ClutterActor     *actor)
{
  CtkIconViewPrivate *priv;
  ClutterActorBox    *box;

  g_return_if_fail (CTK_IS_ICON_VIEW (container));
  g_return_if_fail (CLUTTER_IS_ACTOR (actor));
  priv = CTK_ICON_VIEW (container)->priv;

  g_object_ref (actor);

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

  box = g_slice_new0 (ClutterActorBox);
  g_object_set_data (G_OBJECT (actor), "ctk-icon-view-child-box", box);

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

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

  if (CLUTTER_ACTOR_IS_VISIBLE (CLUTTER_ACTOR (container)))
    clutter_actor_queue_redraw (CLUTTER_ACTOR (container));

  g_object_unref (actor);
}

static void
ctk_icon_view_remove (ClutterContainer *container,
                      ClutterActor     *actor)
{
  CtkIconViewPrivate *priv;
  GList *c;

  g_return_if_fail (CTK_IS_ICON_VIEW (container));
  g_return_if_fail (CLUTTER_IS_ACTOR (actor));
  priv = CTK_ICON_VIEW (container)->priv;

  g_object_ref (actor);

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

      if (child == actor)
        {
          ClutterActorBox *box;
          clutter_actor_unparent (actor);

          box = g_object_get_data (G_OBJECT (actor),
                                   "ctk-icon-view-child-box");
          if (box)
            g_slice_free (ClutterActorBox, box);
          g_object_set_data (G_OBJECT (actor),
                             "ctk-icon-view-child-box",
                             NULL);

          priv->children = g_list_remove (priv->children, child);
          break;
        }
    }

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

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

  if (CLUTTER_ACTOR_IS_VISIBLE (CLUTTER_ACTOR (container)))
    clutter_actor_queue_redraw (CLUTTER_ACTOR (container));

  g_object_unref (actor);
}

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

  g_return_if_fail (CTK_IS_ICON_VIEW (container));
  priv = CTK_ICON_VIEW (container)->priv;

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

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

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

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

/*
 * Public methods
 */

/**
 * ctk_icon_view_set_spacing:
 * @icon_view: A CtkIconView
 * @spacing: an integer defining the spacing size
 *
 * Sets the spacing between each item contained within @icon_view. Spacing is in pixels
 */
void
ctk_icon_view_set_spacing (CtkIconView *icon_view,
                           gint              spacing)
{
  g_return_if_fail (CTK_IS_ICON_VIEW (icon_view));

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

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

      clutter_actor_queue_relayout (CLUTTER_ACTOR (icon_view));
    }
}

/**
 * ctk_icon_view_get_spacing:
 * @icon_view: A CtkIconView
 *
 * Gets the spacing defined previously in ctk_icon_view_set_spacing()
 *
 * Returns: An integer defining the spacing in pixels
 */

gint
ctk_icon_view_get_spacing (CtkIconView *icon_view)
{
  g_return_val_if_fail (CTK_IS_ICON_VIEW (icon_view), 0);
  return icon_view->priv->spacing;
}
