/*
 * Copyright (c) 2004 Jean-Yves Lefort
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of Jean-Yves Lefort nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdarg.h>
#include <errno.h>
#include <glib.h>
#include <glib/gi18n.h>
#include "sg-util.h"
#include "sgtk-util.h"
#include "st-handler.h"
#include "st-handler-field.h"
#include "st-handlers.h"
#include "st-settings.h"
#include "st-cache.h"

/*** cpp *********************************************************************/

#define CATEGORIES_MAGIC		0x43435453	/* STCC (little endian integer) */
#define CATEGORIES_VERSION		0

#define STREAMS_MAGIC			0x43535453	/* STSC (little endian integer) */
#define STREAMS_VERSION			0

#define st_cache_read_int(channel, int_ptr, err) \
  st_cache_read_any((channel), (int_ptr), sizeof(int), (err))
#define st_cache_read_double(channel, double_ptr, err) \
  st_cache_read_any((channel), (double_ptr), sizeof(double), (err))

/*** type definitions ********************************************************/

typedef enum
{
  ATOM_CATEGORY_NAME,
  ATOM_CATEGORY_FLAGS,
  ATOM_CATEGORY_PARENT,
  ATOM_CATEGORY_LABEL,
  ATOM_CATEGORY_URL_POSTFIX
} CategoryAtom;

typedef enum
{
  ATOM_STREAM_NAME,
  ATOM_STREAM_FIELDS
} StreamAtom;

typedef enum
{
  VALUE_TYPE_BOOLEAN,
  VALUE_TYPE_INT,
  VALUE_TYPE_UINT,
  VALUE_TYPE_DOUBLE,
  VALUE_TYPE_STRING,
  VALUE_TYPE_VALUE_ARRAY,
  VALUE_TYPE_PIXBUF
} ValueType;

typedef struct
{
  GIOChannel	*channel;
  gboolean	status;
  GError	**err;
} SaveInfo;

typedef enum
{
  OPEN_MODE_READ,
  OPEN_MODE_WRITE
} OpenMode;

/*** function declarations ***************************************************/

static gboolean st_cache_save_handler (STHandler *handler, GError **err);

static gboolean st_cache_save_category_cb (STCategoryStore *store,
					   STCategoryBag *bag,
					   STCategoryBag *parent,
					   gpointer data);
static gboolean st_cache_save_category (GIOChannel *channel,
					STCategoryBag *category_bag,
					STCategoryBag *parent_bag,
					GError **err);

static gboolean st_cache_save_streams (STCategoryBag *category_bag,
				       STStreamStore *streams,
				       GError **err);

static gboolean st_cache_save_stream_cb (STStreamStore *store,
					 STStreamBag *bag,
					 gpointer data);
static gboolean st_cache_save_stream (GIOChannel *channel,
				      STStreamBag *stream_bag,
				      GError **err);
static gboolean st_cache_save_value (GIOChannel *channel,
				     const GValue *value,
				     GError **err);

static char *st_cache_get_filename (const char *str);
static char *st_cache_get_handler_directory (STHandler *handler);
static char *st_cache_get_categories_filename (STHandler *handler);
static char *st_cache_get_streams_filename (STHandler *handler,
					    const char *category_name);

static GIOChannel *st_cache_io_channel_new_file (const char *filename,
						 OpenMode mode,
						 int magic,
						 int version,
						 int subversion,
						 GError **err);
static GIOChannel *st_cache_io_channel_new_categories (const char *filename,
						       OpenMode mode,
						       GError **err);
static GIOChannel *st_cache_io_channel_new_streams (STHandler *handler,
						    const char *filename,
						    OpenMode mode,
						    GError **err);

static gboolean st_cache_write (GIOChannel *channel,
				const char *format,
				...);

static gboolean st_cache_write_int (GIOChannel *channel, int i, GError **err);
static gboolean st_cache_write_double (GIOChannel *channel, double d, GError **err);
static gboolean st_cache_write_buffer (GIOChannel *channel,
				       const char *buf,
				       int len,
				       GError **err);
static gboolean st_cache_write_string (GIOChannel *channel,
				       const char *str,
				       GError **err);

static void st_cache_append_category (STHandler *handler,
				      GHashTable *parents,
				      GNode *categories,
				      STCategoryBag *category_bag,
				      const char *parent_name);

static gboolean st_cache_load_value (GIOChannel *channel,
				     GValue *value,
				     GError **err);

static gboolean st_cache_read_any (GIOChannel *channel,
				   gpointer ptr,
				   int len,
				   GError **err);
static gboolean st_cache_read_buffer (GIOChannel *channel,
				      char **buf,
				      int *len,
				      GError **err);
static gboolean st_cache_read_string (GIOChannel *channel,
				      char **str,
				      GError **err);

/*** implementation **********************************************************/

gboolean
st_cache_save (GError **err)
{
  GSList *l;

  SG_LIST_FOREACH(l, st_handlers_list)
    if (! st_cache_save_handler(l->data, err))
      return FALSE;

  return TRUE;
}

static gboolean
st_cache_save_handler (STHandler *handler, GError **err)
{
  char *dirname;
  SaveInfo info;
  STCategoryStore *categories;

  g_return_val_if_fail(ST_IS_HANDLER(handler), FALSE);

  dirname = st_cache_get_handler_directory(handler);
  if (! g_file_test(dirname, G_FILE_TEST_IS_DIR) && (mkdir(dirname, 0755) < 0))
    {
      g_set_error(err, 0, 0, _("unable to create directory %s: %s"), dirname, g_strerror(errno));
      g_free(dirname);
      return FALSE;
    }
  g_free(dirname);
  
  if (ST_HANDLER_HAS_CATEGORIES(handler))
    {
      char *filename;
      
      filename = st_cache_get_categories_filename(handler);
      info.channel = st_cache_io_channel_new_categories(filename, OPEN_MODE_WRITE, err);
      g_free(filename);

      if (! info.channel)
	return FALSE;
    }
  else
    info.channel = NULL;

  info.status = TRUE;
  info.err = err;
  
  categories = st_handler_get_categories(handler);
  st_category_store_foreach(categories, st_cache_save_category_cb, &info);
  g_object_unref(categories);

  if (info.channel)
    {
      if (info.status)
	{
	  if (g_io_channel_shutdown(info.channel, TRUE, err) != G_IO_STATUS_NORMAL)
	    info.status = FALSE;
	}
      else
	g_io_channel_shutdown(info.channel, FALSE, NULL);
  
      g_io_channel_unref(info.channel);
    }

  return info.status;
}

static gboolean
st_cache_save_category_cb (STCategoryStore *store,
			   STCategoryBag *bag,
			   STCategoryBag *parent,
			   gpointer data)
{
  SaveInfo *info = data;

  info->status = st_cache_save_category(info->channel, bag, parent, info->err);

  return ! info->status;
}

static gboolean
st_cache_save_category (GIOChannel *channel,
			STCategoryBag *category_bag,
			STCategoryBag *parent_bag,
			GError **err)
{
  STStreamStore *streams;

  g_return_val_if_fail(ST_IS_CATEGORY_BAG(category_bag), FALSE);

  if (channel)
    {
      if (! st_cache_write(channel, "isii",
			   ATOM_CATEGORY_NAME, ST_CATEGORY(category_bag)->name,
			   ATOM_CATEGORY_FLAGS, category_bag->flags,
			   err))
	return FALSE;
      
      if (parent_bag)
	{
	  if (! st_cache_write(channel, "is",
			       ATOM_CATEGORY_PARENT, ST_CATEGORY(parent_bag)->name,
			       err))
	    return FALSE;
	}

      if (! ST_CATEGORY_BAG_IS_STOCK(category_bag))
	{
	  if (! st_cache_write(channel, "isis",
			       ATOM_CATEGORY_LABEL, ST_CATEGORY(category_bag)->label,
			       ATOM_CATEGORY_URL_POSTFIX, ST_CATEGORY(category_bag)->url_postfix,
			       err))
	    return FALSE;
	}
    }
      
  streams = st_handler_get_streams(category_bag->handler, ST_CATEGORY(category_bag)->name);
  if (streams)
    {
      gboolean status;

      status = st_stream_store_is_touched(streams)
	? st_cache_save_streams(category_bag, streams, err)
	: TRUE;
      g_object_unref(streams);
	  
      if (! status)
	return FALSE;
    }

  return TRUE;
}

static gboolean
st_cache_save_streams (STCategoryBag *category_bag,
		       STStreamStore *streams,
		       GError **err)
{
  char *filename;
  SaveInfo info;

  g_return_val_if_fail(ST_IS_CATEGORY_BAG(category_bag), FALSE);
  g_return_val_if_fail(ST_IS_STREAM_STORE(streams), FALSE);
  
  filename = st_cache_get_streams_filename(category_bag->handler, ST_CATEGORY(category_bag)->name);
  info.channel = st_cache_io_channel_new_streams(category_bag->handler, filename, OPEN_MODE_WRITE, err);
  g_free(filename);

  if (! info.channel)
    return FALSE;

  info.status = st_cache_write_int(info.channel, sgtk_tree_model_get_count(GTK_TREE_MODEL(streams)), err);
  if (info.status)
    {
      info.err = err;
      st_stream_store_foreach(streams, st_cache_save_stream_cb, &info);
    }
  
  if (info.status)
    {
      if (g_io_channel_shutdown(info.channel, TRUE, err) != G_IO_STATUS_NORMAL)
	info.status = FALSE;
    }
  else
    g_io_channel_shutdown(info.channel, FALSE, NULL);
  
  g_io_channel_unref(info.channel);

  return info.status;
}

static gboolean
st_cache_save_stream_cb (STStreamStore *store,
			 STStreamBag *bag,
			 gpointer data)
{
  SaveInfo *info = data;

  info->status = st_cache_save_stream(info->channel, bag, info->err);

  return ! info->status;
}

static gboolean
st_cache_save_stream (GIOChannel *channel,
		      STStreamBag *stream_bag,
		      GError **err)
{
  GSList *fields;
  GSList *l;
  
  g_return_val_if_fail(channel != NULL, FALSE);
  g_return_val_if_fail(ST_IS_STREAM_BAG(stream_bag), FALSE);

  fields = st_handler_get_fields(stream_bag->handler);

  if (! st_cache_write(channel, "isii",
		       ATOM_STREAM_NAME, ST_STREAM(stream_bag)->name,
		       ATOM_STREAM_FIELDS, g_slist_length(fields) - st_handler_count_fields(stream_bag->handler, ST_HANDLER_FIELD_VOLATILE),
		       err))
    return FALSE;
  
  SG_LIST_FOREACH(l, fields)
    {
      STHandlerField *field = l->data;

      if (! ST_HANDLER_FIELD_IS_VOLATILE(field))
	{
	  GValue value = { 0, };
	  gboolean status;

	  st_stream_bag_get_field(stream_bag, field, &value);
	  status = st_cache_save_value(channel, &value, err);
	  g_value_unset(&value);

	  if (! status)
	    return FALSE;
	}
    }

  return TRUE;
}

static gboolean
st_cache_save_value (GIOChannel *channel, const GValue *value, GError **err)
{
  ValueType type;

  g_return_val_if_fail(channel != NULL, FALSE);
  g_return_val_if_fail(G_IS_VALUE(value), FALSE);

  if (G_VALUE_HOLDS_BOOLEAN(value))
    type = VALUE_TYPE_BOOLEAN;
  else if (G_VALUE_HOLDS_INT(value))
    type = VALUE_TYPE_INT;
  else if (G_VALUE_HOLDS_UINT(value))
    type = VALUE_TYPE_UINT;
  else if (G_VALUE_HOLDS_DOUBLE(value))
    type = VALUE_TYPE_DOUBLE;
  else if (G_VALUE_HOLDS_STRING(value))
    type = VALUE_TYPE_STRING;
  else if (G_VALUE_HOLDS(value, G_TYPE_VALUE_ARRAY))
    type = VALUE_TYPE_VALUE_ARRAY;
  else if (G_VALUE_HOLDS(value, GDK_TYPE_PIXBUF))
    type = VALUE_TYPE_PIXBUF;
  else
    g_return_val_if_reached(FALSE);
  
  if (! st_cache_write_int(channel, type, err))
    return FALSE;

  switch (type)
    {
    case VALUE_TYPE_BOOLEAN:
      if (! st_cache_write_int(channel, g_value_get_boolean(value), err))
	return FALSE;
      break;

    case VALUE_TYPE_INT:
      if (! st_cache_write_int(channel, g_value_get_int(value), err))
	return FALSE;
      break;

    case VALUE_TYPE_UINT:
      if (! st_cache_write_int(channel, g_value_get_uint(value), err))
	return FALSE;
      break;

    case VALUE_TYPE_DOUBLE:
      if (! st_cache_write_double(channel, g_value_get_double(value), err))
	return FALSE;
      break;

    case VALUE_TYPE_STRING:
      if (! st_cache_write_string(channel, g_value_get_string(value), err))
	return FALSE;
      break;

    case VALUE_TYPE_VALUE_ARRAY:
      {
	GValueArray *value_array = g_value_get_boxed(value);
	int i;

	if (! st_cache_write_int(channel, value_array->n_values, err))
	  return FALSE;

	for (i = 0; i < value_array->n_values; i++)
	  if (! st_cache_save_value(channel, g_value_array_get_nth(value_array, i), err))
	    return FALSE;
	
	break;
      }

    case VALUE_TYPE_PIXBUF:
      {
	GdkPixbuf *pixbuf;
	char *buf;
	gsize size;
	gboolean status;
	
	pixbuf = g_value_get_object(value);
	if (pixbuf)
	  {
	    if (! gdk_pixbuf_save_to_buffer(pixbuf, &buf, &size, "png", err, NULL))
	      return FALSE;
	  }
	else
	  {
	    buf = NULL;
	    size = 0;
	  }
	
	status = st_cache_write_buffer(channel, buf, size, err);
	g_free(buf);

	if (! status)
	  return FALSE;

	break;
      }

    default:
      g_return_val_if_reached(FALSE);
    }

  return TRUE;
}

static char *
st_cache_get_filename (const char *str)
{
  int i;
  GString *filename;

  g_return_val_if_fail(str != NULL, NULL);

  filename = g_string_new(NULL);

  for (i = 0; str[i]; i++)
    if (g_ascii_isalnum(str[i]))
      g_string_append_c(filename, str[i]);
    else
      g_string_append_printf(filename, "%%%.2X", str[i]);

  return g_string_free(filename, FALSE);
}

static char *
st_cache_get_handler_directory (STHandler *handler)
{
  char *handler_filename;
  char *dir;

  g_return_val_if_fail(ST_IS_HANDLER(handler), NULL);

  handler_filename = st_cache_get_filename(st_handler_get_name(handler));
  dir = g_build_filename(st_settings.cache_dir, handler_filename, NULL);
  g_free(handler_filename);

  return dir;
}

static char *
st_cache_get_categories_filename (STHandler *handler)
{
  char *dir;
  char *filename;

  g_return_val_if_fail(ST_IS_HANDLER(handler), NULL);

  dir = st_cache_get_handler_directory(handler);
  filename = g_build_filename(dir, "_categories", NULL);
  g_free(dir);

  return filename;
}

static char *
st_cache_get_streams_filename (STHandler *handler, const char *category_name)
{
  char *dir;
  char *category_filename;
  char *filename;

  g_return_val_if_fail(ST_IS_HANDLER(handler), NULL);

  dir = st_cache_get_handler_directory(handler);
  category_filename = st_cache_get_filename(category_name);
  filename = g_build_filename(dir, category_filename, NULL);
  g_free(dir);
  g_free(category_filename);

  return filename;
}

static GIOChannel *
st_cache_io_channel_new_file (const char *filename,
			      OpenMode mode,
			      int magic,
			      int version,
			      int subversion,
			      GError **err)
{
  GIOChannel *channel;

  g_return_val_if_fail(filename != NULL, NULL);

  channel = g_io_channel_new_file(filename, mode == OPEN_MODE_READ ? "r" : "w", err);
  if (! channel)
    return NULL;

  if (g_io_channel_set_encoding(channel, NULL, err) != G_IO_STATUS_NORMAL)
    goto error;

  switch (mode)
    {
    case OPEN_MODE_READ:
      {
	int file_magic;
	int file_version;

	if (! st_cache_read_int(channel, &file_magic, err))
	  goto error;
	if (file_magic != magic)
	  {
	    g_set_error(err, 0, 0, _("invalid magic number"));
	    goto error;
	  }

	if (! st_cache_read_int(channel, &file_version, err))
	  goto error;
	if (file_version != version)
	  {
	    g_set_error(err, 0, 0, _("invalid version number"));
	    goto error;
	  }

	if (subversion != -1)
	  {
	    int file_subversion;

	    if (! st_cache_read_int(channel, &file_subversion, err))
	      goto error;
	    if (file_subversion != subversion)
	      {
		g_set_error(err, 0, 0, _("invalid subversion number"));
		goto error;
	      }
	  }
	
	break;
      }

    case OPEN_MODE_WRITE:
      if (! st_cache_write(channel, "ii", magic, version, err))
	goto error;
      if (subversion != -1 && ! st_cache_write(channel, "i", subversion, err))
	goto error;
      break;

    default:
      g_return_val_if_reached(NULL);
    }

  return channel;

 error:
  g_io_channel_shutdown(channel, FALSE, NULL);
  g_io_channel_unref(channel);
  return NULL;
}

static GIOChannel *
st_cache_io_channel_new_categories (const char *filename,
				    OpenMode mode,
				    GError **err)
{
  g_return_val_if_fail(filename != NULL, NULL);

  return st_cache_io_channel_new_file(filename,
				      mode,
				      CATEGORIES_MAGIC,
				      CATEGORIES_VERSION,
				      -1,
				      err);
}

static GIOChannel *
st_cache_io_channel_new_streams (STHandler *handler,
				 const char *filename,
				 OpenMode mode,
				 GError **err)
{
  g_return_val_if_fail(ST_IS_HANDLER(handler), NULL);
  g_return_val_if_fail(filename != NULL, NULL);

  return st_cache_io_channel_new_file(filename,
				      mode,
				      STREAMS_MAGIC,
				      STREAMS_VERSION,
				      st_handler_get_stream_version(handler),
				      err);
}

static gboolean
st_cache_write (GIOChannel *channel,
		const char *format,
		...)
{
  va_list args;
  int i;
  GError *err = NULL;
  GError **real_err;

  g_return_val_if_fail(channel != NULL, FALSE);
  g_return_val_if_fail(format != NULL, FALSE);

  va_start(args, format);

  for (i = 0; format[i]; i++)
    switch (format[i])
      {
      case 'i':
	{
	  int val = va_arg(args, int);

	  if (! st_cache_write_int(channel, val, &err))
	    goto error;

	  break;
	}

      case 's':
	{
	  const char *val = va_arg(args, const char *);

	  if (! st_cache_write_string(channel, val, &err))
	    goto error;

	  break;
	}

      case 'b':
	{
	  int len = va_arg(args, int);
	  const char *val = va_arg(args, const char *);

	  if (! st_cache_write_buffer(channel, val, len, &err))
	    goto error;
	  
	  break;
	}

      default:
	g_return_val_if_reached(FALSE);
      }

  va_end(args);
  return TRUE;

 error:
  real_err = va_arg(args, GError **);
  g_propagate_error(real_err, err);

  va_end(args);
  return FALSE;
}

static gboolean
st_cache_write_int (GIOChannel *channel, int i, GError **err)
{
  g_return_val_if_fail(channel != NULL, FALSE);

  return g_io_channel_write_chars(channel, (const char *) &i, sizeof(i), NULL, err) == G_IO_STATUS_NORMAL;
}

static gboolean
st_cache_write_double (GIOChannel *channel, double d, GError **err)
{
  g_return_val_if_fail(channel != NULL, FALSE);

  return g_io_channel_write_chars(channel, (const char *) &d, sizeof(d), NULL, err) == G_IO_STATUS_NORMAL;
}

static gboolean
st_cache_write_buffer (GIOChannel *channel,
		       const char *buf,
		       int len,
		       GError **err)
{
  g_return_val_if_fail(channel != NULL, FALSE);

  if (! st_cache_write_int(channel, len, err))
    return FALSE;

  return g_io_channel_write_chars(channel, buf, len, NULL, err) == G_IO_STATUS_NORMAL;
}

static gboolean
st_cache_write_string (GIOChannel *channel, const char *str, GError **err)
{
  g_return_val_if_fail(channel != NULL, FALSE);

  return st_cache_write_buffer(channel, str, str ? strlen(str) : 0, err);
}

gboolean
st_cache_has_categories (STHandler *handler)
{
  char *filename;
  GIOChannel *channel;

  g_return_val_if_fail(ST_IS_HANDLER(handler), FALSE);

  filename = st_cache_get_categories_filename(handler);
  channel = st_cache_io_channel_new_categories(filename, OPEN_MODE_READ, NULL);
  g_free(filename);

  if (channel)
    {
      g_io_channel_shutdown(channel, FALSE, NULL);
      g_io_channel_unref(channel);
      return TRUE;
    }
  else
    return FALSE;
}

GNode *
st_cache_load_categories (STHandler *handler, GError **err)
{
  char *filename;
  GIOChannel *channel;
  GHashTable *parents;
  GNode *categories;
  STCategoryBag *category_bag = NULL;
  char *parent_name = NULL;
  CategoryAtom atom;
  GIOStatus io_status;
  
  g_return_val_if_fail(ST_IS_HANDLER(handler), NULL);

  filename = st_cache_get_categories_filename(handler);
  channel = st_cache_io_channel_new_categories(filename, OPEN_MODE_READ, err);
  g_free(filename);

  if (! channel)
    return FALSE;

  parents = g_hash_table_new(g_str_hash, g_str_equal);
  categories = g_node_new(NULL);

  while ((io_status = g_io_channel_read_chars(channel, (char *) &atom, sizeof(atom), NULL, err)) == G_IO_STATUS_NORMAL)
    switch (atom)
      {
      case ATOM_CATEGORY_NAME:
	{
	  char *name;

	  if (category_bag)
	    {
	      st_cache_append_category(handler,
				       parents,
				       categories,
				       category_bag,
				       parent_name);
	      g_object_unref(category_bag);
	      category_bag = NULL;
	      g_free(parent_name);
	      parent_name = NULL;
	    }

	  if (! st_cache_read_string(channel, &name, err))
	    goto error;

	  if (ST_CATEGORY_BAG_NAME_IS_STOCK(name))
	    {
	      category_bag = st_handler_get_stock_category(handler, name);
	      if (category_bag)
		g_object_ref(category_bag);
	      else
		st_handler_notice(handler, _("categories cache: no such stock category \"%s\""), name);
	      g_free(name);
	    }
	  else
	    {
	      category_bag = st_category_bag_new(handler);
	      ST_CATEGORY(category_bag)->name = name;
	    }

	  break;
	}

      case ATOM_CATEGORY_FLAGS:
	{
	  unsigned int flags;
	  
	  if (! st_cache_read_int(channel, &flags, err))
	    goto error;

	  if (category_bag)
	    category_bag->flags = flags;

	  break;
	}

      case ATOM_CATEGORY_PARENT:
	if (parent_name)
	  {
	    g_set_error(err, 0, 0, _("parent name (\"%s\") already specified"), parent_name);
	    goto error;
	  }
	
	if (! st_cache_read_string(channel, &parent_name, err))
	  goto error;

	break;

      case ATOM_CATEGORY_LABEL:
	{
	  char *label;

	  if (! st_cache_read_string(channel, &label, err))
	    goto error;

	  if (category_bag)
	    {
	      if (ST_CATEGORY(category_bag)->label)
		{
		  g_free(label);
		  g_set_error(err, 0, 0, _("label (\"%s\") already specified"), ST_CATEGORY(category_bag)->label);
		  goto error;
		}
	    
	      ST_CATEGORY(category_bag)->label = label;
	    }
	  else
	    g_free(label);

	  break;
	}

      case ATOM_CATEGORY_URL_POSTFIX:
	{
	  char *url_postfix;

	  if (! st_cache_read_string(channel, &url_postfix, err))
	    goto error;

	  if (category_bag)
	    {
	      if (ST_CATEGORY(category_bag)->url_postfix)
		{
		  g_free(url_postfix);
		  g_set_error(err, 0, 0, _("URL postfix (\"%s\") already specified"), ST_CATEGORY(category_bag)->url_postfix);
		  goto error;
		}

	      ST_CATEGORY(category_bag)->url_postfix = url_postfix;
	    }
	  else
	    g_free(url_postfix);

	  break;
	}

      default:
	g_set_error(err, 0, 0, _("unknown atom %i"), atom);
	goto error;
      }

  if (io_status == G_IO_STATUS_EOF)
    {
      if (category_bag)
	st_cache_append_category(handler,
				 parents,
				 categories,
				 category_bag,
				 parent_name);
    }
  else
    goto error;

 end:
  g_hash_table_destroy(parents);

  if (category_bag)
    g_object_unref(category_bag);
  g_free(parent_name);

  if (categories)
    {
      if (g_io_channel_shutdown(channel, TRUE, err) != G_IO_STATUS_NORMAL)
	{
	  sg_objects_free_node(categories);
	  categories = NULL;
	}
    }
  else
    g_io_channel_shutdown(channel, FALSE, NULL);

  g_io_channel_unref(channel);

  return categories;

 error:
  sg_objects_free_node(categories);
  categories = NULL;
  goto end;
}

static void
st_cache_append_category (STHandler *handler,
			  GHashTable *parents,
			  GNode *categories,
			  STCategoryBag *category_bag,
			  const char *parent_name)
{
  g_return_if_fail(ST_IS_HANDLER(handler));
  g_return_if_fail(parents != NULL);
  g_return_if_fail(categories != NULL);
  g_return_if_fail(ST_IS_CATEGORY_BAG(category_bag));

  if (! ST_CATEGORY_BAG_IS_STOCK(category_bag))
    {
      GNode *parent = NULL;
      GNode *node = NULL;

      if (parent_name)
	{
	  parent = g_hash_table_lookup(parents, parent_name);
	  if (! parent)
	    {
	      st_handler_notice(handler, _("categories cache: parent category \"%s\" not found"), parent_name);
	      return;
	    }
	}

      if (! parent)
	parent = categories;

      node = g_node_append_data(parent, g_object_ref(category_bag));
      g_hash_table_insert(parents, ST_CATEGORY(category_bag)->name, node);
    }
}

gboolean
st_cache_has_streams (STHandler *handler, const char *category_name)
{
  char *filename;
  GIOChannel *channel;

  g_return_val_if_fail(ST_IS_HANDLER(handler), FALSE);
  g_return_val_if_fail(category_name != NULL, FALSE);

  filename = st_cache_get_streams_filename(handler, category_name);
  channel = st_cache_io_channel_new_streams(handler, filename, OPEN_MODE_READ, NULL);
  g_free(filename);

  if (channel)
    {
      g_io_channel_shutdown(channel, FALSE, NULL);
      g_io_channel_unref(channel);
      return TRUE;
    }
  else
    return FALSE;
}

STStreamStore *
st_cache_load_streams (STHandler *handler,
		       const char *category_name,
		       STCacheLoadStreamsProgressCallback progress_cb,
		       gpointer progress_data,
		       GError **err)
{
  char *filename;
  GIOChannel *channel;
  STStreamStore *streams;
  STStreamBag *stream_bag = NULL;
  StreamAtom atom;
  int n_streams;
  int n = 0;
  GIOStatus io_status;

  g_return_val_if_fail(ST_IS_HANDLER(handler), NULL);
  g_return_val_if_fail(category_name != NULL, NULL);

  filename = st_cache_get_streams_filename(handler, category_name);
  channel = st_cache_io_channel_new_streams(handler, filename, OPEN_MODE_READ, err);
  g_free(filename);

  if (! channel)
    return NULL;

  streams = st_stream_store_new(handler);

  if (! st_cache_read_int(channel, &n_streams, err))
    goto error;

  while ((io_status = g_io_channel_read_chars(channel, (char *) &atom, sizeof(atom), NULL, err)) == G_IO_STATUS_NORMAL)
    {
      switch (atom)
	{
	case ATOM_STREAM_NAME:
	  {
	    char *name;

	    if (progress_cb)
	      {
		if (progress_cb(++n, n_streams, progress_data))
		  goto error; /* aborted by callback */
	      }
	    
	    if (stream_bag)
	      {
		st_stream_store_append(streams, stream_bag);
		g_object_unref(stream_bag);
		stream_bag = NULL;
	      }

	    if (! st_cache_read_string(channel, &name, err))
	      goto error;

	    stream_bag = st_stream_bag_new(handler);
	    ST_STREAM(stream_bag)->name = name;

	    break;
	  }

	case ATOM_STREAM_FIELDS:
	  if (stream_bag)
	    {
	      int n_fields;
	      GSList *iter;
	      int i;

	      if (! st_cache_read_int(channel, &n_fields, err))
		goto error;

	      iter = st_handler_get_fields(stream_bag->handler);

	      for (i = 0; i < n_fields; i++)
		{
		  STHandlerField *field = NULL;

		  while (! field && iter)
		    {
		      if (! ST_HANDLER_FIELD_IS_VOLATILE(iter->data))
			field = iter->data;

		      iter = iter->next;
		    }

		  if (field)
		    {
		      GValue value = { 0, };
		      
		      if (! st_cache_load_value(channel, &value, err))
			goto error;

		      if (G_IS_VALUE(&value))
			{
			  if (G_VALUE_HOLDS(&value, st_handler_field_get_type(field)))
			    st_stream_bag_set_field(stream_bag, field, &value);
			  else
			    st_handler_notice(handler, _("streams cache of category \"%s\": invalid value type"), category_name);
			  
			  g_value_unset(&value);
			}
		    }
		  else
		    st_handler_notice(handler, _("streams cache of category \"%s\": too many stream fields"), category_name);
		}
	    }
	  else
	    {
	      g_set_error(err, 0, 0, _("orphan stream fields"));
	      goto error;
	    }
	  break;
	  
	default:
	  g_set_error(err, 0, 0, _("unknown atom %i"), atom);
	  goto error;
	}
    }

  if (io_status == G_IO_STATUS_EOF)
    {
      if (stream_bag)
	st_stream_store_append(streams, stream_bag);
    }
  else
    goto error;

 end:
  if (stream_bag)
    g_object_unref(stream_bag);

  if (streams)
    {
      if (g_io_channel_shutdown(channel, TRUE, err) != G_IO_STATUS_NORMAL)
	{
	  g_object_unref(streams);
	  streams = NULL;
	}
    }
  else
    g_io_channel_shutdown(channel, FALSE, NULL);

  g_io_channel_unref(channel);

  return streams;

 error:
  g_object_unref(streams);
  streams = NULL;
  goto end;
}

static gboolean
st_cache_load_value (GIOChannel *channel, GValue *value, GError **err)
{
  int type;

  g_return_val_if_fail(channel != NULL, FALSE);
  g_return_val_if_fail(value != NULL, FALSE);

  if (! st_cache_read_int(channel, &type, err))
    return FALSE;

  switch (type)
    {
    case VALUE_TYPE_BOOLEAN:
      {
	gboolean val;

	if (! st_cache_read_int(channel, &val, err))
	  return FALSE;

	g_value_init(value, G_TYPE_BOOLEAN);
	g_value_set_boolean(value, val);

	return TRUE;
      }

    case VALUE_TYPE_INT:
      {
	int val;

	if (! st_cache_read_int(channel, &val, err))
	  return FALSE;

	g_value_init(value, G_TYPE_INT);
	g_value_set_int(value, val);

	return TRUE;
      }

    case VALUE_TYPE_UINT:
      {
	int val;

	if (! st_cache_read_int(channel, &val, err))
	  return FALSE;

	g_value_init(value, G_TYPE_UINT);
	g_value_set_uint(value, val);

	return TRUE;
      }

    case VALUE_TYPE_DOUBLE:
      {
	double val;

	if (! st_cache_read_double(channel, &val, err))
	  return FALSE;

	g_value_init(value, G_TYPE_DOUBLE);
	g_value_set_double(value, val);

	return TRUE;
      }

    case VALUE_TYPE_STRING:
      {
	char *val;

	if (! st_cache_read_string(channel, &val, err))
	  return FALSE;

	g_value_init(value, G_TYPE_STRING);
	g_value_take_string(value, val);

	return TRUE;
      }

    case VALUE_TYPE_VALUE_ARRAY:
      {
	int n_values;
	GValueArray *value_array;
	int i;

	if (! st_cache_read_int(channel, &n_values, err))
	  return FALSE;

	value_array = g_value_array_new(n_values);

	for (i = 0; i < n_values; i++)
	  {
	    GValue this_value = { 0, };

	    if (! st_cache_load_value(channel, &this_value, err))
	      {
		g_value_array_free(value_array);
		return FALSE;
	      }
	    
	    g_value_array_append(value_array, &this_value);
	    g_value_unset(&this_value);
	  }

	g_value_init(value, G_TYPE_VALUE_ARRAY);
	g_value_take_boxed(value, value_array);
	
	return TRUE;
      }

    case VALUE_TYPE_PIXBUF:
      {
	char *buf;
	int len;
	gboolean status = TRUE;

	if (! st_cache_read_buffer(channel, &buf, &len, err))
	  return FALSE;

	if (len > 0)
	  {
	    GdkPixbufLoader *loader;
	    
	    loader = gdk_pixbuf_loader_new();

	    status = gdk_pixbuf_loader_write(loader, buf, len, err);
	    if (status)
	      {
		GdkPixbuf *pixbuf;

		pixbuf = gdk_pixbuf_loader_get_pixbuf(loader);
		if (pixbuf)
		  {
		    g_value_init(value, GDK_TYPE_PIXBUF);
		    g_value_set_object(value, pixbuf);
		  }
	      }
		
	    gdk_pixbuf_loader_close(loader, NULL);
	  }
	else
	  g_value_init(value, GDK_TYPE_PIXBUF);
	g_free(buf);

	return status;
      }

    default:
      g_set_error(err, 0, 0, _("unknown value type %i"), type);
      return FALSE;
    }
}

static gboolean
st_cache_read_any (GIOChannel *channel,
		   gpointer ptr,
		   int len,
		   GError **err)
{
  char buf[len];
  GIOStatus status;
  gsize bytes_read;

  g_return_val_if_fail(channel != NULL, FALSE);
  g_return_val_if_fail(ptr != NULL, FALSE);

  status = g_io_channel_read_chars(channel, buf, len, &bytes_read, err);
  if (status == G_IO_STATUS_NORMAL || status == G_IO_STATUS_EOF)
    {
      if (bytes_read == len)
	{
	  memcpy(ptr, buf, len);
	  return TRUE;
	}
      else
	g_set_error(err, 0, 0, _("unexpected end of file"));
    }
      
  return FALSE;
}

static gboolean
st_cache_read_buffer (GIOChannel *channel,
		      char **buf,
		      int *len,
		      GError **err)
{
  GIOStatus status;
  gsize bytes_read;
  int _len;
  char *_buf;

  g_return_val_if_fail(channel != NULL, FALSE);
  g_return_val_if_fail(buf != NULL, FALSE);

  if (! st_cache_read_int(channel, &_len, err))
    return FALSE;

  _buf = g_new(char, _len + 1);

  status = g_io_channel_read_chars(channel, _buf, _len, &bytes_read, err);
  if (status == G_IO_STATUS_NORMAL || status == G_IO_STATUS_EOF)
    {
      if (bytes_read == _len)
	{
	  *buf = _buf;
	  if (len)
	    *len = _len;
	  
	  _buf[_len] = 0;
	  
	  return TRUE;
	}
      else
	g_set_error(err, 0, 0, _("unexpected end of file"));
    }

  g_free(_buf);
  return FALSE;
}

static gboolean
st_cache_read_string (GIOChannel *channel,
		      char **str,
		      GError **err)
{
  char *buf;

  g_return_val_if_fail(channel != NULL, FALSE);
  g_return_val_if_fail(str != NULL, FALSE);

  if (! st_cache_read_buffer(channel, &buf, NULL, err))
    return FALSE;

  *str = buf;
  return TRUE;
}
