/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */

/* eel-ellipsizing-label.c: Subclass of GtkLabel that ellipsizes the text.

   Copyright (C) 2001 Eazel, Inc.

   The Gnome Library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Library General Public License as
   published by the Free Software Foundation; either version 2 of the
   License, or (at your option) any later version.

   The Gnome Library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Library General Public License for more priv.

   You should have received a copy of the GNU Library General Public
   License along with the Gnome Library; see the file COPYING.LIB.  If not,
   write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
   Boston, MA 02111-1307, USA.

   Author: John Sullivan <sullivan@eazel.com>,
	   Marco Pesenti Gritti <marco@it.gnome.org> Markup support
 */

#include "ephy-ellipsizing-label.h"

#include <string.h>

struct EphyEllipsizingLabelPrivate
{
	char *full_text;

	EphyEllipsizeMode mode;
};

static void ephy_ellipsizing_label_class_init (EphyEllipsizingLabelClass *class);
static void ephy_ellipsizing_label_init       (EphyEllipsizingLabel      *label);

static GObjectClass *parent_class = NULL;

static int
ephy_strcmp (const char *string_a, const char *string_b)
{
       return strcmp (string_a == NULL ? "" : string_a,
                      string_b == NULL ? "" : string_b);
}

static gboolean
ephy_str_is_equal (const char *string_a, const char *string_b)
{
       return ephy_strcmp (string_a, string_b) == 0;
}

#define ELLIPSIS "..."

/* Caution: this is an _expensive_ function */
static int
measure_string_width (const char  *string,
		      PangoLayout *layout,
		      gboolean markup)
{
	int width;

	if (markup)
	{
		pango_layout_set_markup (layout, string, -1);
	}
	else
	{
		pango_layout_set_text (layout, string, -1);
	}

	pango_layout_get_pixel_size (layout, &width, NULL);

	return width;
}

/* this is also plenty slow */
static void
compute_character_widths (const char    *string,
			  PangoLayout   *layout,
			  int           *char_len_return,
			  int          **widths_return,
			  int          **cuts_return,
			  gboolean       markup)
{
	int *widths;
	int *offsets;
	int *cuts;
	int char_len;
	int byte_len;
	const char *p;
	const char *nm_string;
	int i;
	PangoLayoutIter *iter;
	PangoLogAttr *attrs;

#define BEGINS_UTF8_CHAR(x) (((x) & 0xc0) != 0x80)

	if (markup)
	{
		pango_layout_set_markup (layout, string, -1);
	}
	else
	{
		pango_layout_set_text (layout, string, -1);
	}

	nm_string = pango_layout_get_text (layout);

	char_len = g_utf8_strlen (nm_string, -1);
	byte_len = strlen (nm_string);

	widths = g_new (int, char_len);
	offsets = g_new (int, byte_len);

	/* Create a translation table from byte index to char offset */
	p = nm_string;
	i = 0;
	while (*p) {
		int byte_index = p - nm_string;

		if (BEGINS_UTF8_CHAR (*p)) {
			offsets[byte_index] = i;
			++i;
		} else {
			offsets[byte_index] = G_MAXINT; /* segv if we try to use this */
		}

		++p;
	}

	/* Now fill in the widths array */
	iter = pango_layout_get_iter (layout);

	do {
		PangoRectangle extents;
		int byte_index;

		byte_index = pango_layout_iter_get_index (iter);

		if (byte_index < byte_len) {
			pango_layout_iter_get_char_extents (iter, &extents);

			g_assert (BEGINS_UTF8_CHAR (nm_string[byte_index]));
			g_assert (offsets[byte_index] < char_len);

			widths[offsets[byte_index]] = PANGO_PIXELS (extents.width);
		}

	} while (pango_layout_iter_next_char (iter));

	pango_layout_iter_free (iter);

	g_free (offsets);

	*widths_return = widths;

	/* Now compute character offsets that are legitimate places to
	 * chop the string
	 */
	attrs = g_new (PangoLogAttr, char_len + 1);

	pango_get_log_attrs (nm_string, byte_len, -1,
			     pango_context_get_language (
				     pango_layout_get_context (layout)),
			     attrs,
			     char_len + 1);

	cuts = g_new (int, char_len);
	i = 0;
	while (i < char_len) {
		cuts[i] = attrs[i].is_cursor_position;

		++i;
	}

	g_free (attrs);

	*cuts_return = cuts;

	*char_len_return = char_len;
}

typedef struct
{
	GString *string;
	int start_offset;
	int end_offset;
	int position;
} EllipsizeStringData;

static void
start_element_handler (GMarkupParseContext *context,
                       const gchar         *element_name,
                       const gchar        **attribute_names,
                       const gchar        **attribute_values,
                       gpointer             user_data,
                       GError             **error)
{
	EllipsizeStringData *data = (EllipsizeStringData *)user_data;
	int i;

	g_string_append_c (data->string, '<');
	g_string_append (data->string, element_name);

	for (i = 0; attribute_names[i] != NULL; i++)
	{
		g_string_append_c (data->string, ' ');
		g_string_append (data->string, attribute_names[i]);
		g_string_append (data->string, "=\"");
		g_string_append (data->string, attribute_values[i]);
		g_string_append_c (data->string, '"');
	}

	g_string_append_c (data->string, '>');
}

static void
end_element_handler    (GMarkupParseContext *context,
                        const gchar         *element_name,
                        gpointer             user_data,
                        GError             **error)
{
	EllipsizeStringData *data = (EllipsizeStringData *)user_data;

	g_string_append (data->string, "</");
	g_string_append (data->string, element_name);
	g_string_append_c (data->string, '>');
}

static void
append_ellipsized_text (const char *text,
			EllipsizeStringData *data,
			int text_len)
{
	int position;
	int new_position;

	position = data->position;
	new_position = data->position + text_len;

	if (position > data->start_offset &&
	    new_position < data->end_offset)
	{
		return;
	}
	else if ((position < data->start_offset &&
	          new_position < data->start_offset) ||
	         (position > data->end_offset &&
	          new_position > data->end_offset))
	{
		g_string_append (data->string,
				text);
	}
	else if (position <= data->start_offset &&
		 new_position >= data->end_offset)
	{
		if (position < data->start_offset)
		{
			g_string_append_len (data->string,
					     text,
					     data->start_offset -
					     position);
		}

		g_string_append (data->string,
				 ELLIPSIS);

		if (new_position > data->end_offset)
		{
			g_string_append_len (data->string,
					     text + data->end_offset -
					     position,
					     position + text_len -
					     data->end_offset);
		}
	}

	data->position = new_position;
}

static void
text_handler           (GMarkupParseContext *context,
                        const gchar         *text,
                        gsize                text_len,
                        gpointer             user_data,
                        GError             **error)
{
	EllipsizeStringData *data = (EllipsizeStringData *)user_data;

	append_ellipsized_text (text, data, text_len);
}

static GMarkupParser pango_markup_parser = {
  start_element_handler,
  end_element_handler,
  text_handler,
  NULL,
  NULL
};

static char *
ellipsize_string (const char *string,
		  int start_offset,
		  int end_offset,
		  gboolean markup)
{
	GString *str;
	EllipsizeStringData data;
	char *result;
	GMarkupParseContext *c;

	str = g_string_new (NULL);
	data.string = str;
	data.start_offset = start_offset;
	data.end_offset = end_offset;
	data.position = 0;

	if (markup)
	{
		c = g_markup_parse_context_new (&pango_markup_parser,
					        0, &data, NULL);
		g_markup_parse_context_parse (c, string, -1, NULL);
		g_markup_parse_context_free (c);
	}
	else
	{
		append_ellipsized_text (string, &data,
					g_utf8_strlen (string, -1));
	}

	result = g_string_free (str, FALSE);

	return result;
}

static char *
ephy_string_ellipsize_start (const char *string, PangoLayout *layout, int width, gboolean markup)
{
	int resulting_width;
	int *cuts;
	int *widths;
	int char_len;
	int truncate_offset;
	int bytes_end;

	/* Zero-length string can't get shorter - catch this here to
	 * avoid expensive calculations
	 */
	if (*string == '\0')
		return g_strdup ("");

	/* I'm not sure if this short-circuit is a net win; it might be better
	 * to just dump this, and always do the compute_character_widths() etc.
	 * down below.
	 */
	resulting_width = measure_string_width (string, layout, markup);

	if (resulting_width <= width) {
		/* String is already short enough. */
		return g_strdup (string);
	}

	/* Remove width of an ellipsis */
	width -= measure_string_width (ELLIPSIS, layout, markup);

	if (width < 0) {
		/* No room even for an ellipsis. */
		return g_strdup ("");
	}

	/* Our algorithm involves removing enough chars from the string to bring
	 * the width to the required small size. However, due to ligatures,
	 * combining characters, etc., it's not guaranteed that the algorithm
	 * always works 100%. It's sort of a heuristic thing. It should work
	 * nearly all the time... but I wouldn't put in
	 * g_assert (width of resulting string < width).
	 *
	 * Hmm, another thing that this breaks with is explicit line breaks
	 * in "string"
	 */

	compute_character_widths (string, layout, &char_len, &widths, &cuts, markup);

        for (truncate_offset = 1; truncate_offset < char_len; truncate_offset++) {

		resulting_width -= widths[truncate_offset];

		if (resulting_width <= width &&
		    cuts[truncate_offset]) {
			break;
		}
        }

	g_free (cuts);
	g_free (widths);

	bytes_end = g_utf8_offset_to_pointer (string, truncate_offset) - string;

	return ellipsize_string (string, 0, bytes_end, markup);
}

static char *
ephy_string_ellipsize_end (const char *string, PangoLayout *layout, int width, gboolean markup)
{
	int resulting_width;
	int *cuts;
	int *widths;
	int char_len;
	int truncate_offset;
	int bytes_end;

	/* See explanatory comments in ellipsize_start */

	if (*string == '\0')
		return g_strdup ("");

	resulting_width = measure_string_width (string, layout, markup);

	if (resulting_width <= width) {
		return g_strdup (string);
	}

	width -= measure_string_width (ELLIPSIS, layout, markup);

	if (width < 0) {
		return g_strdup ("");
	}

	compute_character_widths (string, layout, &char_len, &widths, &cuts, markup);

        for (truncate_offset = char_len - 1; truncate_offset > 0; truncate_offset--) {
		resulting_width -= widths[truncate_offset];
		if (resulting_width <= width &&
		    cuts[truncate_offset]) {
			break;
		}
        }

	g_free (cuts);
	g_free (widths);

	bytes_end = g_utf8_offset_to_pointer (string, truncate_offset) - string;

	return ellipsize_string (string, bytes_end,
				 char_len, markup);
}

static char *
ephy_string_ellipsize_middle (const char *string, PangoLayout *layout, int width, gboolean markup)
{
	int resulting_width;
	int *cuts;
	int *widths;
	int char_len;
	int starting_fragment_length;
	int ending_fragment_offset;
	int bytes_start;
	int bytes_end;

	/* See explanatory comments in ellipsize_start */

	if (*string == '\0')
		return g_strdup ("");

	resulting_width = measure_string_width (string, layout, markup);

	if (resulting_width <= width) {
		return g_strdup (string);
	}

	width -= measure_string_width (ELLIPSIS, layout, markup);

	if (width < 0) {
		return g_strdup ("");
	}

	compute_character_widths (string, layout, &char_len, &widths, &cuts, markup);

	starting_fragment_length = char_len / 2;
	ending_fragment_offset = starting_fragment_length + 1;

	/* depending on whether the original string length is odd or even, start by
	 * shaving off the characters from the starting or ending fragment
	 */
	if (char_len % 2) {
		goto shave_end;
	}

	while (starting_fragment_length > 0 || ending_fragment_offset < char_len) {
		if (resulting_width <= width &&
		    cuts[ending_fragment_offset] &&
		    cuts[starting_fragment_length]) {
			break;
		}

		if (starting_fragment_length > 0) {
			resulting_width -= widths[starting_fragment_length];
			starting_fragment_length--;
		}

	shave_end:
		if (resulting_width <= width &&
		    cuts[ending_fragment_offset] &&
		    cuts[starting_fragment_length]) {
			break;
		}

		if (ending_fragment_offset < char_len) {
			resulting_width -= widths[ending_fragment_offset];
			ending_fragment_offset++;
		}
	}

	g_free (cuts);
	g_free (widths);

	bytes_start = g_utf8_offset_to_pointer (string, starting_fragment_length) - string;
	bytes_end = g_utf8_offset_to_pointer (string, ending_fragment_offset) - string;

	return ellipsize_string (string, bytes_start, bytes_end, markup);
}


/**
 * ephy_pango_layout_set_text_ellipsized
 *
 * @layout: a pango layout
 * @string: A a string to be ellipsized.
 * @width: Desired maximum width in points.
 * @mode: The desired ellipsizing mode.
 *
 * Truncates a string if required to fit in @width and sets it on the
 * layout. Truncation involves removing characters from the start, middle or end
 * respectively and replacing them with "...". Algorithm is a bit
 * fuzzy, won't work 100%.
 *
 */
static void
gul_pango_layout_set_text_ellipsized (PangoLayout  *layout,
				      const char   *string,
				      int           width,
				      EphyEllipsizeMode mode,
				      gboolean markup)
{
	char *s;

	g_return_if_fail (PANGO_IS_LAYOUT (layout));
	g_return_if_fail (string != NULL);
	g_return_if_fail (width >= 0);

	switch (mode) {
	case EPHY_ELLIPSIZE_START:
		s = ephy_string_ellipsize_start (string, layout, width, markup);
		break;
	case EPHY_ELLIPSIZE_MIDDLE:
		s = ephy_string_ellipsize_middle (string, layout, width, markup);
		break;
	case EPHY_ELLIPSIZE_END:
		s = ephy_string_ellipsize_end (string, layout, width, markup);
		break;
	default:
		g_return_if_reached ();
		s = NULL;
	}

	if (markup)
	{
		pango_layout_set_markup (layout, s, -1);
	}
	else
	{
		pango_layout_set_text (layout, s, -1);
	}

	g_free (s);
}

GType
ephy_ellipsizing_label_get_type (void)
{
        static GType ephy_ellipsizing_label_type = 0;

        if (ephy_ellipsizing_label_type == 0)
        {
                static const GTypeInfo our_info =
                {
                        sizeof (EphyEllipsizingLabelClass),
                        NULL, /* base_init */
                        NULL, /* base_finalize */
                        (GClassInitFunc) ephy_ellipsizing_label_class_init,
                        NULL,
                        NULL, /* class_data */
                        sizeof (EphyEllipsizingLabel),
                        0, /* n_preallocs */
                        (GInstanceInitFunc) ephy_ellipsizing_label_init
                };

                ephy_ellipsizing_label_type = g_type_register_static (GTK_TYPE_LABEL,
								      "EphyEllipsizingLabel",
                                                                      &our_info, 0);
        }

        return ephy_ellipsizing_label_type;
}

static void
ephy_ellipsizing_label_init (EphyEllipsizingLabel *label)
{
	label->priv = g_new0 (EphyEllipsizingLabelPrivate, 1);

	label->priv->mode = EPHY_ELLIPSIZE_NONE;
}

static void
real_finalize (GObject *object)
{
	EphyEllipsizingLabel *label;

	label = EPHY_ELLIPSIZING_LABEL (object);

	g_free (label->priv->full_text);
	g_free (label->priv);

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

GtkWidget*
ephy_ellipsizing_label_new (const char *string)
{
	EphyEllipsizingLabel *label;

	label = g_object_new (EPHY_TYPE_ELLIPSIZING_LABEL, NULL);
	ephy_ellipsizing_label_set_text (label, string);

	return GTK_WIDGET (label);
}

void
ephy_ellipsizing_label_set_text (EphyEllipsizingLabel *label,
				 const char          *string)
{
	g_return_if_fail (EPHY_IS_ELLIPSIZING_LABEL (label));

	if (ephy_str_is_equal (string, label->priv->full_text)) {
		return;
	}

	g_free (label->priv->full_text);
	label->priv->full_text = g_strdup (string);

	/* Queues a resize as side effect */
	gtk_label_set_text (GTK_LABEL (label), label->priv->full_text);
}

void
ephy_ellipsizing_label_set_markup (EphyEllipsizingLabel *label,
				   const char          *string)
{
	g_return_if_fail (EPHY_IS_ELLIPSIZING_LABEL (label));

	if (ephy_str_is_equal (string, label->priv->full_text)) {
		return;
	}

	g_free (label->priv->full_text);
	label->priv->full_text = g_strdup (string);

	/* Queues a resize as side effect */
	gtk_label_set_markup (GTK_LABEL (label), label->priv->full_text);
}

void
ephy_ellipsizing_label_set_mode (EphyEllipsizingLabel *label,
		               EphyEllipsizeMode mode)
{
	g_return_if_fail (EPHY_IS_ELLIPSIZING_LABEL (label));

	label->priv->mode = mode;
}

static void
real_size_request (GtkWidget *widget, GtkRequisition *requisition)
{
	GTK_WIDGET_CLASS (parent_class)->size_request (widget, requisition);

	/* Don't demand any particular width; will draw ellipsized into whatever size we're given */
	requisition->width = 0;
}

static void
real_size_allocate (GtkWidget *widget, GtkAllocation *allocation)
{
	EphyEllipsizingLabel *label;
	gboolean markup;

	markup = gtk_label_get_use_markup (GTK_LABEL (widget));

	label = EPHY_ELLIPSIZING_LABEL (widget);

	/* This is the bad hack of the century, using private
	 * GtkLabel layout object. If the layout is NULL
	 * then it got blown away since size request,
	 * we just punt in that case, I don't know what to do really.
	 */

	if (GTK_LABEL (label)->layout != NULL) {
		if (label->priv->full_text == NULL) {
			pango_layout_set_text (GTK_LABEL (label)->layout, "", -1);
		} else {
			EphyEllipsizeMode mode;

			if (label->priv->mode != EPHY_ELLIPSIZE_NONE)
				mode = label->priv->mode;

			if (ABS (GTK_MISC (label)->xalign - 0.5) < 1e-12)
				mode = EPHY_ELLIPSIZE_MIDDLE;
			else if (GTK_MISC (label)->xalign < 0.5)
				mode = EPHY_ELLIPSIZE_END;
			else
				mode = EPHY_ELLIPSIZE_START;

			gul_pango_layout_set_text_ellipsized (GTK_LABEL (label)->layout,
							      label->priv->full_text,
							      allocation->width,
							      mode,
							      markup);

			gtk_widget_queue_draw (GTK_WIDGET (label));
		}
	}

	GTK_WIDGET_CLASS (parent_class)->size_allocate (widget, allocation);
}

static gboolean
real_expose_event (GtkWidget *widget, GdkEventExpose *event)
{
	EphyEllipsizingLabel *label;
	GtkRequisition req;

	label = EPHY_ELLIPSIZING_LABEL (widget);

	/* push/pop the actual size so expose draws in the right
	 * place, yes this is bad hack central. Here we assume the
	 * ellipsized text has been set on the layout in size_allocate
	 */
	GTK_WIDGET_CLASS (parent_class)->size_request (widget, &req);
	widget->requisition.width = req.width;
	GTK_WIDGET_CLASS (parent_class)->expose_event (widget, event);
	widget->requisition.width = 0;

	return FALSE;
}


static void
ephy_ellipsizing_label_class_init (EphyEllipsizingLabelClass *klass)
{
	GtkWidgetClass *widget_class;

	parent_class = g_type_class_peek_parent (klass);

	widget_class = GTK_WIDGET_CLASS (klass);

	G_OBJECT_CLASS (klass)->finalize = real_finalize;

	widget_class->size_request = real_size_request;
	widget_class->size_allocate = real_size_allocate;
	widget_class->expose_event = real_expose_event;
}