/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 *
 * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
 *
 * Authors: Michael Zucchi <notzed@ximian.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 <dirent.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>

#include <glib/gstdio.h>
#include <glib/gi18n-lib.h>

#include "camel-mh-folder.h"
#include "camel-mh-settings.h"
#include "camel-mh-store.h"
#include "camel-mh-summary.h"

#define d(x)

G_DEFINE_TYPE (CamelMhStore, camel_mh_store, CAMEL_TYPE_LOCAL_STORE)

enum {
	UPDATE_NONE,
	UPDATE_ADD,
	UPDATE_REMOVE,
	UPDATE_RENAME
};

/* update the .folders file if it exists, or create it if it doesn't */
static void
folders_update (const gchar *root,
                gint mode,
                const gchar *folder,
                const gchar *new,
                GCancellable *cancellable)
{
	gchar *tmp, *tmpnew, *line = NULL;
	CamelStream *stream, *in = NULL, *out = NULL;
	gchar *folder_newline;
	gint flen = strlen (folder);

	folder_newline = g_strdup_printf ("%s\n", folder);

	tmpnew = g_alloca (strlen (root) + 16);
	sprintf (tmpnew, "%s.folders~", root);

	out = camel_stream_fs_new_with_name (
		tmpnew, O_WRONLY|O_CREAT|O_TRUNC, 0666, NULL);
	if (out == NULL)
		goto fail;

	tmp = g_alloca (strlen (root) + 16);
	sprintf (tmp, "%s.folders", root);
	stream = camel_stream_fs_new_with_name (tmp, O_RDONLY, 0, NULL);
	if (stream) {
		in = camel_stream_buffer_new (stream, CAMEL_STREAM_BUFFER_READ);
		g_object_unref (stream);
	}
	if (in == NULL || stream == NULL) {
		if (mode == UPDATE_ADD) {
			gint ret;

			ret = camel_stream_write_string (
				out, folder_newline, cancellable, NULL);

			if (ret == -1)
				goto fail;
		}
		goto done;
	}

	while ((line = camel_stream_buffer_read_line ((CamelStreamBuffer *) in, cancellable, NULL))) {
		gint copy = TRUE;

		switch (mode) {
		case UPDATE_REMOVE:
			if (strcmp (line, folder) == 0)
				copy = FALSE;
			break;
		case UPDATE_RENAME:
			if (strncmp (line, folder, flen) == 0
			    && (line[flen] == 0 || line[flen] == '/')) {
				if (camel_stream_write (out, new, strlen (new), cancellable, NULL) == -1
				    || camel_stream_write (out, line+flen, strlen (line)-flen, cancellable, NULL) == -1
				    || camel_stream_write(out, "\n", 1, cancellable, NULL) == -1)
					goto fail;
				copy = FALSE;
			}
			break;
		case UPDATE_ADD: {
			gint cmp = strcmp (line, folder);

			if (cmp > 0) {
				gint ret;

				/* found insertion point */
				ret = camel_stream_write_string (
					out, folder_newline, cancellable, NULL);

				if (ret == -1)
					goto fail;
				mode = UPDATE_NONE;
			} else if (tmp == NULL) {
				/* already there */
				mode = UPDATE_NONE;
			}
			break; }
		case UPDATE_NONE:
			break;
		}

		if (copy) {
			gchar *string;
			gint ret;

			string = g_strdup_printf ("%s\n", line);
			ret = camel_stream_write_string (
				out, string, cancellable, NULL);
			g_free (string);

			if (ret == -1)
				goto fail;
		}

		g_free (line);
		line = NULL;
	}

	/* add to end? */
	if (mode == UPDATE_ADD) {
		gint ret;

		ret = camel_stream_write_string (
			out, folder_newline, cancellable, NULL);

		if (ret == -1)
			goto fail;
	}

	if (camel_stream_close (out, cancellable, NULL) == -1)
		goto fail;

done:
	/* should we care if this fails?  I suppose so ... */
	g_rename (tmpnew, tmp);
fail:
	unlink (tmpnew);		/* remove it if its there */
	g_free (line);
	if (in)
		g_object_unref (in);
	if (out)
		g_object_unref (out);

	g_free (folder_newline);
}

static void
fill_fi (CamelStore *store,
         CamelFolderInfo *fi,
         guint32 flags,
         GCancellable *cancellable)
{
	CamelLocalStore *local_store;
	CamelFolder *folder;

	local_store = CAMEL_LOCAL_STORE (store);
	folder = camel_object_bag_peek (store->folders, fi->full_name);

	if (folder == NULL
	    && (flags & CAMEL_STORE_FOLDER_INFO_FAST) == 0)
		folder = camel_store_get_folder_sync (
			store, fi->full_name, 0, cancellable, NULL);

	if (folder != NULL) {
		if ((flags & CAMEL_STORE_FOLDER_INFO_FAST) == 0)
			camel_folder_refresh_info_sync (folder, cancellable, NULL);
		fi->unread = camel_folder_get_unread_message_count (folder);
		fi->total = camel_folder_get_message_count (folder);
		g_object_unref (folder);

	} else {
		gchar *path, *folderpath;
		CamelFolderSummary *s;
		const gchar *root;

		/* This should be fast enough not to have to test for INFO_FAST */

		/* We could: if we have no folder, and FAST isn't specified, perform a full
		   scan of all messages for their status flags.  But its probably not worth
		   it as we need to read the top of every file, i.e. very very slow */

		root = camel_local_store_get_toplevel_dir (local_store);
		path = g_strdup_printf (
			"%s/%s.ev-summary", root, fi->full_name);
		folderpath = g_strdup_printf ("%s/%s", root, fi->full_name);
		s = (CamelFolderSummary *) camel_mh_summary_new (
			NULL, path, folderpath, NULL);
		if (camel_folder_summary_header_load_from_db (
			s, store, fi->full_name, NULL) != -1) {
			fi->unread = s->unread_count;
			fi->total = s->saved_count;
		}
		g_object_unref (s);
		g_free (folderpath);
		g_free (path);
	}

	if (camel_local_store_is_main_store (local_store) && fi->full_name
	    && (fi->flags & CAMEL_FOLDER_TYPE_MASK) == CAMEL_FOLDER_TYPE_NORMAL)
		fi->flags =
			(fi->flags & ~CAMEL_FOLDER_TYPE_MASK) |
			camel_local_store_get_folder_type_by_full_name (
				local_store, fi->full_name);
}

static CamelFolderInfo *
folder_info_new (CamelStore *store,
                 const gchar *root,
                 const gchar *path,
                 guint32 flags,
                 GCancellable *cancellable)
{
	/* FIXME Need to set fi->flags = CAMEL_FOLDER_NOSELECT
	 *       (and possibly others) when appropriate. */
	CamelFolderInfo *fi;
	gchar *base;

	base = strrchr (path, '/');

	/* Build the folder info structure. */
	fi = camel_folder_info_new ();
	fi->full_name = g_strdup (path);
	fi->display_name = g_strdup (base?base+1:path);
	fill_fi (store, fi, flags, cancellable);

	return fi;
}

/* used to find out where we've visited already */
struct _inode {
	dev_t dnode;
	ino_t inode;
};

/* Scan path, under root, for directories to add folders for.  Both
 * root and path should have a trailing "/" if they aren't empty. */
static void
recursive_scan (CamelStore *store,
                CamelFolderInfo **fip,
                CamelFolderInfo *parent,
                GHashTable *visited,
                const gchar *root,
                const gchar *path,
                guint32 flags,
                GCancellable *cancellable)
{
	gchar *fullpath, *tmp;
	DIR *dp;
	struct dirent *d;
	struct stat st;
	CamelFolderInfo *fi;
	struct _inode in, *inew;

	/* Open the specified directory. */
	if (path[0]) {
		fullpath = alloca (strlen (root) + strlen (path) + 2);
		sprintf (fullpath, "%s/%s", root, path);
	} else
		fullpath = (gchar *) root;

	if (g_stat (fullpath, &st) == -1 || !S_ISDIR (st.st_mode))
		return;

	in.dnode = st.st_dev;
	in.inode = st.st_ino;

	/* see if we've visited already */
	if (g_hash_table_lookup (visited, &in) != NULL)
		return;

	inew = g_malloc (sizeof (*inew));
	*inew = in;
	g_hash_table_insert (visited, inew, inew);

	/* link in ... */
	fi = folder_info_new (store, root, path, flags, cancellable);
	fi->parent = parent;
	fi->next = *fip;
	*fip = fi;

	if (((flags & CAMEL_STORE_FOLDER_INFO_RECURSIVE) || parent == NULL)) {
		/* now check content for possible other directories */
		dp = opendir (fullpath);
		if (dp == NULL)
			return;

		/* Look for subdirectories to add and scan. */
		while ((d = readdir (dp)) != NULL) {
			/* Skip current and parent directory. */
			if (strcmp(d->d_name, ".") == 0
			    || strcmp(d->d_name, "..") == 0)
				continue;

			/* skip fully-numerical entries (i.e. mh messages) */
			strtoul (d->d_name, &tmp, 10);
			if (*tmp == 0)
				continue;

			/* Otherwise, treat at potential node, and recurse,
			 * a bit more expensive than needed, but tough! */
			if (path[0]) {
				tmp = g_strdup_printf("%s/%s", path, d->d_name);
				recursive_scan (
					store, &fi->child, fi, visited,
					root, tmp, flags, cancellable);
				g_free (tmp);
			} else {
				recursive_scan (
					store, &fi->child, fi, visited,
					root, d->d_name, flags, cancellable);
			}
		}

		closedir (dp);
	}
}

/* scan a .folders file */
static void
folders_scan (CamelStore *store,
              const gchar *root,
              const gchar *top,
              CamelFolderInfo **fip,
              guint32 flags,
              GCancellable *cancellable)
{
	CamelFolderInfo *fi;
	gchar  line[512], *path, *tmp;
	CamelStream *stream, *in;
	struct stat st;
	GPtrArray *folders;
	GHashTable *visited;
	gint len;

	tmp = g_alloca (strlen (root) + 16);
	sprintf (tmp, "%s/.folders", root);
	stream = camel_stream_fs_new_with_name (tmp, 0, O_RDONLY, NULL);
	if (stream == NULL)
		return;

	in = camel_stream_buffer_new (stream, CAMEL_STREAM_BUFFER_READ);
	g_object_unref (stream);
	if (in == NULL)
		return;

	visited = g_hash_table_new (g_str_hash, g_str_equal);
	folders = g_ptr_array_new ();

	while ((len = camel_stream_buffer_gets (
		(CamelStreamBuffer *) in, line,
		sizeof (line), cancellable, NULL)) > 0) {

		/* ignore blank lines */
		if (len <= 1)
			continue;

		/* Check for invalidly long lines,
		 * we abort everything and fallback. */
		if (line[len-1] != '\n') {
			gint i;

			for (i=0;i<folders->len;i++)
				camel_folder_info_free (folders->pdata[i]);
			g_ptr_array_set_size (folders, 0);
			break;
		}
		line[len-1] = 0;

		/* check for \r ? */

		if (top && top[0]) {
			gint toplen = strlen (top);

			/* check is dir or subdir */
			if (strncmp (top, line, toplen) != 0
			    || (line[toplen] != 0 && line[toplen] != '/'))
				continue;

			/* check is not sub-subdir if not recursive */
			if ((flags & CAMEL_STORE_FOLDER_INFO_RECURSIVE) == 0
			    && (tmp = strrchr (line, '/'))
			    && tmp > line+toplen)
				continue;
		}

		if (g_hash_table_lookup (visited, line) != NULL)
			continue;

		tmp = g_strdup (line);
		g_hash_table_insert (visited, tmp, tmp);

		path = g_strdup_printf("%s/%s", root, line);
		if (g_stat (path, &st) == 0 && S_ISDIR (st.st_mode)) {
			fi = folder_info_new (
				store, root, line, flags, cancellable);
			g_ptr_array_add (folders, fi);
		}
		g_free (path);
	}

	if (folders->len)
		*fip = camel_folder_info_build(folders, top, '/', TRUE);
	g_ptr_array_free (folders, TRUE);

	g_hash_table_foreach (visited, (GHFunc) g_free, NULL);
	g_hash_table_destroy (visited);

	g_object_unref (in);
}

/* FIXME: move to camel-local, this is shared with maildir code */
static guint
inode_hash (gconstpointer d)
{
	const struct _inode *v = d;

	return v->inode ^ v->dnode;
}

static gboolean
inode_equal (gconstpointer a, gconstpointer b)
{
	const struct _inode *v1 = a, *v2 = b;

	return v1->inode == v2->inode && v1->dnode == v2->dnode;
}

static void
inode_free (gpointer k, gpointer v, gpointer d)
{
	g_free (k);
}

static CamelFolder *
mh_store_get_folder_sync (CamelStore *store,
                          const gchar *folder_name,
                          CamelStoreGetFolderFlags flags,
                          GCancellable *cancellable,
                          GError **error)
{
	CamelStoreClass *store_class;
	CamelLocalStore *local_store;
	CamelService *service;
	CamelSettings *settings;
	gboolean use_dot_folders;
	gchar *name;
	struct stat st;

	local_store = CAMEL_LOCAL_STORE (store);

	service = CAMEL_SERVICE (store);
	settings = camel_service_get_settings (service);

	use_dot_folders = camel_mh_settings_get_use_dot_folders (
		CAMEL_MH_SETTINGS (settings));

	/* Chain up to parent's get_folder() method. */
	store_class = CAMEL_STORE_CLASS (camel_mh_store_parent_class);
	if (store_class->get_folder_sync (
		store, folder_name, flags, cancellable, error) == NULL)
		return NULL;

	name = g_strconcat (local_store->toplevel_dir, folder_name, NULL);

	if (g_stat (name, &st) == -1) {
		if (errno != ENOENT) {
			g_set_error (
				error, G_IO_ERROR,
				g_io_error_from_errno (errno),
				_("Cannot get folder '%s': %s"),
				folder_name, g_strerror (errno));
			g_free (name);
			return NULL;
		}
		if ((flags & CAMEL_STORE_FOLDER_CREATE) == 0) {
			g_set_error (
				error, CAMEL_STORE_ERROR,
				CAMEL_STORE_ERROR_NO_FOLDER,
				_("Cannot get folder '%s': "
				  "folder does not exist."),
				folder_name);
			g_free (name);
			return NULL;
		}

		if (g_mkdir (name, 0777) != 0) {
			g_set_error (
				error, G_IO_ERROR,
				g_io_error_from_errno (errno),
				_("Could not create folder '%s': %s"),
				folder_name, g_strerror (errno));
			g_free (name);
			return NULL;
		}

		/* add to .folders if we are supposed to */
		/* FIXME: throw exception on error */
		if (use_dot_folders)
			folders_update (
				local_store->toplevel_dir,
				UPDATE_ADD, folder_name, NULL, cancellable);

	} else if (!S_ISDIR (st.st_mode)) {
		g_set_error (
			error, CAMEL_STORE_ERROR,
			CAMEL_STORE_ERROR_NO_FOLDER,
			_("Cannot get folder '%s': not a directory."),
			folder_name);
		g_free (name);
		return NULL;

	} else if (flags & CAMEL_STORE_FOLDER_EXCL) {
		g_set_error (
			error, CAMEL_ERROR, CAMEL_ERROR_GENERIC,
			_("Cannot create folder '%s': folder exists."),
			folder_name);
		g_free (name);
		return NULL;
	}

	g_free (name);

	return camel_mh_folder_new (
		store, folder_name, flags, cancellable, error);
}

static CamelFolderInfo *
mh_store_get_folder_info_sync (CamelStore *store,
                               const gchar *top,
                               CamelStoreGetFolderInfoFlags flags,
                               GCancellable *cancellable,
                               GError **error)
{
	CamelService *service;
	CamelSettings *settings;
	CamelFolderInfo *fi = NULL;
	CamelURL *url;
	gboolean use_dot_folders;

	service = CAMEL_SERVICE (store);
	url = camel_service_get_camel_url (service);
	settings = camel_service_get_settings (service);

	use_dot_folders = camel_mh_settings_get_use_dot_folders (
		CAMEL_MH_SETTINGS (settings));

	/* use .folders if we are supposed to */
	if (use_dot_folders) {
		folders_scan (
			store, url->path, top, &fi, flags, cancellable);
	} else {
		GHashTable *visited;

		visited = g_hash_table_new (inode_hash, inode_equal);

		if (top == NULL)
			top = "";

		recursive_scan (
			store, &fi, NULL, visited, url->path,
			top, flags, cancellable);

		/* If we actually scanned from root,
		 * we have a "" root node we dont want. */
		if (fi != NULL && top[0] == 0) {
			CamelFolderInfo *rfi;

			rfi = fi;
			fi = rfi->child;
			rfi->child = NULL;
			camel_folder_info_free (rfi);
		}

		g_hash_table_foreach (visited, inode_free, NULL);
		g_hash_table_destroy (visited);
	}

	return fi;
}

static CamelFolder *
mh_store_get_inbox_sync (CamelStore *store,
                         GCancellable *cancellable,
                         GError **error)
{
	return mh_store_get_folder_sync (
		store, "inbox", 0, cancellable, error);
}

static gboolean
mh_store_delete_folder_sync (CamelStore *store,
                             const gchar *folder_name,
                             GCancellable *cancellable,
                             GError **error)
{
	CamelStoreClass *store_class;
	CamelLocalStore *local_store;
	CamelService *service;
	CamelSettings *settings;
	gboolean use_dot_folders;
	gchar *name;

	local_store = CAMEL_LOCAL_STORE (store);

	service = CAMEL_SERVICE (store);
	settings = camel_service_get_settings (service);

	use_dot_folders = camel_mh_settings_get_use_dot_folders (
		CAMEL_MH_SETTINGS (settings));

	/* remove folder directory - will fail if not empty */
	name = g_strconcat (local_store->toplevel_dir, folder_name, NULL);
	if (rmdir (name) == -1) {
		g_set_error (
			error, G_IO_ERROR,
			g_io_error_from_errno (errno),
			_("Could not delete folder '%s': %s"),
			folder_name, g_strerror (errno));
		g_free (name);
		return FALSE;
	}
	g_free (name);

	/* remove from .folders if we are supposed to */
	if (use_dot_folders)
		folders_update (
			local_store->toplevel_dir,
			UPDATE_REMOVE, folder_name, NULL, cancellable);

	/* Chain up to parent's delete_folder() method. */
	store_class = CAMEL_STORE_CLASS (camel_mh_store_parent_class);
	return store_class->delete_folder_sync (
		store, folder_name, cancellable, error);
}

static gboolean
mh_store_rename_folder_sync (CamelStore *store,
                             const gchar *old,
                             const gchar *new,
                             GCancellable *cancellable,
                             GError **error)
{
	CamelStoreClass *store_class;
	CamelLocalStore *local_store;
	CamelService *service;
	CamelSettings *settings;
	gboolean use_dot_folders;

	local_store = CAMEL_LOCAL_STORE (store);

	service = CAMEL_SERVICE (store);
	settings = camel_service_get_settings (service);

	use_dot_folders = camel_mh_settings_get_use_dot_folders (
		CAMEL_MH_SETTINGS (settings));

	/* Chain up to parent's rename_folder() method. */
	store_class = CAMEL_STORE_CLASS (camel_mh_store_parent_class);
	if (!store_class->rename_folder_sync (
		store, old, new, cancellable, error))
		return FALSE;

	if (use_dot_folders) {
		/* yeah this is messy, but so is mh! */
		folders_update (
			local_store->toplevel_dir,
			UPDATE_RENAME, old, new, cancellable);
	}

	return TRUE;
}

static void
camel_mh_store_class_init (CamelMhStoreClass *class)
{
	CamelServiceClass *service_class;
	CamelStoreClass *store_class;

	service_class = CAMEL_SERVICE_CLASS (class);
	service_class->settings_type = CAMEL_TYPE_MH_SETTINGS;

	store_class = CAMEL_STORE_CLASS (class);
	store_class->get_folder_sync = mh_store_get_folder_sync;
	store_class->get_folder_info_sync = mh_store_get_folder_info_sync;
	store_class->get_inbox_folder_sync = mh_store_get_inbox_sync;
	store_class->delete_folder_sync = mh_store_delete_folder_sync;
	store_class->rename_folder_sync = mh_store_rename_folder_sync;
}

static void
camel_mh_store_init (CamelMhStore *mh_store)
{
}

