/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/* camel-disco-diary.c: class for a disconnected operation log */

/* 
 * Authors: Dan Winship <danw@ximian.com>
 *
 * Copyright (C) 2001 Ximian, Inc.
 *
 * This program is free software; you can redistribute it and/or 
 * modify it under the terms of version 2 of the GNU 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
 * USA
 */

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

#define __USE_LARGEFILE 1
#include <stdio.h>
#include <errno.h>

#include "camel-disco-diary.h"
#include "camel-disco-folder.h"
#include "camel-disco-store.h"
#include "camel-exception.h"
#include "camel-file-utils.h"
#include "camel-folder.h"
#include "camel-operation.h"
#include "camel-session.h"
#include "camel-store.h"


static void
camel_disco_diary_class_init (CamelDiscoDiaryClass *camel_disco_diary_class)
{
	/* virtual method definition */
}

static void
camel_disco_diary_init (CamelDiscoDiary *diary)
{
	diary->folders = g_hash_table_new (g_str_hash, g_str_equal);
	diary->uidmap = g_hash_table_new (g_str_hash, g_str_equal);
}

static void
unref_folder (gpointer key, gpointer value, gpointer data)
{
	camel_object_unref (value);
}

static void
free_uid (gpointer key, gpointer value, gpointer data)
{
	g_free (key);
	g_free (value);
}

static void
camel_disco_diary_finalize (CamelDiscoDiary *diary)
{
	if (diary->file)
		fclose (diary->file);
	if (diary->folders) {
		g_hash_table_foreach (diary->folders, unref_folder, NULL);
		g_hash_table_destroy (diary->folders);
	}
	if (diary->uidmap) {
		g_hash_table_foreach (diary->uidmap, free_uid, NULL);
		g_hash_table_destroy (diary->uidmap);
	}
}

CamelType
camel_disco_diary_get_type (void)
{
	static CamelType camel_disco_diary_type = CAMEL_INVALID_TYPE;

	if (camel_disco_diary_type == CAMEL_INVALID_TYPE) {
		camel_disco_diary_type = camel_type_register (
			CAMEL_OBJECT_TYPE, "CamelDiscoDiary",
			sizeof (CamelDiscoDiary),
			sizeof (CamelDiscoDiaryClass),
			(CamelObjectClassInitFunc) camel_disco_diary_class_init,
			NULL,
			(CamelObjectInitFunc) camel_disco_diary_init,
			(CamelObjectFinalizeFunc) camel_disco_diary_finalize);
	}

	return camel_disco_diary_type;
}


static int
diary_encode_uids (CamelDiscoDiary *diary, GPtrArray *uids)
{
	int i, status;

	status = camel_file_util_encode_uint32 (diary->file, uids->len);
	for (i = 0; status != -1 && i < uids->len; i++)
		status = camel_file_util_encode_string (diary->file, uids->pdata[i]);
	return status;
}

void
camel_disco_diary_log (CamelDiscoDiary *diary, CamelDiscoDiaryAction action,
		       ...)
{
	va_list ap;
	int status;

	/* You may already be a loser. */
	if (!diary->file)
		return;

	status = camel_file_util_encode_uint32 (diary->file, action);
	if (status == -1)
		goto lose;

	va_start (ap, action);
	switch (action) {
	case CAMEL_DISCO_DIARY_FOLDER_EXPUNGE:
	{
		CamelFolder *folder = va_arg (ap, CamelFolder *);
		GPtrArray *uids = va_arg (ap, GPtrArray *);

		status = camel_file_util_encode_string (diary->file, folder->full_name);
		if (status != -1)
			status = diary_encode_uids (diary, uids);
		break;
	}

	case CAMEL_DISCO_DIARY_FOLDER_APPEND:
	{
		CamelFolder *folder = va_arg (ap, CamelFolder *);
		char *uid = va_arg (ap, char *);

		status = camel_file_util_encode_string (diary->file, folder->full_name);
		if (status != -1)
			status = camel_file_util_encode_string (diary->file, uid);
		break;
	}

	case CAMEL_DISCO_DIARY_FOLDER_TRANSFER:
	{
		CamelFolder *source = va_arg (ap, CamelFolder *);
		CamelFolder *destination = va_arg (ap, CamelFolder *);
		GPtrArray *uids = va_arg (ap, GPtrArray *);
		gboolean delete_originals = va_arg (ap, gboolean);

		status = camel_file_util_encode_string (diary->file, source->full_name);
		if (status == -1)
			break;
		status = camel_file_util_encode_string (diary->file, destination->full_name);
		if (status == -1)
			break;
		status = diary_encode_uids (diary, uids);
		if (status == -1)
			break;
		status = camel_file_util_encode_uint32 (diary->file, delete_originals);
		break;
	}

	default:
		g_assert_not_reached ();
		break;
	}

	va_end (ap);

 lose:
	if (status == -1) {
		char *msg;

		msg = g_strdup_printf (_("Could not write log entry: %s\n"
					 "Further operations on this server "
					 "will not be replayed when you\n"
					 "reconnect to the network."),
				       g_strerror (errno));
		camel_session_alert_user (camel_service_get_session (CAMEL_SERVICE (diary->store)),
					  CAMEL_SESSION_ALERT_ERROR,
					  msg, FALSE);
		g_free (msg);

		fclose (diary->file);
		diary->file = NULL;
	}
}

static void
free_uids (GPtrArray *array)
{
	while (array->len--)
		g_free (array->pdata[array->len]);
	g_ptr_array_free (array, TRUE);
}

static GPtrArray *
diary_decode_uids (CamelDiscoDiary *diary)
{
	GPtrArray *uids;
	char *uid;
	guint32 i;

	if (camel_file_util_decode_uint32 (diary->file, &i) == -1)
		return NULL;
	uids = g_ptr_array_new ();
	while (i--) {
		if (camel_file_util_decode_string (diary->file, &uid) == -1) {
			free_uids (uids);
			return NULL;
		}
		g_ptr_array_add (uids, uid);
	}

	return uids;
}

static CamelFolder *
diary_decode_folder (CamelDiscoDiary *diary)
{
	CamelFolder *folder;
	char *name;

	if (camel_file_util_decode_string (diary->file, &name) == -1)
		return NULL;
	folder = g_hash_table_lookup (diary->folders, name);
	if (!folder) {
		CamelException ex;
		char *msg;

		camel_exception_init (&ex);
		folder = camel_store_get_folder (CAMEL_STORE (diary->store),
						 name, 0, &ex);
		if (folder)
			g_hash_table_insert (diary->folders, name, folder);
		else {
			msg = g_strdup_printf (_("Could not open `%s':\n%s\nChanges made to this folder will not be resynchronized."),
					       name, camel_exception_get_description (&ex));
			camel_exception_clear (&ex);
			camel_session_alert_user (camel_service_get_session (CAMEL_SERVICE (diary->store)),
						  CAMEL_SESSION_ALERT_WARNING,
						  msg, FALSE);
			g_free (msg);
			g_free (name);
		}
	} else
		g_free (name);
	return folder;
}

static void
close_folder (gpointer name, gpointer folder, gpointer data)
{
	g_free (name);
	camel_folder_sync (folder, FALSE, NULL);
	camel_object_unref (folder);
}

void
camel_disco_diary_replay (CamelDiscoDiary *diary, CamelException *ex)
{
	guint32 action;
	off_t size;
	double pc;

	fseek (diary->file, 0, SEEK_END);
	size = ftell (diary->file);
	g_return_if_fail (size != 0);
	rewind (diary->file);

	camel_operation_start (NULL, _("Resynchronizing with server"));
	while (!camel_exception_is_set (ex)) {
		pc = ftell (diary->file) / size;
		camel_operation_progress (NULL, pc * 100);

		if (camel_file_util_decode_uint32 (diary->file, &action) == -1)
			break;
		if (action == CAMEL_DISCO_DIARY_END)
			break;

		switch (action) {
		case CAMEL_DISCO_DIARY_FOLDER_EXPUNGE:
		{
			CamelFolder *folder;
			GPtrArray *uids;

			folder = diary_decode_folder (diary);
			uids = diary_decode_uids (diary);
			if (!uids)
				goto lose;

			if (folder)
				camel_disco_folder_expunge_uids (folder, uids, ex);
			free_uids (uids);
			break;
		}

		case CAMEL_DISCO_DIARY_FOLDER_APPEND:
		{
			CamelFolder *folder;
			char *uid, *ret_uid;
			CamelMimeMessage *message;
			CamelMessageInfo *info;

			folder = diary_decode_folder (diary);
			if (camel_file_util_decode_string (diary->file, &uid) == -1)
				goto lose;

			if (!folder) {
				g_free (uid);
				continue;
			}

			message = camel_folder_get_message (folder, uid, NULL);
			if (!message) {
				/* The message was appended and then deleted. */
				g_free (uid);
				continue;
			}
			info = camel_folder_get_message_info (folder, uid);

			camel_folder_append_message (folder, message, info, &ret_uid, ex);
			camel_folder_free_message_info (folder, info);

			if (ret_uid) {
				camel_disco_diary_uidmap_add (diary, uid, ret_uid);
				g_free (ret_uid);
			}
			g_free (uid);

			break;
		}

		case CAMEL_DISCO_DIARY_FOLDER_TRANSFER:
		{
			CamelFolder *source, *destination;
			GPtrArray *uids, *ret_uids;
			guint32 delete_originals;
			int i;

			source = diary_decode_folder (diary);
			destination = diary_decode_folder (diary);
			uids = diary_decode_uids (diary);
			if (!uids)
				goto lose;
			if (camel_file_util_decode_uint32 (diary->file, &delete_originals) == -1)
				goto lose;

			if (!source || !destination) {
				free_uids (uids);
				continue;
			}

			camel_folder_transfer_messages_to (source, uids, destination, &ret_uids, delete_originals, ex);

			if (ret_uids) {
				for (i = 0; i < uids->len; i++) {
					if (!ret_uids->pdata[i])
						continue;
					camel_disco_diary_uidmap_add (diary, uids->pdata[i], ret_uids->pdata[i]);
					g_free (ret_uids->pdata[i]);
				}
				g_ptr_array_free (ret_uids, TRUE);
			}
			free_uids (uids);
			break;
		}

		}
	}

 lose:
	camel_operation_end (NULL);

	/* Close folders */
	g_hash_table_foreach (diary->folders, close_folder, diary);
	g_hash_table_destroy (diary->folders);
	diary->folders = NULL;

	/* Truncate the log */
	ftruncate (fileno (diary->file), 0);
}

CamelDiscoDiary *
camel_disco_diary_new (CamelDiscoStore *store, const char *filename, CamelException *ex)
{
	CamelDiscoDiary *diary;

	g_return_val_if_fail (CAMEL_IS_DISCO_STORE (store), NULL);
	g_return_val_if_fail (filename != NULL, NULL);

	diary = CAMEL_DISCO_DIARY (camel_object_new (CAMEL_DISCO_DIARY_TYPE));
	diary->store = store;

	diary->file = fopen (filename, "a+");
	if (!diary->file) {
		camel_object_unref (CAMEL_OBJECT (diary));
		camel_exception_setv (ex, CAMEL_EXCEPTION_SYSTEM,
				      "Could not open journal file: %s",
				      g_strerror (errno));
		return NULL;
	}

	return diary;
}

gboolean
camel_disco_diary_empty  (CamelDiscoDiary *diary)
{
	return ftell (diary->file) == 0;
}

void
camel_disco_diary_uidmap_add (CamelDiscoDiary *diary, const char *old_uid,
			      const char *new_uid)
{
	g_hash_table_insert (diary->uidmap, g_strdup (old_uid),
			     g_strdup (new_uid));
}

const char *
camel_disco_diary_uidmap_lookup (CamelDiscoDiary *diary, const char *uid)
{
	return g_hash_table_lookup (diary->uidmap, uid);
}