/* -*- 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 details.

   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>,
 */

#include "gul-ellipsizing-label.h"

#include <string.h>

typedef enum {
        GUL_ELLIPSIZE_START,
        GUL_ELLIPSIZE_MIDDLE,
        GUL_ELLIPSIZE_END
} GulEllipsizeMode;

struct GulEllipsizingLabelDetails
{
	char *full_text;
};

static void gul_ellipsizing_label_class_init (GulEllipsizingLabelClass *class);
static void gul_ellipsizing_label_init       (GulEllipsizingLabel      *label);

static GObjectClass *parent_class = NULL;

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

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

#define ELLIPSIS "..."

/* Caution: this is an _expensive_ function */
static int
measure_string_width (const char  *string,
		      PangoLayout *layout)
{
	int width;
	
	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)
{
	int *widths;
	int *offsets;
	int *cuts;
	int char_len;
	int byte_len;
	const char *p;
	int i;
	PangoLayoutIter *iter;
	PangoLogAttr *attrs;
	
#define BEGINS_UTF8_CHAR(x) (((x) & 0xc0) != 0x80)
	
	char_len = g_utf8_strlen (string, -1);
	byte_len = strlen (string);
	
	widths = g_new (int, char_len);
	offsets = g_new (int, byte_len);

	/* Create a translation table from byte index to char offset */
	p = string;
	i = 0;
	while (*p) {
		int byte_index = p - 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 */
	pango_layout_set_text (layout, string, -1);
	
	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 (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 (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;
}


static char *
gul_string_ellipsize_start (const char *string, PangoLayout *layout, int width)
{
	int resulting_width;
	int *cuts;
	int *widths;
	int char_len;
	const char *p;
	int truncate_offset;

	/* 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);

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

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

	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);

        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);
	
	p = g_utf8_offset_to_pointer (string, truncate_offset);
	
	return g_strconcat (ELLIPSIS, p, NULL);
}

static char *
gul_string_ellipsize_end (const char *string, PangoLayout *layout, int width)
{
	int resulting_width;
	int *cuts;
	int *widths;
	int char_len;
	const char *p;
	int truncate_offset;
	char *result;
	
	/* See explanatory comments in ellipsize_start */
	
	if (*string == '\0')
		return g_strdup ("");

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

	width -= measure_string_width (ELLIPSIS, layout);

	if (width < 0) {
		return g_strdup ("");
	}
	
	compute_character_widths (string, layout, &char_len, &widths, &cuts);
	
        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);

	p = g_utf8_offset_to_pointer (string, truncate_offset);
	
	result = g_malloc ((p - string) + strlen (ELLIPSIS) + 1);
	memcpy (result, string, (p - string));
	strcpy (result + (p - string), ELLIPSIS);

	return result;
}

static char *
gul_string_ellipsize_middle (const char *string, PangoLayout *layout, int width)
{
	int resulting_width;
	int *cuts;
	int *widths;
	int char_len;
	int starting_fragment_byte_len;
	int ending_fragment_byte_index;
	int starting_fragment_length;
	int ending_fragment_offset;
	char *result;
	
	/* See explanatory comments in ellipsize_start */
	
	if (*string == '\0')
		return g_strdup ("");

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

	width -= measure_string_width (ELLIPSIS, layout);

	if (width < 0) {
		return g_strdup ("");
	}
	
	compute_character_widths (string, layout, &char_len, &widths, &cuts);
	
	starting_fragment_length = char_len / 2;
	ending_fragment_offset = starting_fragment_length + 1;
	
	/* Shave off a character at a time from the first and the second half
	 * until we can fit
	 */
	resulting_width -= widths[ending_fragment_offset - 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);	
	
	/* patch the two fragments together with an ellipsis */
	result = g_malloc (strlen (string) + strlen (ELLIPSIS) + 1); /* a bit wasteful, no biggie */

	starting_fragment_byte_len = g_utf8_offset_to_pointer (string, starting_fragment_length) - string;
	ending_fragment_byte_index = g_utf8_offset_to_pointer (string, ending_fragment_offset) - string;
	
	memcpy (result, string, starting_fragment_byte_len);
	strcpy (result + starting_fragment_byte_len, ELLIPSIS);
	strcpy (result + starting_fragment_byte_len + strlen (ELLIPSIS), string + ending_fragment_byte_index);

	return result;
}


/**
 * gul_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,
				      GulEllipsizeMode mode)
{
	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 GUL_ELLIPSIZE_START:
		s = gul_string_ellipsize_start (string, layout, width);
		break;
	case GUL_ELLIPSIZE_MIDDLE:
		s = gul_string_ellipsize_middle (string, layout, width);
		break;
	case GUL_ELLIPSIZE_END:
		s = gul_string_ellipsize_end (string, layout, width);
		break;
	default:
		g_return_if_reached ();
		s = NULL;
	}
	
	pango_layout_set_text (layout, s, -1);
	
	g_free (s);
}

GType 
gul_ellipsizing_label_get_type (void)
{
        static GType gul_ellipsizing_label_type = 0;

        if (gul_ellipsizing_label_type == 0)
        {
                static const GTypeInfo our_info =
                {
                        sizeof (GulEllipsizingLabelClass),
                        NULL, /* base_init */
                        NULL, /* base_finalize */
                        (GClassInitFunc) gul_ellipsizing_label_class_init,
                        NULL,
                        NULL, /* class_data */
                        sizeof (GulEllipsizingLabel),
                        0, /* n_preallocs */
                        (GInstanceInitFunc) gul_ellipsizing_label_init
                };

                gul_ellipsizing_label_type = g_type_register_static (GTK_TYPE_LABEL,
                                                             	     "GulEllipsizingLabel",
                                                                     &our_info, 0);
        }

        return gul_ellipsizing_label_type;
}

static void
gul_ellipsizing_label_init (GulEllipsizingLabel *label)
{
	label->details = g_new0 (GulEllipsizingLabelDetails, 1);
}

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

	label = GUL_ELLIPSIZING_LABEL (object);

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

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

GtkWidget*
gul_ellipsizing_label_new (const char *string)
{
	GulEllipsizingLabel *label;
  
	label = g_object_new (GUL_TYPE_ELLIPSIZING_LABEL, NULL);
	gul_ellipsizing_label_set_text (label, string);
  
	return GTK_WIDGET (label);
}

void
gul_ellipsizing_label_set_text (GulEllipsizingLabel *label, 
				const char          *string)
{
	g_return_if_fail (GUL_IS_ELLIPSIZING_LABEL (label));

	if (gul_str_is_equal (string, label->details->full_text)) {
		return;
	}

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

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

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)
{
	GulEllipsizingLabel *label;

	label = GUL_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->details->full_text == NULL) {
			pango_layout_set_text (GTK_LABEL (label)->layout, "", -1);
		} else {
			GulEllipsizeMode mode;

			if (ABS (GTK_MISC (label)->xalign - 0.5) < 1e-12)
				mode = GUL_ELLIPSIZE_MIDDLE;
			else if (GTK_MISC (label)->xalign < 0.5)
				mode = GUL_ELLIPSIZE_END;
			else
				mode = GUL_ELLIPSIZE_START;
			
			gul_pango_layout_set_text_ellipsized (GTK_LABEL (label)->layout,
							      label->details->full_text,
							      allocation->width,
							      mode);
		}
	}

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

static gboolean
real_expose_event (GtkWidget *widget, GdkEventExpose *event)
{
	GulEllipsizingLabel *label;
	GtkRequisition req;
	
	label = GUL_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
gul_ellipsizing_label_class_init (GulEllipsizingLabelClass *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;
}

