/* Pango
 * viewer.c: Example program to view a UTF-8 encoding file
 *           using Pango to render result.
 *
 * Copyright (C) 1999 Red Hat Software
 *
 * This 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.
 *
 * This 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 this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

#include <gtk/gtk.h>
#include <gdk/gdkx.h>

#include <pango/pango.h>
#include <pango/pangox.h>

#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#define BUFSIZE 1024

typedef struct _Paragraph Paragraph;

/* Structure representing a paragraph
 */
struct _Paragraph {
  char *text;
  int length;
  int height;   /* Height, in pixels */
  PangoLayout *layout;
};

GList *paragraphs;

static PangoFontDescription font_description;
static Paragraph *highlight_para;
static int highlight_offset;

GtkWidget *styles_combo;

static GtkWidget *message_label;
GtkWidget *layout;

PangoContext *context;

static void fill_styles_combo (GtkWidget *combo);

/* Read an entire file into a string
 */
static char *
read_file(char *name)
{
  GString *inbuf;
  int fd;
  char *text;
  char buffer[BUFSIZE];

  fd = open(name, O_RDONLY);
  if (!fd)
    {
      fprintf(stderr, "gscript-viewer: Cannot open %s: %s\n",
	      name, g_strerror (errno));
      return NULL;
    }

  inbuf = g_string_new (NULL);
  while (1)
    {
      int count = read (fd, buffer, BUFSIZE-1);
      if (count < 0)
	{
	  fprintf(stderr, "gscript-viewer: Error reading %s: %s\n",
		  name, g_strerror (errno));
	  g_string_free (inbuf, TRUE);
	  return NULL;
	}
      else if (count == 0)
	break;

      buffer[count] = '\0';

      g_string_append (inbuf, buffer);
    }

  close (fd);

  text = inbuf->str;
  g_string_free (inbuf, FALSE);

  return text;
}

/* Take a UTF8 string and break it into paragraphs on \n characters
 */
static GList *
split_paragraphs (char *text)
{
  char *p = text;
  char *next;
  gunichar wc;
  GList *result = NULL;
  char *last_para = text;
  
  while (*p)
    {
      wc = g_utf8_get_char (p);
      next = g_utf8_next_char (p);
      if (wc == (gunichar)-1)
	{
	  fprintf (stderr, "gscript-viewer: Invalid character in input\n");
	  g_list_foreach (result, (GFunc)g_free, NULL);
	  return NULL;
	}
      if (!*p || !wc || wc == '\n')
	{
	  Paragraph *para = g_new (Paragraph, 1);
	  para->text = last_para;
	  para->length = p - last_para;
	  para->layout = pango_layout_new (context);
	  pango_layout_set_text (para->layout, para->text, para->length);
	  para->height = 0;

	  last_para = next;
	    
	  result = g_list_prepend (result, para);
	}
      if (!wc) /* incomplete character at end */
	break;
      p = next;
    }

  return g_list_reverse (result);
}

/* Given an x-y position, return the paragraph and offset
 * within the paragraph of the click.
 */
gboolean
xy_to_cp (int width, int x, int y, Paragraph **para_return, int *index)
{
  GList *para_list;
  int height = 0;

  *para_return = NULL;

  para_list = paragraphs;
  while (para_list && height < y)
    {
      Paragraph *para = para_list->data;
      
      if (height + para->height >= y)
	{
	  gboolean result = pango_layout_xy_to_index (para->layout, x * 1000, (y - height) * 1000, 
						      index, NULL);
	  if (result && para_return)
	    *para_return = para;

	  return result;
	}
      
      height += para->height;
      para_list = para_list->next;
    }

  return FALSE;
}

/* Given a paragraph and offset in that paragraph, find the
 * bounding rectangle for the character at the offset.
 */ 
void
char_bounds (Paragraph *para, int index, int width, PangoRectangle *rect)
{
  GList *para_list;
  
  int height = 0;
  
  para_list = paragraphs;
  while (para_list)
    {
      Paragraph *cur_para = para_list->data;
      
      if (cur_para == para)
	{
	  PangoRectangle pos;

	  pango_layout_index_to_pos (cur_para->layout, index, &pos);

	  rect->x = MIN (pos.x, pos.x + pos.width) / 1000;
	  rect->width = ABS (pos.width) / 1000;
	  rect->y = height + pos.y / 1000;
	  rect->height = pos.height / 1000;
	}
      
      height += cur_para->height;
      para_list = para_list->next;
    }
}

/* XOR a rectangle over a given character
 */
void
xor_char (GtkWidget *layout, GdkRectangle *clip_rect,
	  Paragraph *para, int offset)
{
  static GdkGC *gc;
  PangoRectangle rect;		/* GdkRectangle in 1.2 is too limited
				 */
  if (!gc)
    {
      GdkGCValues values;
      values.foreground = layout->style->white.pixel ?
	layout->style->white : layout->style->black;
      values.function = GDK_XOR;
      gc = gdk_gc_new_with_values (GTK_LAYOUT (layout)->bin_window,
				   &values,
				   GDK_GC_FOREGROUND | GDK_GC_FUNCTION);
    }

  gdk_gc_set_clip_rectangle (gc, clip_rect);

  char_bounds (para, offset, layout->allocation.width, &rect);

  rect.y -= GTK_LAYOUT (layout)->yoffset;

  if ((rect.y + rect.height >= 0) && (rect.y < layout->allocation.height))
    gdk_draw_rectangle (GTK_LAYOUT (layout)->bin_window, gc, TRUE,
			rect.x, rect.y, rect.width, rect.height);
}

/* Handle a size allocation by re-laying-out each paragraph to
 * the new width, setting the new size for the layout and
 * then queing a redraw
 */
void
size_allocate (GtkWidget *layout, GtkAllocation *allocation)
{
  GList *tmp_list;
  int height = 0;
  PangoDirection base_dir = pango_context_get_base_dir (context);

  tmp_list = paragraphs;
  while (tmp_list)
    {
      Paragraph *para = tmp_list->data;
      PangoRectangle logical_rect;
	  
      tmp_list = tmp_list->next;

      pango_layout_set_alignment (para->layout,
				  base_dir == PANGO_DIRECTION_LTR ? PANGO_ALIGN_LEFT : PANGO_ALIGN_RIGHT);
      pango_layout_set_width (para->layout, layout->allocation.width * 1000);

      pango_layout_get_extents (para->layout, NULL, &logical_rect);
      para->height = logical_rect.height / 1000;
      
      height += para->height;
    }

  gtk_layout_set_size (GTK_LAYOUT (layout), allocation->width, height);

  if (GTK_LAYOUT (layout)->yoffset + allocation->height > height)
    gtk_adjustment_set_value (GTK_LAYOUT (layout)->vadjustment, height - allocation->height);
}

/* Handle a draw/expose by finding the paragraphs that intersect
 * the region and reexposing them.
 */
void
draw (GtkWidget *layout, GdkRectangle *area)
{
  GList *tmp_list;
  int height = 0;
  
  gdk_draw_rectangle (GTK_LAYOUT (layout)->bin_window,
		      layout->style->base_gc[layout->state],
		      TRUE,
		      area->x, area->y, 
		      area->width, area->height);
  
  gdk_gc_set_clip_rectangle (layout->style->text_gc[layout->state], area);

  tmp_list = paragraphs;
  while (tmp_list &&
	 height < area->y + area->height + GTK_LAYOUT (layout)->yoffset)
    {
      Paragraph *para = tmp_list->data;
      tmp_list = tmp_list->next;
      
      if (height + para->height >= GTK_LAYOUT (layout)->yoffset + area->y)
	pango_x_render_layout (GDK_DISPLAY(), GDK_WINDOW_XWINDOW (GTK_LAYOUT (layout)->bin_window),
			       GDK_GC_XGC (layout->style->text_gc[GTK_STATE_NORMAL]),
			       para->layout, 0, height - GTK_LAYOUT (layout)->yoffset);

      height += para->height;
    }

  gdk_gc_set_clip_rectangle (layout->style->text_gc[layout->state], NULL);

  if (highlight_para)
    xor_char (layout, area, highlight_para, highlight_offset);
}

gboolean
expose (GtkWidget *layout, GdkEventExpose *event)
{
  if (event->window == GTK_LAYOUT (layout)->bin_window)
    draw (layout, &event->area);

  return TRUE;
}

void
button_press (GtkWidget *layout, GdkEventButton *event)
{
  Paragraph *para = NULL;
  int offset;
  gchar *message;
  
  xy_to_cp (layout->allocation.width,
	    event->x, event->y + GTK_LAYOUT (layout)->yoffset,
	    &para, &offset);

  if (highlight_para)
    xor_char (layout, NULL, highlight_para, highlight_offset);

  highlight_para = para;
  highlight_offset = offset;
  
  if (para)
    {
      gunichar wc;

      wc = g_utf8_get_char (para->text + offset);
      message = g_strdup_printf ("Current char: U%04x", wc);
      
      xor_char (layout, NULL, highlight_para, highlight_offset);
    }
  else
    message = g_strdup_printf ("Current char:");

  gtk_label_set_text (GTK_LABEL (message_label), message);
  g_free (message);
}

static void
checkbutton_toggled (GtkWidget *widget, gpointer data)
{
  GList *para_list;
  
  pango_context_set_base_dir (context, GTK_TOGGLE_BUTTON (widget)->active ? PANGO_DIRECTION_RTL : PANGO_DIRECTION_LTR);

  para_list = paragraphs;
  while (para_list)
    {
      Paragraph *para = para_list->data;

      pango_layout_context_changed (para->layout);
      para_list = para_list->next;
    }

  gtk_widget_queue_resize (layout);
}

static void
reload_font ()
{
  GList *para_list;

  pango_context_set_font_description (context, &font_description);

  para_list = paragraphs;
  while (para_list)
    {
      Paragraph *para = para_list->data;

      pango_layout_context_changed (para->layout);
      para_list = para_list->next;
    }

  if (layout)
    gtk_widget_queue_resize (layout);
}

void
set_family (GtkWidget *entry, gpointer data)
{
  font_description.family_name = gtk_editable_get_chars (GTK_EDITABLE (entry), 0, -1);
  fill_styles_combo (styles_combo);
}

void
set_style (GtkWidget *entry, gpointer data)
{
  char *str = gtk_editable_get_chars (GTK_EDITABLE (entry), 0, -1);
  PangoFontDescription *tmp_desc;
  
  tmp_desc = pango_font_description_from_string (str);

  font_description.style = tmp_desc->style;
  font_description.variant = tmp_desc->variant;
  font_description.weight = tmp_desc->weight;
  font_description.stretch = tmp_desc->stretch;

  pango_font_description_free (tmp_desc);
  g_free (str);
  
  reload_font ();
}

void
font_size_changed (GtkAdjustment *adj)
{
  font_description.size = (int)(adj->value * PANGO_SCALE + 0.5);
  reload_font();
}

static int
compare_font_descriptions (const PangoFontDescription *a, const PangoFontDescription *b)
{
  int val = strcmp (a->family_name, b->family_name);
  if (val != 0)
    return val;

  if (a->weight != b->weight)
    return a->weight - b->weight;

  if (a->style != b->style)
    return a->style - b->style;
  
  if (a->stretch != b->stretch)
    return a->stretch - b->stretch;

  if (a->variant != b->variant)
    return a->variant - b->variant;

  return 0;
}

static int
font_description_sort_func (const void *a, const void *b)
{
  return compare_font_descriptions (*(PangoFontDescription **)a, *(PangoFontDescription **)b);
}

typedef struct 
{
  PangoFontDescription **descs;
  int n_descs;
} FontDescInfo;

static void
free_info (FontDescInfo *info)
{
  pango_font_descriptions_free (info->descs, info->n_descs);
}

static void
fill_styles_combo (GtkWidget *combo)
{
  int i;
  GList *style_list = NULL;
  
  FontDescInfo *info = g_new (FontDescInfo, 1);
  pango_context_list_fonts (context, font_description.family_name, &info->descs, &info->n_descs);
  gtk_object_set_data_full (GTK_OBJECT (combo), "descs", info, (GtkDestroyNotify)free_info);

  qsort (info->descs, info->n_descs, sizeof(PangoFontDescription *), font_description_sort_func);

  for (i=0; i<info->n_descs; i++)
    {
      char *str;
	
      PangoFontDescription tmp_desc;

      tmp_desc = *info->descs[i];
      tmp_desc.family_name = NULL;
      tmp_desc.size = 0;
      
      str = pango_font_description_to_string (&tmp_desc);
      style_list = g_list_prepend (style_list, str);
    }

  style_list = g_list_reverse (style_list);

  gtk_combo_set_popdown_strings (GTK_COMBO (combo), style_list);
  g_list_foreach (style_list, (GFunc)g_free, NULL);
}

static GtkWidget *
make_styles_combo ()
{
  GtkWidget *combo;

  combo = gtk_combo_new ();
  gtk_combo_set_value_in_list (GTK_COMBO (combo), TRUE, FALSE);
  gtk_editable_set_editable (GTK_EDITABLE (GTK_COMBO (combo)->entry), FALSE);

  gtk_signal_connect (GTK_OBJECT (GTK_COMBO (combo)->entry), "changed",
		      GTK_SIGNAL_FUNC (set_style), NULL);

  styles_combo = combo;
  fill_styles_combo (combo);
  
  return combo;
}

static int
cmp_strings (const void *a, const void *b)
{
  return strcmp (*(const char **)a, *(const char **)b);
}

GtkWidget *
make_families_menu ()
{
  GtkWidget *combo;
  gchar **families;
  int n_families;
  GList *family_list = NULL;
  int i;
  
  pango_context_list_families (context, &families, &n_families);
  qsort (families, n_families, sizeof(char *), cmp_strings);

  for (i=0; i<n_families; i++)
    family_list = g_list_prepend (family_list, families[i]);

  family_list = g_list_reverse (family_list);
  
  combo = gtk_combo_new ();
  gtk_combo_set_popdown_strings (GTK_COMBO (combo), family_list);
  gtk_combo_set_value_in_list (GTK_COMBO (combo), TRUE, FALSE);
  gtk_editable_set_editable (GTK_EDITABLE (GTK_COMBO (combo)->entry), FALSE);

  gtk_entry_set_text (GTK_ENTRY (GTK_COMBO (combo)->entry), font_description.family_name);

  gtk_signal_connect (GTK_OBJECT (GTK_COMBO (combo)->entry), "changed",
		      GTK_SIGNAL_FUNC (set_family), NULL);
  
  g_list_free (family_list);
  pango_font_map_free_families (families, n_families);

  return combo;
}


GtkWidget *
make_font_selector (void)
{
  GtkWidget *hbox;
  GtkWidget *util_hbox;
  GtkWidget *label;
  GtkWidget *option_menu;
  GtkWidget *spin_button;
  GtkAdjustment *adj;
  
  hbox = gtk_hbox_new (FALSE, 4);
  
  util_hbox = gtk_hbox_new (FALSE, 2);
  label = gtk_label_new ("Family:");
  gtk_box_pack_start (GTK_BOX (util_hbox), label, FALSE, FALSE, 0);
  option_menu = make_families_menu ();
  gtk_box_pack_start (GTK_BOX (util_hbox), option_menu, FALSE, FALSE, 0);
  
  gtk_box_pack_start (GTK_BOX (hbox), util_hbox, FALSE, FALSE, 0);

  util_hbox = gtk_hbox_new (FALSE, 2);
  label = gtk_label_new ("Style:");
  gtk_box_pack_start (GTK_BOX (util_hbox), label, FALSE, FALSE, 0);
  option_menu = make_styles_combo ();
  gtk_box_pack_start (GTK_BOX (util_hbox), option_menu, FALSE, FALSE, 0);
  
  gtk_box_pack_start (GTK_BOX (hbox), util_hbox, FALSE, FALSE, 0);

  util_hbox = gtk_hbox_new (FALSE, 2);
  label = gtk_label_new ("Size:");
  gtk_box_pack_start (GTK_BOX (util_hbox), label, FALSE, FALSE, 0);
  spin_button = gtk_spin_button_new (NULL, 1., 0);
  gtk_box_pack_start (GTK_BOX (util_hbox), spin_button, FALSE, FALSE, 0);

  gtk_box_pack_start (GTK_BOX (hbox), util_hbox, FALSE, FALSE, 0);

  adj = gtk_spin_button_get_adjustment (GTK_SPIN_BUTTON (spin_button));
  adj->value = font_description.size / 1000.;
  adj->lower = 0;
  adj->upper = 1024;
  adj->step_increment = 1;
  adj->page_size = 10;
  gtk_adjustment_changed (adj);
  gtk_adjustment_value_changed (adj);

  gtk_signal_connect (GTK_OBJECT (adj), "value_changed",
		      GTK_SIGNAL_FUNC (font_size_changed), NULL);
  
  return hbox;
}

int 
main (int argc, char **argv)
{
  char *text;
  GtkWidget *window;
  GtkWidget *scrollwin;
  GtkWidget *vbox, *hbox;
  GtkWidget *frame;
  GtkWidget *checkbutton;

  gtk_init (&argc, &argv);
  
  if (argc != 2)
    {
      fprintf (stderr, "Usage: gscript-viewer FILE\n");
      exit(1);
    }

  /* Create the list of paragraphs from the supplied file
   */
  text = read_file (argv[1]);
  if (!text)
    exit(1);

  context = pango_x_get_context (GDK_DISPLAY());

  paragraphs = split_paragraphs (text);

  pango_context_set_lang (context, "en_US");
  pango_context_set_base_dir (context, PANGO_DIRECTION_LTR);

  font_description.family_name = g_strdup ("sans");
  font_description.style = PANGO_STYLE_NORMAL;
  font_description.variant = PANGO_VARIANT_NORMAL;
  font_description.weight = 500;
  font_description.stretch = PANGO_STRETCH_NORMAL;
  font_description.size = 16000;

  pango_context_set_font_description (context, &font_description);

  /* Create the user interface
   */
  window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
  gtk_window_set_default_size (GTK_WINDOW (window), 400, 400);

  gtk_signal_connect (GTK_OBJECT (window), "destroy",
		      GTK_SIGNAL_FUNC (gtk_main_quit), NULL);

  vbox = gtk_vbox_new (FALSE, 4);
  gtk_container_add (GTK_CONTAINER (window), vbox);

  hbox = make_font_selector ();
  gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 0);
  
  scrollwin = gtk_scrolled_window_new (NULL, NULL);
  gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrollwin),
				  GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
  
  gtk_box_pack_start (GTK_BOX (vbox), scrollwin, TRUE, TRUE, 0);
  
  layout = gtk_layout_new (NULL, NULL);
  gtk_widget_set_events (layout, GDK_BUTTON_PRESS_MASK);
  gtk_widget_set_app_paintable (layout, TRUE);

  gtk_signal_connect (GTK_OBJECT (layout), "size_allocate",
		      GTK_SIGNAL_FUNC (size_allocate), paragraphs);
  gtk_signal_connect (GTK_OBJECT (layout), "expose_event",
		      GTK_SIGNAL_FUNC (expose), paragraphs);
  gtk_signal_connect (GTK_OBJECT (layout), "draw",
		      GTK_SIGNAL_FUNC (draw), paragraphs);
  gtk_signal_connect (GTK_OBJECT (layout), "button_press_event",
		      GTK_SIGNAL_FUNC (button_press), paragraphs);

  gtk_container_add (GTK_CONTAINER (scrollwin), layout);

  frame = gtk_frame_new (NULL);
  gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_IN);
  gtk_box_pack_start (GTK_BOX (vbox), frame, FALSE, FALSE, 0);

  message_label = gtk_label_new ("Current char:");
  gtk_misc_set_padding (GTK_MISC (message_label), 1, 1);
  gtk_misc_set_alignment (GTK_MISC (message_label), 0.0, 0.5);
  gtk_container_add (GTK_CONTAINER (frame), message_label);

  checkbutton = gtk_check_button_new_with_label ("Use RTL global direction");
  gtk_signal_connect (GTK_OBJECT (checkbutton), "toggled",
		      GTK_SIGNAL_FUNC (checkbutton_toggled), NULL);
  gtk_box_pack_start (GTK_BOX (vbox), checkbutton, FALSE, FALSE, 0);

  gtk_widget_show_all (window);

  gtk_main ();
  
  return 0;
}