/*
 * Copyright (c) 2002, 2003, 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 <stdarg.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <glib-object.h>
#include <glib/gi18n.h>
#include "sg-dumper.h"

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

struct _SGDumper
{
  char		*filename;	/* the file to write */
  char		*tmpfile;	/* the temporary file we write into */

  GIOChannel	*io_channel;
  
  int		depth;		/* the current tab depth */
  gboolean	ok;		/* is everything ok so far? */
};

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

static gboolean sg_dumper_write_indent	(SGDumper	*dumper,
					 GError		**err);
static gboolean sg_dumper_printf	(SGDumper	*dumper,
					 GError		**err,
					 const char	*format,
					 ...) G_GNUC_PRINTF(3, 4);
static gboolean sg_dumper_write_str	(SGDumper	*dumper,
					 const char	*str,
					 GError		**err);
static gboolean sg_dumper_write_value	(SGDumper	*dumper,
					 const GValue	*value,
					 GError		**err);

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

SGDumper *
sg_dumper_new (const char *filename, GError **err)
{
  SGDumper *dumper;
  int fd;
  
  g_return_val_if_fail(filename != NULL, NULL);

  dumper = g_new0(SGDumper, 1);

  dumper->filename = g_strdup(filename);
  dumper->tmpfile = g_strconcat(dumper->filename, ".XXXXXX", NULL);

  fd = g_mkstemp(dumper->tmpfile);
  if (fd < 0)
    {
      g_set_error(err, 0, 0, _("unable to create a temporary file: %s"), g_strerror(errno));
      sg_dumper_free(dumper, NULL); /* ignore further errors */
      return NULL;
    }
  
  /* fine */

  dumper->io_channel = g_io_channel_unix_new(fd);
  dumper->ok = TRUE;

  return dumper;
}

gboolean
sg_dumper_free (SGDumper *dumper, GError **err)
{
  gboolean status = TRUE;

  g_return_val_if_fail(dumper != NULL, FALSE);

  if (dumper->io_channel)
    {
      GError *tmp_err = NULL;
      
      status = g_io_channel_shutdown(dumper->io_channel, TRUE, &tmp_err) == G_IO_STATUS_NORMAL;
      g_io_channel_unref(dumper->io_channel);
      
      if (! status)
	{
	  g_set_error(err, 0, 0, _("unable to close %s: %s"),
		      dumper->tmpfile, tmp_err->message);
	  g_error_free(tmp_err);
	  
	  goto end;
	}
    }
  
  if (dumper->ok)
    {
      if (g_file_test(dumper->filename, G_FILE_TEST_EXISTS))
	{
	  status = unlink(dumper->filename) >= 0;
	  if (! status)
	    {
	      g_set_error(err, 0, 0, _("unable to unlink %s: %s"),
			  dumper->filename, g_strerror(errno));
	      goto end;
	    }
	}

      status = rename(dumper->tmpfile, dumper->filename) >= 0;
      if (! status)
	{
	  g_set_error(err, 0, 0, _("unable to rename %s to %s: %s"),
		      dumper->tmpfile, dumper->filename, g_strerror(errno));
	  goto end;
	}
    }
  
 end:
  g_free(dumper->filename);
  g_free(dumper->tmpfile);
  g_free(dumper);

  return status;
}

gboolean
sg_dumper_write (SGDumper *dumper, GError **err, ...)
{
  va_list args;
  char *format;

  g_return_val_if_fail(dumper != NULL, FALSE);

  va_start(args, err);

  while ((format = va_arg(args, char *)))
    {
      int i;
      const char *terminator = ";";
      
      if (! format[0])
	terminator = NULL;
      else if (format[0] == '}')
	{
	  dumper->depth--;
	  terminator = "}";
	}
      
      if (! sg_dumper_write_indent(dumper, err))
	return FALSE;

      for (i = 0; format[i]; i++)
	{
	  switch (format[i])
	    {
	    case 'k':
	      {
		const char *key;

		key = va_arg(args, const char *);
		g_return_val_if_fail(key != NULL, FALSE);

		if (! sg_dumper_write_str(dumper, key, err))
		  return FALSE;

		break;
	      }

#define CASE_VALUE(letter, ctype, gtype, setter)			\
	      case letter:						\
		{							\
		  GValue _value =	{ 0, };				\
		  gboolean _status;					\
									\
		  g_value_init(&_value, gtype);				\
		  setter(&_value, va_arg(args, ctype));			\
		  _status = sg_dumper_write_value(dumper, &_value, err); \
		  g_value_unset(&_value);				\
		  							\
		  if (! _status)					\
		    return FALSE;					\
		  							\
		  break;						\
		}

	      CASE_VALUE('b', gboolean, G_TYPE_BOOLEAN, g_value_set_boolean);
	      CASE_VALUE('i', int, G_TYPE_INT, g_value_set_int);
	      CASE_VALUE('u', unsigned int, G_TYPE_UINT, g_value_set_uint);
	      CASE_VALUE('d', double, G_TYPE_DOUBLE, g_value_set_double);
	      CASE_VALUE('s', const char *, G_TYPE_STRING, g_value_set_string);
#undef CASE_VALUE

	    case 'v':
	      {
		const GValue *value;

		value = va_arg(args, const GValue *);
		g_return_val_if_fail(value != NULL, FALSE);

		if (! sg_dumper_write_value(dumper, value, err))
		  return FALSE;

		break;
	      }
	      
	    case '{':
	      dumper->depth++;
	      terminator = "{";
	      break;

	    case '#':
	      {
		const char *comment;
		
		comment = va_arg(args, const char *);
		g_return_val_if_fail(comment != NULL, FALSE);
		
		terminator = NULL;
		if (! sg_dumper_printf(dumper, err, "# %s", comment))
		  return FALSE;

		break;
	      }

	    case '}':
	    case 0:
	      break;

	    default:
	      g_return_val_if_reached(FALSE);
	    }
	  
	  if (format[i + 1] != 0) /* not the last item */
	    {
	      if (! sg_dumper_write_str(dumper, " ", err))
		return FALSE;
	    }
	}

      if (terminator)
	{
	  if (! sg_dumper_write_str(dumper, terminator, err))
	    return FALSE;
	}

      if (! sg_dumper_write_str(dumper, "\n", err))
	return FALSE;
    }

  va_end(args);
  
  return TRUE;
}

static gboolean
sg_dumper_write_indent (SGDumper *dumper, GError **err)
{
  char *indent;
  gboolean status;

  indent = g_strnfill(dumper->depth, '\t');
  status = sg_dumper_write_str(dumper, indent, err);
  g_free(indent);

  return status;
}

static gboolean
sg_dumper_printf (SGDumper *dumper,
		  GError **err,
		  const char *format,
		  ...)
{
  va_list args;
  char *str;
  gboolean status;

  va_start(args, format);
  str = g_strdup_vprintf(format, args);
  va_end(args);

  status = sg_dumper_write_str(dumper, str, err);
  g_free(str);
  
  return status;
}

static gboolean
sg_dumper_write_str (SGDumper *dumper,
		     const char *str,
		     GError **err)
{
  dumper->ok = g_io_channel_write_chars(dumper->io_channel,
					str,
					-1,
					NULL,
					err) == G_IO_STATUS_NORMAL;

  return dumper->ok;
}

static gboolean
sg_dumper_write_value (SGDumper *dumper,
		       const GValue *value,
		       GError **err)
{
  g_return_val_if_fail(dumper != NULL, FALSE);
  g_return_val_if_fail(value != NULL, FALSE);

  if (G_VALUE_HOLDS_BOOLEAN(value))
    {
      gboolean val = g_value_get_boolean(value);
      return sg_dumper_write_str(dumper, val ? "yes" : "no", err);
    }
  else if (G_VALUE_HOLDS_INT(value))
    return sg_dumper_printf(dumper, err, "%i", g_value_get_int(value));
  else if (G_VALUE_HOLDS_UINT(value))
    return sg_dumper_printf(dumper, err, "%u", g_value_get_uint(value));
  else if (G_VALUE_HOLDS_DOUBLE(value))
    return sg_dumper_printf(dumper, err, "%f", g_value_get_double(value));
  else if (G_VALUE_HOLDS_STRING(value))
    {
      const char *val;
      char *escaped;
      gboolean status;

      val = g_value_get_string(value);
      escaped = g_strescape(val ? val : "", NULL);
      status = sg_dumper_printf(dumper, err, "\"%s\"", escaped);
      g_free(escaped);

      return status;
    }
  else
    g_return_val_if_reached(FALSE);
}
