/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- *
 *
 * Author:
 *  Michael Zucchi <notzed@ximian.com>
 *
 * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of version 2 of the GNU Lesser General Public
 * License as published by the Free Software Foundation.
 *
 * This program 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
 * USA
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <errno.h>
#include <stdio.h>
#include <string.h>

#include <glib/gstdio.h>

#include "camel-file-utils.h"
#include "camel-object.h"

#define d(x)

#define CAMEL_OBJECT_GET_PRIVATE(obj) \
	(G_TYPE_INSTANCE_GET_PRIVATE \
	((obj), CAMEL_TYPE_OBJECT, CamelObjectPrivate))

struct _CamelObjectPrivate {
	gchar *state_filename;
};

enum {
	PROP_0,
	PROP_STATE_FILENAME
};

G_DEFINE_ABSTRACT_TYPE (CamelObject, camel_object, G_TYPE_OBJECT)

/* State file for CamelObject data.
 * Any later versions should only append data.
 *
 * version:uint32
 *
 * Version 0 of the file:
 *
 * version:uint32 = 0
 * count:uint32				-- count of meta-data items
 * ( name:string value:string ) *count		-- meta-data items
 *
 * Version 1 of the file adds:
 * count:uint32					-- count of persistent properties
 * ( tag:uing32 value:tagtype ) *count		-- persistent properties
 */

#define CAMEL_OBJECT_STATE_FILE_MAGIC "CLMD"

/* XXX This is a holdover from Camel's old homegrown type system.
 *     CamelArg was a kind of primitive version of GObject properties.
 *     The argument ID and data type were encoded into a 32-bit integer.
 *     Unfortunately the encoding was also used in the binary state file
 *     format, so we still need the secret decoder ring. */
enum camel_arg_t {
	CAMEL_ARG_END = 0,
	CAMEL_ARG_IGNORE = 1,	/* override/ignore an arg in-place */

	CAMEL_ARG_FIRST = 1024,	/* 1024 args reserved for arg system */

	CAMEL_ARG_TYPE = 0xf0000000, /* type field for tags */
	CAMEL_ARG_TAG = 0x0fffffff, /* tag field for args */

	CAMEL_ARG_OBJ = 0x00000000, /* object */
	CAMEL_ARG_INT = 0x10000000, /* gint */
	CAMEL_ARG_DBL = 0x20000000, /* gdouble */
	CAMEL_ARG_STR = 0x30000000, /* c string */
	CAMEL_ARG_PTR = 0x40000000, /* ptr */
	CAMEL_ARG_BOO = 0x50000000  /* bool */
};

#define CAMEL_ARGV_MAX (20)

static void
object_set_property (GObject *object,
                     guint property_id,
                     const GValue *value,
                     GParamSpec *pspec)
{
	switch (property_id) {
		case PROP_STATE_FILENAME:
			camel_object_set_state_filename (
				CAMEL_OBJECT (object),
				g_value_get_string (value));
			return;
	}

	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}

static void
object_get_property (GObject *object,
                     guint property_id,
                     GValue *value,
                     GParamSpec *pspec)
{
	switch (property_id) {
		case PROP_STATE_FILENAME:
			g_value_set_string (
				value, camel_object_get_state_filename (
				CAMEL_OBJECT (object)));
			return;
	}

	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}

static void
object_finalize (GObject *object)
{
	CamelObjectPrivate *priv;

	priv = CAMEL_OBJECT_GET_PRIVATE (object);

	g_free (priv->state_filename);

	/* Chain up to parent's finalize() method. */
	G_OBJECT_CLASS (camel_object_parent_class)->finalize (object);
}

static gint
object_state_read (CamelObject *object,
                   FILE *fp)
{
	GValue value = G_VALUE_INIT;
	GObjectClass *class;
	GParamSpec **properties;
	guint32 count, version;
	guint ii, jj, n_properties;

	if (camel_file_util_decode_uint32 (fp, &version) == -1)
		return -1;

	if (version > 1)
		return -1;

	if (camel_file_util_decode_uint32 (fp, &count) == -1)
		return -1;

	/* XXX Camel no longer supports meta-data in state
	 *     files, so we're just eating dead data here. */
	for (ii = 0; ii < count; ii++) {
		gchar *name = NULL;
		gchar *value = NULL;
		gboolean success;

		success =
			camel_file_util_decode_string (fp, &name) == 0 &&
			camel_file_util_decode_string (fp, &value) == 0;

		g_free (name);
		g_free (value);

		if (!success)
			return -1;
	}

	if (version == 0)
		return 0;

	if (camel_file_util_decode_uint32 (fp, &count) == -1)
		return 0;

	if (count == 0 || count > 1024)
		/* Maybe it was just version 0 afterall. */
		return 0;

	count = MIN (count, CAMEL_ARGV_MAX);

	class = G_OBJECT_GET_CLASS (object);
	properties = g_object_class_list_properties (class, &n_properties);

	for (ii = 0; ii < count; ii++) {
		gboolean property_set = FALSE;
		guint32 tag, v_uint32;

		if (camel_file_util_decode_uint32 (fp, &tag) == -1)
			goto exit;

		/* Record state file values into GValues.
		 * XXX We currently only support booleans. */
		switch (tag & CAMEL_ARG_TYPE) {
			case CAMEL_ARG_BOO:
				if (camel_file_util_decode_uint32 (fp, &v_uint32) == -1)
					goto exit;
				g_value_init (&value, G_TYPE_BOOLEAN);
				g_value_set_boolean (&value, (gboolean) v_uint32);
				break;
			default:
				g_warn_if_reached ();
				goto exit;
		}

		/* Now we have to match the legacy numeric CamelArg tag
		 * value with a GObject property.  The GObject property
		 * IDs have been set to the same legacy tag values, but
		 * we have to access a private GParamSpec field to get
		 * to them (pspec->param_id). */

		tag &= CAMEL_ARG_TAG;  /* filter out the type code */

		for (jj = 0; jj < n_properties; jj++) {
			GParamSpec *pspec = properties[jj];

			if (pspec->param_id != tag)
				continue;

			/* Sanity check. */
			g_warn_if_fail (pspec->flags & CAMEL_PARAM_PERSISTENT);
			if ((pspec->flags & CAMEL_PARAM_PERSISTENT) == 0)
				continue;

			g_object_set_property (
				G_OBJECT (object), pspec->name, &value);

			property_set = TRUE;
			break;
		}

		/* XXX This tag was used by the old IMAP backend.
		 *     It may still show up in accounts that were
		 *     migrated from IMAP to IMAPX.  Silence the
		 *     warning. */
		if (tag == 0x2500)
			property_set = TRUE;

		if (!property_set)
			g_warning (
				"Could not find a corresponding %s "
				"property for state file tag 0x%x",
				G_OBJECT_TYPE_NAME (object), tag);

		g_value_unset (&value);
	}

exit:
	g_free (properties);

	return 0;
}

static gint
object_state_write (CamelObject *object,
                    FILE *fp)
{
	GValue value = G_VALUE_INIT;
	GObjectClass *class;
	GParamSpec **properties;
	guint ii, n_properties;
	guint32 n_persistent = 0;

	class = G_OBJECT_GET_CLASS (object);
	properties = g_object_class_list_properties (class, &n_properties);

	/* Version = 1 */
	if (camel_file_util_encode_uint32 (fp, 1) == -1)
		goto exit;

	/* No meta-data items. */
	if (camel_file_util_encode_uint32 (fp, 0) == -1)
		goto exit;

	/* Count persistent properties. */
	for (ii = 0; ii < n_properties; ii++)
		if (properties[ii]->flags & CAMEL_PARAM_PERSISTENT)
			n_persistent++;

	if (camel_file_util_encode_uint32 (fp, n_persistent) == -1)
		goto exit;

	/* Write a tag + value pair for each persistent property.
	 * Tags identify the property ID and data type; they're an
	 * artifact of CamelArgs.  The persistent GObject property
	 * IDs are set to match the legacy CamelArg tag values. */

	for (ii = 0; ii < n_properties; ii++) {
		GParamSpec *pspec = properties[ii];
		guint32 tag, v_uint32;

		if ((pspec->flags & CAMEL_PARAM_PERSISTENT) == 0)
			continue;

		g_value_init (&value, pspec->value_type);

		g_object_get_property (
			G_OBJECT (object), pspec->name, &value);

		tag = pspec->param_id;

		/* Record the GValue to the state file.
		 * XXX We currently only support booleans. */
		switch (pspec->value_type) {
			case G_TYPE_BOOLEAN:
				tag |= CAMEL_ARG_BOO;
				v_uint32 = g_value_get_boolean (&value);
				if (camel_file_util_encode_uint32 (fp, tag) == -1)
					goto exit;
				if (camel_file_util_encode_uint32 (fp, v_uint32) == -1)
					goto exit;
				break;
			default:
				g_warn_if_reached ();
				goto exit;
		}

		g_value_unset (&value);
	}

exit:
	g_free (properties);

	return 0;
}

static void
camel_object_class_init (CamelObjectClass *class)
{
	GObjectClass *object_class;

	g_type_class_add_private (class, sizeof (CamelObjectPrivate));

	object_class = G_OBJECT_CLASS (class);
	object_class->set_property = object_set_property;
	object_class->get_property = object_get_property;
	object_class->finalize = object_finalize;

	class->state_read = object_state_read;
	class->state_write = object_state_write;

	/**
	 * CamelObject:state-filename
	 *
	 * The file in which to store persistent property values for this
	 * instance.
	 **/
	g_object_class_install_property (
		object_class,
		PROP_STATE_FILENAME,
		g_param_spec_string (
			"state-filename",
			"State Filename",
			"File containing persistent property values",
			NULL,
			G_PARAM_READWRITE |
			G_PARAM_CONSTRUCT));
}

static void
camel_object_init (CamelObject *object)
{
	object->priv = CAMEL_OBJECT_GET_PRIVATE (object);
}

G_DEFINE_QUARK (camel-error-quark, camel_error)

/**
 * camel_object_state_read:
 * @object: a #CamelObject
 *
 * Read persistent object state from #CamelObject:state-filename.
 *
 * Returns: -1 on error.
 **/
gint
camel_object_state_read (CamelObject *object)
{
	CamelObjectClass *class;
	const gchar *state_filename;
	gint res = -1;
	FILE *fp;
	gchar magic[4];

	g_return_val_if_fail (CAMEL_IS_OBJECT (object), -1);

	class = CAMEL_OBJECT_GET_CLASS (object);

	state_filename = camel_object_get_state_filename (object);
	if (state_filename == NULL)
		return 0;

	fp = g_fopen (state_filename, "rb");
	if (fp != NULL) {
		if (fread (magic, 4, 1, fp) == 1
		    && memcmp (magic, CAMEL_OBJECT_STATE_FILE_MAGIC, 4) == 0)
			res = class->state_read (object, fp);
		fclose (fp);
	}

	return res;
}

/**
 * camel_object_state_write:
 * @object: a #CamelObject
 *
 * Write persistent object state #CamelObject:state-filename.
 *
 * Returns: -1 on error.
 **/
gint
camel_object_state_write (CamelObject *object)
{
	CamelObjectClass *class;
	const gchar *state_filename;
	gchar *savename, *dirname;
	gint res = -1;
	FILE *fp;

	g_return_val_if_fail (CAMEL_IS_OBJECT (object), -1);

	class = CAMEL_OBJECT_GET_CLASS (object);

	state_filename = camel_object_get_state_filename (object);
	if (state_filename == NULL)
		return 0;

	savename = camel_file_util_savename (state_filename);

	dirname = g_path_get_dirname (savename);
	g_mkdir_with_parents (dirname, 0700);
	g_free (dirname);

	fp = g_fopen (savename, "wb");
	if (fp != NULL) {
		if (fwrite (CAMEL_OBJECT_STATE_FILE_MAGIC, 4, 1, fp) == 1
		    && class->state_write (object, fp) == 0) {
			if (fclose (fp) == 0) {
				res = 0;
				g_rename (savename, state_filename);
			}
		} else {
			fclose (fp);
		}
	} else {
		g_warning ("Could not save object state file to '%s': %s", savename, g_strerror (errno));
	}

	g_free (savename);

	return res;
}

/**
 * camel_object_get_state_filename:
 * @object: a #CamelObject
 *
 * Returns the name of the file in which persistent property values for
 * @object are stored.  The file is used by camel_object_state_write()
 * and camel_object_state_read() to save and restore object state.
 *
 * Returns: the name of the persistent property file
 *
 * Since: 2.32
 **/
const gchar *
camel_object_get_state_filename (CamelObject *object)
{
	g_return_val_if_fail (CAMEL_IS_OBJECT (object), NULL);

	return object->priv->state_filename;
}

/**
 * camel_object_set_state_filename:
 * @object: a #CamelObject
 * @state_filename: path to a local file
 *
 * Sets the name of the file in which persistent property values for
 * @object are stored.  The file is used by camel_object_state_write()
 * and camel_object_state_read() to save and restore object state.
 *
 * Since: 2.32
 **/
void
camel_object_set_state_filename (CamelObject *object,
                                 const gchar *state_filename)
{
	g_return_if_fail (CAMEL_IS_OBJECT (object));

	if (g_strcmp0 (object->priv->state_filename, state_filename) == 0)
		return;

	g_free (object->priv->state_filename);
	object->priv->state_filename = g_strdup (state_filename);

	g_object_notify (G_OBJECT (object), "state-filename");
}
