/*
 * 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, SATISFTOOLTIPY 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-tooltip
 * @short_description: Provides a way to show tooltips in Clutk
 *
 * #CtkTooltip Provides a widget to show a tooltip on a given #ClutterActor object
 */
#if HAVE_CONFIG_H
#include <config.h>
#endif

#include "ctk-tooltip.h"

#include "ctk-private.h"
#include "ctk-text.h"

G_DEFINE_TYPE (CtkTooltip, ctk_tooltip, CTK_TYPE_ACTOR);

#define CTK_TOOLTIP_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj),\
  CTK_TYPE_TOOLTIP, \
  CtkTooltipPrivate))

struct _CtkTooltipPrivate
{
  ClutterActor *actor;
  ClutterActor *text;
  gchar        *label;
};

enum
{
  PROP_0,

  PROP_ACTOR,
  PROP_LABEL
};


/* Globals */

/* Forwards */
static void     ctk_tooltip_paint           (ClutterActor         *actor);

static void     ctk_tooltip_allocate        (ClutterActor          *tooltip,
    const ClutterActorBox *box,
    ClutterAllocationFlags flags);
static void     ctk_tooltip_map             (ClutterActor          *tooltip);
static void     ctk_tooltip_unmap           (ClutterActor          *tooltip);
static void     ctk_tooltip_actor_weak_notify  (CtkTooltip *tooltip,
    GObject *actor);

static void     ctk_tooltip_get_preferred_width (ClutterActor *actor,
    gfloat        for_height,
    gfloat       *min_width,
    gfloat       *nat_width);

static void     ctk_tooltip_get_preferred_height(ClutterActor *actor,
    gfloat        for_width,
    gfloat       *min_height,
    gfloat       *nat_height);
/* GObject stuff */
static void
ctk_tooltip_finalize (GObject *object)
{
  CtkTooltipPrivate *priv = CTK_TOOLTIP (object)->priv;

  if (priv->label)
    {
      g_free (priv->label);
      priv->label = NULL;
    }
  if (priv->actor)
    {
      priv->actor = NULL;
    }
  if (priv->text)
    {
      clutter_actor_unparent (priv->text);
      priv->text = NULL;
    }

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

static void
ctk_tooltip_set_property (GObject      *object,
                          guint         prop_id,
                          const GValue *value,
                          GParamSpec   *pspec)
{
  CtkTooltip *tooltip = CTK_TOOLTIP (object);

  switch (prop_id)
    {
    case PROP_ACTOR:
      ctk_tooltip_set_actor (tooltip, g_value_get_pointer (value));
      break;

    case PROP_LABEL:
      ctk_tooltip_set_label (tooltip, g_value_get_string (value));
      break;

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

static void
ctk_tooltip_get_property (GObject    *object,
                          guint       prop_id,
                          GValue     *value,
                          GParamSpec *pspec)
{
  CtkTooltip *tooltip = CTK_TOOLTIP (object);

  switch (prop_id)
    {
    case PROP_ACTOR:
      g_value_set_pointer (value, tooltip->priv->actor);
      break;

    case PROP_LABEL:
      g_value_set_string (value, tooltip->priv->label);
      break;

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

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

  /* Overrides */
  obj_class->finalize        = ctk_tooltip_finalize;
  obj_class->set_property    = ctk_tooltip_set_property;
  obj_class->get_property    = ctk_tooltip_get_property;

  act_class->paint           = ctk_tooltip_paint;
  act_class->allocate        = ctk_tooltip_allocate;
  act_class->map             = ctk_tooltip_map;
  act_class->unmap           = ctk_tooltip_unmap;
  act_class->get_preferred_width  = ctk_tooltip_get_preferred_width;
  act_class->get_preferred_height = ctk_tooltip_get_preferred_height;

  /* Install Properties */
  pspec = g_param_spec_pointer ("actor", "actor",
                                "Actor the tooltip is associated with",
                                CTK_PARAM_READWRITE);
  g_object_class_install_property (obj_class, PROP_ACTOR, pspec);

  pspec = g_param_spec_string ("label", "Label", "The contents of the tooltip",
                               NULL, CTK_PARAM_READWRITE);
  g_object_class_install_property (obj_class, PROP_LABEL, pspec);

  /* Add Private struct */
  g_type_class_add_private (obj_class, sizeof (CtkTooltipPrivate));
}

static void
ctk_tooltip_init (CtkTooltip *tooltip)
{
  CtkTooltipPrivate *priv;
  CtkPadding         padding = { 4, 4, 4, 4 };
  ClutterColor       color = { 0x00, 0x00, 0x00, 0xbb };
  ClutterActor      *bg;

  priv = tooltip->priv = CTK_TOOLTIP_GET_PRIVATE (tooltip);

  priv->text = ctk_text_new (NULL);
  clutter_text_set_line_wrap (CLUTTER_TEXT (priv->text), TRUE);
  clutter_text_set_line_alignment (CLUTTER_TEXT (priv->text),
                                   PANGO_ALIGN_CENTER);
  clutter_actor_set_parent (priv->text, CLUTTER_ACTOR (tooltip));
  clutter_actor_show (priv->text);

  clutter_actor_set_opacity (CLUTTER_ACTOR (tooltip), 0);
  ctk_actor_set_padding (CTK_ACTOR (tooltip), &padding);

  bg = clutter_rectangle_new_with_color (&color);
  ctk_actor_set_background (CTK_ACTOR (tooltip), bg);
}

/*
 * Private methods
 */
static void
ctk_tooltip_paint (ClutterActor *tooltip)
{
  CtkTooltipPrivate *priv = CTK_TOOLTIP (tooltip)->priv;

  CLUTTER_ACTOR_CLASS (ctk_tooltip_parent_class)->paint (tooltip);

  if (priv->text)
    clutter_actor_paint (priv->text);
}

static void
ctk_tooltip_allocate (ClutterActor          *tooltip,
                      const ClutterActorBox *box,
                      ClutterAllocationFlags flags)
{
  CtkTooltipPrivate *priv;
  ClutterActorClass *klass;
  ClutterActorBox    child_box;
  CtkPadding         padding;

  klass = CLUTTER_ACTOR_CLASS (ctk_tooltip_parent_class);
  klass->allocate (tooltip, box, flags);
  priv = CTK_TOOLTIP (tooltip)->priv;

  if (priv->text)
    {
      ctk_actor_get_padding (CTK_ACTOR (tooltip), &padding);

      child_box.x1 = padding.left;
      child_box.x2 = box->x2 - box->x1 - padding.left - padding.right;
      child_box.y1 = padding.top;
      child_box.y2 = box->y2 - box->y1 - padding.top - padding.bottom;

      clutter_actor_allocate (priv->text, &child_box, flags);
    }
}

static void
ctk_tooltip_map (ClutterActor *tooltip)
{
  CtkTooltipPrivate *priv = CTK_TOOLTIP (tooltip)->priv;

  CLUTTER_ACTOR_CLASS (ctk_tooltip_parent_class)->map (tooltip);

  if (priv->text)
    clutter_actor_map (priv->text);
}

static void
ctk_tooltip_unmap (ClutterActor *tooltip)
{
  CtkTooltipPrivate *priv = CTK_TOOLTIP (tooltip)->priv;

  CLUTTER_ACTOR_CLASS (ctk_tooltip_parent_class)->unmap (tooltip);

  if (priv->text)
    clutter_actor_unmap (priv->text);
}

static void
ctk_tooltip_actor_weak_notify (CtkTooltip *self, GObject *actor)
{
  CtkTooltipPrivate *priv;
  ClutterActor      *parent;

  g_return_if_fail (CTK_IS_TOOLTIP (self));
  priv = self->priv;

  /* We destroy the tooltip with it's 'parent actor'.
   * To do this, we need to check if the tooltip has a parent and, if so,
   * simply remove it from the parent. If not, the tooltip still has a
   * floating reference, so we sink that reference and unref it to destroy
   */
  parent = clutter_actor_get_parent (CLUTTER_ACTOR (self));
  if (parent == NULL)
    {
      g_object_ref_sink (G_OBJECT (self));
      g_object_unref (G_OBJECT (self));
    }
  else
    {
      if (CLUTTER_IS_CONTAINER (parent))
        clutter_container_remove_actor (CLUTTER_CONTAINER (parent),
                                        CLUTTER_ACTOR (self));
      else
        clutter_actor_unparent (CLUTTER_ACTOR (self));
    }
}

static void
ctk_tooltip_get_preferred_width (ClutterActor *actor,
                                 gfloat        for_height,
                                 gfloat       *min_width,
                                 gfloat       *nat_width)
{
  CtkTooltipPrivate *priv = CTK_TOOLTIP (actor)->priv;
  CtkPadding padding;
  gfloat     req_width;
  gfloat     actor_width;

  clutter_actor_get_preferred_width (priv->text, for_height,
                                     min_width, nat_width);

  clutter_actor_get_preferred_width (priv->actor, -1, NULL, &actor_width);

  req_width = MAX (*min_width, *nat_width);

  if (req_width > actor_width * 1.5)
    req_width = actor_width * 1.5;

  *min_width = req_width;
  *nat_width = req_width;

  ctk_actor_get_padding (CTK_ACTOR (actor), &padding);
  *min_width += padding.right + padding.left;
  *nat_width += padding.right + padding.left;
}

static void
ctk_tooltip_get_preferred_height (ClutterActor *actor,
                                  gfloat        for_width,
                                  gfloat       *min_height,
                                  gfloat       *nat_height)
{
  CtkTooltipPrivate *priv = CTK_TOOLTIP (actor)->priv;
  CtkPadding padding;

  clutter_actor_get_preferred_width (actor, -1, NULL, &for_width);

  clutter_actor_get_preferred_height (priv->text, for_width,
                                      min_height, nat_height);

  ctk_actor_get_padding (CTK_ACTOR (actor), &padding);
  *min_height += padding.top + padding.bottom;
  *nat_height += padding.top + padding.bottom;
}

/*
 * Public methods
 */

/**
 * ctk_tooltip_new:
 * @actor: a #ClutterActor
 *
 * Creates a new ctk_tooltip object thats attached to @actor
 *
 * Returns: A #CtkTooltip object
 */
ClutterActor *
ctk_tooltip_new (ClutterActor *actor)
{
  g_return_val_if_fail (CLUTTER_IS_ACTOR (actor), NULL);

  return g_object_new (CTK_TYPE_TOOLTIP, "actor", actor, NULL);
}

/**
 * ctk_tooltip_set_actor:
 * @self: a #Ctktooltip object
 * @actor: a #ClutterActor
 *
 * sets @self to use @actor as the #ClutterActor its attached to
 */
void
ctk_tooltip_set_actor (CtkTooltip   *self,
                       ClutterActor *actor)
{
  CtkTooltipPrivate *priv;

  g_return_if_fail (CTK_IS_TOOLTIP (self));
  g_return_if_fail (CLUTTER_IS_ACTOR (actor));
  priv = self->priv;

  if (priv->actor)
    {
      g_object_weak_unref (G_OBJECT (priv->actor),
                           (GWeakNotify)ctk_tooltip_actor_weak_notify, self);
      priv->actor = NULL;
    }
  priv->actor = actor;
  g_object_weak_ref (G_OBJECT (priv->actor),
                     (GWeakNotify)ctk_tooltip_actor_weak_notify, self);
}

/**
 * ctk_tooltip_get_actor:
 * @self: a #CtkTooltip object
 *
 * Retrives the actor @self is attached to
 *
 * Returns: a #ClutterActor
 */
ClutterActor *
ctk_tooltip_get_actor (CtkTooltip *self)
{
  g_return_val_if_fail (CTK_IS_TOOLTIP (self), NULL);
  return self->priv->actor;
}

/**
 * ctk_tooltip_set_label:
 * @self: A #CtkTooltip object
 * @label: a string for the label
 *
 * Sets the tooltip @self to use the text @label for its label
 */
void
ctk_tooltip_set_label (CtkTooltip  *self,
                       const gchar *label)
{
  CtkTooltipPrivate *priv;

  g_return_if_fail (CTK_IS_TOOLTIP (self));
  priv = self->priv;

  if (priv->label)
    {
      g_free (priv->label);
      priv->label = NULL;
    }

  if (label)
    priv->label = g_strdup (label);

  clutter_text_set_text (CLUTTER_TEXT (priv->text), label);
}


/**
 * ctk_tooltip_get_label:
 * @self: a #CtkTooltip object
 *
 * Retrives the label set previously by ctk_tooltip_set_label()
 *
 * Returns: a string
 */
const gchar *
ctk_tooltip_get_label (CtkTooltip *self)
{
  g_return_val_if_fail (CTK_IS_TOOLTIP (self), NULL);
  return self->priv->label;
}

/*
 * ctk_tooltip_show:
 * @self: #CtkTooltip object
 * @x: an integer position
 * @y: an integer position
 *
 * shows the tooltip @self at the given position, x/y should generally be the position
 * of the mouse on the stage
 */
void
ctk_tooltip_show (CtkTooltip   *self,
                  gint          x,
                  gint          y)
{
  CtkTooltipPrivate *priv;
  ClutterAnimation  *anim = NULL;
  ClutterActor      *stage;
  gfloat             self_width, self_height;
  gfloat             stage_width, stage_height;
  gfloat             actor_x, actor_y, actor_width, actor_height;

  g_return_if_fail (CTK_IS_TOOLTIP (self));
  priv = self->priv;

  return;

  if (priv->label == NULL || priv->actor == NULL)
    return;

  /* The tooltip should be above any other actor on the stage, therefore we
   * want the parent of the tooltip to be the stage itself plus, before we
   * show it, the tooltip is raised to the top of the stage
   */
  stage = clutter_actor_get_stage (priv->actor);
  if (!stage)
    {
      g_warning ("CtkTooltip's associated actor must be on a stage before the "
                 "tooltip can be shown");
      return;
    }

  if (clutter_actor_get_parent (CLUTTER_ACTOR (self)) != stage)
    {
      /* Use reparent just in case someone's decided to add the tooltip to
       * another container (or should this be allowed??)
       */
      clutter_actor_reparent (CLUTTER_ACTOR (self), stage);
    }
  clutter_container_raise_child (CLUTTER_CONTAINER (stage),
                                 CLUTTER_ACTOR (self),
                                 NULL);

  /* So, we have the stage co-ordinates (x, y), so only thing we want to do
   * is position the tooltip to the bottom of the mouse pointer (we'll take an
   * educated guess on the size of the pointer), and also make sure it isn't
   * displaying off the edge of the stage
   */
  x += 24;
  y += 24;

  clutter_actor_get_preferred_size (CLUTTER_ACTOR (self), NULL, NULL,
                                    &self_width, &self_height);
  clutter_actor_get_preferred_size (stage, NULL, NULL, &stage_width,
                                    &stage_height);
  clutter_actor_get_transformed_size (priv->actor, &actor_width, &actor_height);
  clutter_actor_get_transformed_position (priv->actor, &actor_x, &actor_y);

  x = actor_x + actor_width/2 - self_width/2;
  y = actor_y + actor_height;

  if (x < 0)
    x = 0;
  if (y < 0)
    y = 0;
  if ((x + self_width) > stage_width)
    x = stage_width - self_width;
  if ((y + self_height) > stage_height)
    y = stage_height - self_height;

  clutter_actor_set_position (CLUTTER_ACTOR (self), x, y);
  clutter_actor_show (CLUTTER_ACTOR (self));
  clutter_actor_queue_relayout (CLUTTER_ACTOR (self));

  if ((anim = clutter_actor_get_animation (CLUTTER_ACTOR (self))))
    clutter_animation_completed (anim);

  clutter_actor_animate (CLUTTER_ACTOR (self), CLUTTER_EASE_IN_SINE, 400,
                         "opacity", 255, NULL);
}

static void
on_hide_complete (ClutterAnimation *anim, ClutterActor *self)
{
  if (CLUTTER_IS_ACTOR (self))
    clutter_actor_hide (self);
}

/**
 * ctk_tooltip_hide:
 * @self: A #CtkTooltip object
 *
 * Hides the tooltip @self - animating it as it goes
 */
void
ctk_tooltip_hide (CtkTooltip *self)
{
  g_return_if_fail (CTK_IS_TOOLTIP (self));

  clutter_actor_animate (CLUTTER_ACTOR (self), CLUTTER_EASE_IN_SINE, 400,
                         "opacity", 0,
                         "signal::completed", on_hide_complete, self,
                         NULL);
}
