#include <unistd.h>
#include <string.h>
#include <metacity-private/util.h>
#include <metacity-private/theme.h>
#include <metacity-private/theme-parser.h>
#include <metacity-private/preview-widget.h>
#include <signal.h>
#include <errno.h>

/* We have to #undef this as metacity #defines these. */
#undef _
#undef N_

#include <libgnomeui/gnome-icon-theme.h>
#include <config.h>

#include "theme-thumbnail.h"
#include "capplet-util.h"

static gint child_pid;
#define ICON_SIZE_WIDTH 150
#define ICON_SIZE_HEIGHT 150

typedef struct
{
  gboolean set;
  GByteArray *data;
  gchar *meta_theme_name;
  ThemeThumbnailFunc func;
  gpointer user_data;
  GDestroyNotify destroy;
  GIOChannel *channel;
  guint watch_id;
} ThemeThumbnailAsyncData;


GHashTable *theme_hash = NULL;
ThemeThumbnailAsyncData async_data;


/* Protocol */

/* Our protocol is pretty simple.  The parent process will write four strings
 * (separated by a '\000') They are the widget theme, the wm theme, the icon
 * theme, and the font string.  Then, it will wait for the child to write back
 * the data.  The parent expects ICON_SIZE_WIDTH * ICON_SIZE_HEIGHT * 4 bytes of
 * information.  After that, the child is ready for the next theme to render.
 */

enum
{
  READY_FOR_THEME,
  READING_CONTROL_THEME_NAME,
  READING_WM_THEME_NAME,
  READING_ICON_THEME_NAME,
  READING_APPLICATION_FONT,
  WRITING_PIXBUF_DATA
};

typedef struct
{
  gint status;
  GByteArray *control_theme_name;
  GByteArray *wm_theme_name;
  GByteArray *icon_theme_name;
  GByteArray *application_font;
} ThemeThumbnailData;

int pipe_to_factory_fd[2];
int pipe_from_factory_fd[2];

static void
fake_expose_widget (GtkWidget *widget,
		    GdkPixmap *pixmap,
		    GdkRectangle *area)
{
  GdkWindow *tmp_window;
  GdkEventExpose event;

  event.type = GDK_EXPOSE;
  event.window = pixmap;
  event.send_event = FALSE;
  event.area = area ? *area : widget->allocation;
  event.region = NULL;
  event.count = 0;

  tmp_window = widget->window;
  widget->window = pixmap;
  gtk_widget_send_expose (widget, (GdkEvent *) &event);
  widget->window = tmp_window;
}



static void
hbox_foreach (GtkWidget *widget,
	      gpointer   data)
{
  gtk_widget_realize (widget);
  gtk_widget_map (widget);
  gtk_widget_ensure_style (widget);
  fake_expose_widget (widget, (GdkPixmap *) data, NULL);
}

static void
create_image (ThemeThumbnailData *theme_thumbnail_data,
	      GdkPixbuf          *pixbuf)
{
  GtkWidget *window;
  GtkWidget *preview;
  GtkWidget *align;
  GtkWidget *stock_button;

  GtkRequisition requisition;
  GtkAllocation allocation;
  GdkPixmap *pixmap;
  GdkVisual *visual;
  MetaFrameFlags flags;
  MetaTheme *theme = NULL;
  GtkSettings *settings;
  GtkIconTheme *icon_theme;
  GdkPixbuf *folder_icon = NULL;
  GtkIconInfo *folder_icon_info;
  const gchar *filename;
  gchar *example_icon_name;
  int width, height;
  double scale;

  settings = gtk_settings_get_default ();
  g_object_set (G_OBJECT (settings),
		"gtk-theme-name", (char *) theme_thumbnail_data->control_theme_name->data,
		"gtk-font-name", (char *) theme_thumbnail_data->application_font->data,
		"gtk-icon-theme-name", (char *) theme_thumbnail_data->icon_theme_name->data,
		NULL);
  theme = meta_theme_load ((char *) theme_thumbnail_data->wm_theme_name->data, NULL);

  flags = META_FRAME_ALLOWS_DELETE |
    META_FRAME_ALLOWS_MENU |
    META_FRAME_ALLOWS_MINIMIZE |
    META_FRAME_ALLOWS_MAXIMIZE |
    META_FRAME_ALLOWS_VERTICAL_RESIZE |
    META_FRAME_ALLOWS_HORIZONTAL_RESIZE |
    META_FRAME_HAS_FOCUS |
    META_FRAME_ALLOWS_SHADE |
    META_FRAME_ALLOWS_MOVE;

  window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
  preview = meta_preview_new ();
  gtk_container_add (GTK_CONTAINER (window), preview);
  gtk_widget_realize (window);
  gtk_widget_realize (preview);
  align = gtk_alignment_new (0.0, 0.0, 0.0, 0.0);
  gtk_container_add (GTK_CONTAINER (preview), align);
  gtk_container_set_border_width (GTK_CONTAINER (align), 5);
  stock_button = gtk_button_new_from_stock (GTK_STOCK_OPEN);
  gtk_container_add (GTK_CONTAINER (align), stock_button);

  gtk_widget_show_all (preview);
  gtk_widget_realize (align);
  gtk_widget_realize (stock_button);
  gtk_widget_realize (GTK_BIN (stock_button)->child);
  gtk_widget_map (stock_button);
  gtk_widget_map (GTK_BIN (stock_button)->child);

  meta_preview_set_frame_flags (META_PREVIEW (preview), flags);
  meta_preview_set_theme (META_PREVIEW (preview), theme);
  meta_preview_set_title (META_PREVIEW (preview), "");


  gtk_window_set_default_size (GTK_WINDOW (window), ICON_SIZE_WIDTH, ICON_SIZE_HEIGHT);

  gtk_widget_size_request (window, &requisition);
  allocation.x = 0;
  allocation.y = 0;
  allocation.width = ICON_SIZE_WIDTH;
  allocation.height = ICON_SIZE_HEIGHT;
  gtk_widget_size_allocate (window, &allocation);
  gtk_widget_size_request (window, &requisition);

  /* Create a pixmap */
  visual = gtk_widget_get_visual (window);
  pixmap = gdk_pixmap_new (NULL, ICON_SIZE_WIDTH, ICON_SIZE_HEIGHT, visual->depth);
  gdk_drawable_set_colormap (GDK_DRAWABLE (pixmap), gtk_widget_get_colormap (window));

  /* Draw the window */
  gtk_widget_ensure_style (window);
  g_assert (window->style);
  g_assert (window->style->font_desc);

  fake_expose_widget (window, pixmap, NULL);
  fake_expose_widget (preview, pixmap, NULL);
  /* we call this again here because the preview sometimes draws into the area
   * of the contents, see http://bugzilla.gnome.org/show_bug.cgi?id=351389 */
  fake_expose_widget (window, pixmap, &align->allocation);
  fake_expose_widget (stock_button, pixmap, NULL);
  gtk_container_foreach (GTK_CONTAINER (GTK_BIN (GTK_BIN (stock_button)->child)->child),
			 hbox_foreach,
			 pixmap);
  fake_expose_widget (GTK_BIN (stock_button)->child, pixmap, NULL);


  gdk_pixbuf_get_from_drawable (pixbuf, pixmap, NULL, 0, 0, 0, 0, ICON_SIZE_WIDTH, ICON_SIZE_HEIGHT);

  /* Handle the icon theme */
  icon_theme = gtk_icon_theme_new ();
  gtk_icon_theme_set_custom_theme (icon_theme, (char *) theme_thumbnail_data->icon_theme_name->data);

  folder_icon_info = NULL;
  /* Get the Example icon name in the theme if specified */
  example_icon_name = gtk_icon_theme_get_example_icon_name (icon_theme);
  if (example_icon_name != NULL)
    folder_icon_info = gtk_icon_theme_lookup_icon (icon_theme, example_icon_name, 48, GTK_ICON_LOOKUP_FORCE_SVG);
  g_free (example_icon_name);

  /* If an Example is not specified, fall back to using the folder icons in
     the order of Icon Nameing Spec, "gnome-fs-directory", and "folder" */
  if (folder_icon_info == NULL)
    folder_icon_info = gtk_icon_theme_lookup_icon (icon_theme, "x-directory-normal", 48, GTK_ICON_LOOKUP_FORCE_SVG);
  if (folder_icon_info == NULL)
    folder_icon_info = gtk_icon_theme_lookup_icon (icon_theme, "gnome-fs-directory", 48, GTK_ICON_LOOKUP_FORCE_SVG);
  if (folder_icon_info == NULL)
    folder_icon_info = gtk_icon_theme_lookup_icon (icon_theme, "folder", 48, GTK_ICON_LOOKUP_FORCE_SVG);
 
  g_object_unref (icon_theme);

  if (folder_icon_info != NULL) 
    {
      filename = gtk_icon_info_get_filename (folder_icon_info);
     
      if (filename != NULL)
	{
	  folder_icon = gdk_pixbuf_new_from_file (filename, NULL);
	}

      gtk_icon_info_free (folder_icon_info);
    }

  /* render the icon to the thumbnail */
  if (folder_icon == NULL)
    folder_icon = gtk_widget_render_icon (window, 
					  GTK_STOCK_MISSING_IMAGE, 
					  GTK_ICON_SIZE_DIALOG,
					  NULL);

  /* Some icons don't come back at the requested dimensions, so we need to scale
   * them. The width is usually the largest dimension for icons that come at
   * irregular sizes, so use this to calculate the scale factor
   */
  scale = 48.0 / gdk_pixbuf_get_width (folder_icon);
  width = 48;
  height = scale * gdk_pixbuf_get_height (folder_icon);

  gdk_pixbuf_composite (folder_icon,
			pixbuf,
			align->allocation.x + align->allocation.width - width - 5,
			align->allocation.y + align->allocation.height - height - 5,
			width,
			height,
			align->allocation.x + align->allocation.width - width - 5,
			align->allocation.y + align->allocation.height - height - 5,
			scale,
			scale,
			GDK_INTERP_BILINEAR, 255);
  g_object_unref (folder_icon);
  gtk_widget_destroy (window);
}

static void
handle_bytes (const gchar        *buffer,
	      gint                bytes_read,
	      ThemeThumbnailData *theme_thumbnail_data)
{
  const gchar *ptr;
  ptr = buffer;

  while (bytes_read > 0)
    {
      char *nil;
      switch (theme_thumbnail_data->status)
	{
	case READY_FOR_THEME:
	case READING_CONTROL_THEME_NAME:
	  theme_thumbnail_data->status = READING_CONTROL_THEME_NAME;
	  nil = memchr (ptr, '\000', bytes_read);
	  if (nil == NULL)
	    {
	      g_byte_array_append (theme_thumbnail_data->control_theme_name, ptr, bytes_read);
	      bytes_read = 0;
	    }
	  else
	    {
	      g_byte_array_append (theme_thumbnail_data->control_theme_name, ptr, nil - ptr + 1);
	      bytes_read -= (nil - ptr + 1);
	      ptr = nil + 1;
	      theme_thumbnail_data->status = READING_WM_THEME_NAME;
	    }
	  break;
	case READING_WM_THEME_NAME:
	  nil = memchr (ptr, '\000', bytes_read);
	  if (nil == NULL)
	    {
	      g_byte_array_append (theme_thumbnail_data->wm_theme_name, ptr, bytes_read);
	      bytes_read = 0;
	    }
	  else
	    {
	      g_byte_array_append (theme_thumbnail_data->wm_theme_name, ptr, nil - ptr + 1);
	      bytes_read -= (nil - ptr + 1);
	      ptr = nil + 1;
	      theme_thumbnail_data->status = READING_ICON_THEME_NAME;
	    }
	  break;
	case READING_ICON_THEME_NAME:
	  nil = memchr (ptr, '\000', bytes_read);
	  if (nil == NULL)
	    {
	      g_byte_array_append (theme_thumbnail_data->icon_theme_name, ptr, bytes_read);
	      bytes_read = 0;
	    }
	  else
	    {
	      g_byte_array_append (theme_thumbnail_data->icon_theme_name, ptr, nil - ptr + 1);
	      bytes_read -= (nil - ptr + 1);
	      ptr = nil + 1;
	      theme_thumbnail_data->status = READING_APPLICATION_FONT;
	    }
	  break;
	case READING_APPLICATION_FONT:
	  nil = memchr (ptr, '\000', bytes_read);
	  if (nil == NULL)
	    {
	      g_byte_array_append (theme_thumbnail_data->application_font, ptr, bytes_read);
	      bytes_read = 0;
	    }
	  else
	    {
	      g_byte_array_append (theme_thumbnail_data->application_font, ptr, nil - ptr + 1);
	      bytes_read -= (nil - ptr + 1);
	      ptr = nil + 1;
	      theme_thumbnail_data->status = WRITING_PIXBUF_DATA;
	    }
	  break;
	default:
	  g_assert_not_reached ();
	}
    }
}

static gboolean
message_from_capplet (GIOChannel   *source,
		      GIOCondition  condition,
		      gpointer      data)
{
  gchar buffer[1024];
  GIOStatus status;
  gsize bytes_read;
  GdkPixbuf *pixbuf;
  gint i, rowstride;
  char *pixels;
  ThemeThumbnailData *theme_thumbnail_data;

  theme_thumbnail_data = (ThemeThumbnailData *)data;

  status = g_io_channel_read_chars (source,
                                    buffer,
                                    1024,
                                    &bytes_read,
                                    NULL);

  switch (status)
    {
    case G_IO_STATUS_NORMAL:
      handle_bytes (buffer, bytes_read, theme_thumbnail_data);

      if (theme_thumbnail_data->status == WRITING_PIXBUF_DATA)
	{
	  pixbuf = gdk_pixbuf_new (GDK_COLORSPACE_RGB, TRUE, 8, ICON_SIZE_WIDTH, ICON_SIZE_HEIGHT);
	  create_image (theme_thumbnail_data, pixbuf);
	  rowstride = gdk_pixbuf_get_rowstride (pixbuf);
	  pixels = gdk_pixbuf_get_pixels (pixbuf);
	  for (i = 0; i < ICON_SIZE_HEIGHT; i ++)
	    {
	      write (pipe_from_factory_fd[1], pixels + (rowstride)*i, ICON_SIZE_WIDTH * gdk_pixbuf_get_n_channels (pixbuf));
	    }
	  g_object_unref (pixbuf);
	  theme_thumbnail_data->status = READY_FOR_THEME;
	  g_byte_array_set_size (theme_thumbnail_data->control_theme_name, 0);
	  g_byte_array_set_size (theme_thumbnail_data->wm_theme_name, 0);
	  g_byte_array_set_size (theme_thumbnail_data->icon_theme_name, 0);
	  g_byte_array_set_size (theme_thumbnail_data->application_font, 0);
	}
      return TRUE;
    case G_IO_STATUS_AGAIN:
      return TRUE;
    case G_IO_STATUS_EOF:
    case G_IO_STATUS_ERROR:
      _exit (0);
    default:
      g_assert_not_reached ();
    }

  return TRUE;
}

static gboolean
message_from_child (GIOChannel   *source,
		    GIOCondition  condition,
		    gpointer      data)
{

  gchar buffer[1024];
  GIOStatus status;
  gsize bytes_read;

  if (async_data.set == FALSE)
    return TRUE;

  status = g_io_channel_read_chars (source,
                                    buffer,
                                    1024,
                                    &bytes_read,
                                    NULL);
  switch (status)
    {
    case G_IO_STATUS_NORMAL:
      g_byte_array_append (async_data.data, buffer, bytes_read);
      if (async_data.data->len == ICON_SIZE_WIDTH * ICON_SIZE_HEIGHT * 4)
	{
	  GdkPixbuf *pixbuf;
	  GdkPixbuf *scaled_pixbuf;
	  gchar *pixels;
	  gint i, rowstride;

	  pixbuf = gdk_pixbuf_new (GDK_COLORSPACE_RGB, TRUE, 8, ICON_SIZE_WIDTH, ICON_SIZE_HEIGHT);
	  pixels = gdk_pixbuf_get_pixels (pixbuf);
	  rowstride = gdk_pixbuf_get_rowstride (pixbuf);
	  for (i = 0; i < ICON_SIZE_HEIGHT; i++)
	    memcpy (pixels + rowstride * i, async_data.data->data + 4 * ICON_SIZE_WIDTH * i, ICON_SIZE_WIDTH * 4);

	  scaled_pixbuf = gdk_pixbuf_scale_simple (pixbuf, ICON_SIZE_WIDTH/2, ICON_SIZE_HEIGHT/2, GDK_INTERP_BILINEAR);
	  g_hash_table_insert (theme_hash, g_strdup(async_data.meta_theme_name), scaled_pixbuf);
	  g_object_unref (pixbuf);

 	  (* async_data.func) (scaled_pixbuf, async_data.user_data);
	  if (async_data.destroy)
	    (* async_data.destroy) (async_data.user_data);

	  /* Clean up async_data */
	  g_free (async_data.meta_theme_name);
	  g_source_remove (async_data.watch_id);
	  g_io_channel_unref (async_data.channel);

	  /* reset async_data */
	  async_data.meta_theme_name = NULL;
	  async_data.channel = NULL;
	  async_data.func = NULL;
	  async_data.user_data = NULL;
	  async_data.destroy = NULL;
	  async_data.set = FALSE;
	  g_byte_array_set_size (async_data.data, 0);
	}
      return TRUE;
    case G_IO_STATUS_AGAIN:
      return TRUE;

    case G_IO_STATUS_EOF:
    case G_IO_STATUS_ERROR:
      return TRUE;
    default:
      g_assert_not_reached ();
    }

  return TRUE;
}

void  
theme_thumbnail_invalidate_cache (GnomeThemeMetaInfo *meta_theme_info)
{
  gboolean success;

  success = g_hash_table_remove (theme_hash, meta_theme_info->name);
  printf ("Success is %d\n", success);
}

GdkPixbuf *
generate_theme_thumbnail (GnomeThemeMetaInfo *meta_theme_info,
			  gboolean            clear_cache)
{
  GdkPixbuf *retval = NULL;
  GdkPixbuf *pixbuf = NULL;
  gint i, rowstride;
  char *pixels;

  g_return_val_if_fail (async_data.set == FALSE, NULL);

  pixbuf = g_hash_table_lookup (theme_hash, meta_theme_info->name);
  if (pixbuf != NULL)
    {
      if (clear_cache)
	g_hash_table_remove (theme_hash, meta_theme_info->name);
      else
	return pixbuf;
    }

  if (!pipe_to_factory_fd[1] || !pipe_from_factory_fd[0])
    return NULL;

  pixbuf = gdk_pixbuf_new (GDK_COLORSPACE_RGB, TRUE, 8, ICON_SIZE_WIDTH, ICON_SIZE_HEIGHT);
  write (pipe_to_factory_fd[1], meta_theme_info->gtk_theme_name, strlen (meta_theme_info->gtk_theme_name) + 1);
  write (pipe_to_factory_fd[1], meta_theme_info->metacity_theme_name, strlen (meta_theme_info->metacity_theme_name) + 1);
  write (pipe_to_factory_fd[1], meta_theme_info->icon_theme_name, strlen (meta_theme_info->icon_theme_name) + 1);
  if (meta_theme_info->application_font == NULL)
    write (pipe_to_factory_fd[1], "Sans 10", strlen ("Sans 10") + 1);
  else
    write (pipe_to_factory_fd[1], meta_theme_info->application_font, strlen (meta_theme_info->application_font) + 1);

  rowstride = gdk_pixbuf_get_rowstride (pixbuf);
  pixels = gdk_pixbuf_get_pixels (pixbuf);

  for (i = 0; i < ICON_SIZE_HEIGHT; i++)
    {
      gint j = 0;
      gint bytes_read;

      do
	{
	  bytes_read = read (pipe_from_factory_fd[0], pixels + (rowstride)*i + j, ICON_SIZE_WIDTH * gdk_pixbuf_get_n_channels (pixbuf) - j);
	  if (bytes_read > 0)
	    j += bytes_read;
	  else if (bytes_read == 0)
	    {
	      g_warning ("Received EOF while reading thumbnail for gtk: '%s', metacity '%s', icon: '%s', font: '%s'\n",
			 meta_theme_info->gtk_theme_name,
			 meta_theme_info->metacity_theme_name,
			 meta_theme_info->icon_theme_name,
			 meta_theme_info->application_font ? meta_theme_info->application_font : "Sans 10");
	      g_object_unref (pixbuf);
	      close (pipe_to_factory_fd[1]);
	      pipe_to_factory_fd[1] = 0;
	      close (pipe_from_factory_fd[0]);
	      pipe_from_factory_fd[0] = 0;
	      return NULL;
	    }
	}
      while (j < ICON_SIZE_WIDTH * gdk_pixbuf_get_n_channels (pixbuf));
    }

  retval = gdk_pixbuf_scale_simple (pixbuf, ICON_SIZE_WIDTH/2, ICON_SIZE_HEIGHT/2, GDK_INTERP_BILINEAR);
  g_object_unref (pixbuf);
  
  g_hash_table_insert (theme_hash, g_strdup (meta_theme_info->name), retval);
  return retval;
}

void
generate_theme_thumbnail_async (GnomeThemeMetaInfo *meta_theme_info,
				ThemeThumbnailFunc  func,
				gpointer            user_data,
				GDestroyNotify      destroy)
{
  GdkPixbuf *pixbuf;

  g_return_if_fail (async_data.set == FALSE);

  pixbuf = g_hash_table_lookup (theme_hash, meta_theme_info->name);
  if (pixbuf != NULL)
    {
      (* func) (pixbuf, user_data);
      if (destroy)
	(* destroy) (user_data);
      return;
    }

  if (!pipe_to_factory_fd[1] || !pipe_from_factory_fd[0])
    {
      (* func) (NULL, user_data);
      if (destroy)
	(* destroy) (user_data);
      return;
    }

  if (async_data.channel == NULL)
    {
      async_data.channel = g_io_channel_unix_new (pipe_from_factory_fd[0]);
      g_io_channel_set_flags (async_data.channel, g_io_channel_get_flags (async_data.channel) |
			      G_IO_FLAG_NONBLOCK, NULL);
      g_io_channel_set_encoding (async_data.channel, NULL, NULL);
      async_data.watch_id = g_io_add_watch (async_data.channel, G_IO_IN | G_IO_HUP, message_from_child, NULL);
    }


  async_data.set = TRUE;
  async_data.meta_theme_name = g_strdup (meta_theme_info->name);
  async_data.func = func;
  async_data.user_data = user_data;
  async_data.destroy = destroy;

  write (pipe_to_factory_fd[1], meta_theme_info->gtk_theme_name, strlen (meta_theme_info->gtk_theme_name) + 1);
  write (pipe_to_factory_fd[1], meta_theme_info->metacity_theme_name, strlen (meta_theme_info->metacity_theme_name) + 1);
  write (pipe_to_factory_fd[1], meta_theme_info->icon_theme_name, strlen (meta_theme_info->icon_theme_name) + 1);
  if (meta_theme_info->application_font == NULL)
    write (pipe_to_factory_fd[1], "Sans 10", strlen ("Sans 10") + 1);
  else
    write (pipe_to_factory_fd[1], meta_theme_info->application_font, strlen (meta_theme_info->application_font) + 1);
}

void
theme_thumbnail_factory_init (int argc, char *argv[])
{
  pipe (pipe_to_factory_fd);
  pipe (pipe_from_factory_fd);

  child_pid = fork ();
  if (child_pid == 0)
    {
      ThemeThumbnailData data;
      GIOChannel *channel;

      /* Child */
      gtk_init (&argc, &argv);

      close (pipe_to_factory_fd[1]);
      pipe_to_factory_fd[1] = 0;
      close (pipe_from_factory_fd[0]);
      pipe_from_factory_fd[0] = 0;

      data.status = READY_FOR_THEME;
      data.control_theme_name = g_byte_array_new ();
      data.wm_theme_name = g_byte_array_new ();
      data.icon_theme_name = g_byte_array_new ();
      data.application_font = g_byte_array_new ();

      channel = g_io_channel_unix_new (pipe_to_factory_fd[0]);
      g_io_channel_set_flags (channel, g_io_channel_get_flags (channel) |
			      G_IO_FLAG_NONBLOCK, NULL);
      g_io_channel_set_encoding (channel, NULL, NULL);
      g_io_add_watch (channel, G_IO_IN | G_IO_HUP, message_from_capplet, &data);
      g_io_channel_unref (channel);

      gtk_main ();
      _exit (0);
    }

  g_assert (child_pid > 0);

  /* Parent */
  close (pipe_to_factory_fd[0]);
  close (pipe_from_factory_fd[1]);
  async_data.set = FALSE;
  async_data.meta_theme_name = NULL;
  async_data.data = g_byte_array_new ();

  theme_hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
}
