/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */

/*
 *  Engrampa
 *
 *  Copyright (C) 2008 Free Software Foundation, Inc.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include <string.h>
#include <glib.h>
#include <gio/gio.h>
#include "glib-utils.h"
#include "file-utils.h"
#include "gio-utils.h"

#define N_FILES_PER_REQUEST 128

/* -- filter -- */

typedef enum {
	FILTER_DEFAULT = 0,
	FILTER_NODOTFILES = 1 << 1,
	FILTER_IGNORECASE = 1 << 2,
	FILTER_NOBACKUPFILES = 1 << 3
} FilterOptions;

typedef struct {
	char           *pattern;
	FilterOptions   options;
	GRegex        **regexps;
} Filter;

static Filter *
filter_new (const char    *pattern,
	    FilterOptions  options)
{
	Filter             *filter;
	GRegexCompileFlags  flags;

	filter = g_new0 (Filter, 1);

	if ((pattern != NULL) && (strcmp (pattern, "*") != 0))
		filter->pattern = g_strdup (pattern);

	filter->options = options;
	if (filter->options & FILTER_IGNORECASE)
		flags = G_REGEX_CASELESS;
	else
		flags = 0;
	filter->regexps = search_util_get_regexps (pattern, flags);

	return filter;
}

static void
filter_destroy (Filter *filter)
{
	if (filter == NULL)
		return;

	g_free (filter->pattern);
	if (filter->regexps != NULL)
		free_regexps (filter->regexps);
	g_free (filter);
}

static gboolean
filter_matches (Filter     *filter,
	        const char *name)
{
	const char *file_name;
	char       *utf8_name;
	gboolean    matched;

	g_return_val_if_fail (name != NULL, FALSE);

	file_name = file_name_from_path (name);

	if ((filter->options & FILTER_NODOTFILES)
	    && ((file_name[0] == '.') || (strstr (file_name, "/.") != NULL)))
		return FALSE;

	if ((filter->options & FILTER_NOBACKUPFILES)
	    && (file_name[strlen (file_name) - 1] == '~'))
		return FALSE;

	if (filter->pattern == NULL)
		return TRUE;

	utf8_name = g_filename_to_utf8 (file_name, -1, NULL, NULL, NULL);
	matched = match_regexps (filter->regexps, utf8_name, 0);
	g_free (utf8_name);

	return matched;
}

static gboolean
filter_empty (Filter *filter)
{
	return ((filter->pattern == NULL) || (strcmp (filter->pattern, "*") == 0));
}

/* -- g_directory_foreach_child -- */

typedef struct {
	GFile                *base_directory;
	gboolean              recursive;
	gboolean              follow_links;
	StartDirCallback      start_dir_func;
	ForEachChildCallback  for_each_file_func;
	ForEachDoneCallback   done_func;
	gpointer              user_data;

	/* private */

	GFile                *current;
	GHashTable           *already_visited;
	GList                *to_visit;
	GCancellable         *cancellable;
	GFileEnumerator      *enumerator;
	GError               *error;
	guint                 source_id;
} ForEachChildData;

static void
for_each_child_data_free (ForEachChildData *fec)
{
	if (fec == NULL)
		return;

	if (fec->base_directory != NULL)
		g_object_unref (fec->base_directory);
	if (fec->current != NULL)
		g_object_unref (fec->current);
	if (fec->already_visited)
		g_hash_table_destroy (fec->already_visited);
	if (fec->to_visit != NULL)
		g_list_free (fec->to_visit);
	g_free (fec);
}

static gboolean
for_each_child_done_cb (gpointer user_data)
{
	ForEachChildData *fec = user_data;

	g_source_remove (fec->source_id);
	if (fec->current != NULL) {
		g_object_unref (fec->current);
		fec->current = NULL;
	}
	if (fec->done_func)
		fec->done_func (fec->error, fec->user_data);
	for_each_child_data_free (fec);

	return FALSE;
}

static void
for_each_child_done (ForEachChildData *fec)
{
	fec->source_id = g_idle_add (for_each_child_done_cb, fec);
}

static void for_each_child_start_current (ForEachChildData *fec);

static gboolean
for_each_child_start_cb (gpointer user_data)
{
	ForEachChildData *fec = user_data;

	g_source_remove (fec->source_id);
	for_each_child_start_current (fec);

	return FALSE;
}

static void
for_each_child_start (ForEachChildData *fec)
{
	fec->source_id = g_idle_add (for_each_child_start_cb, fec);
}

static void
for_each_child_set_current_uri (ForEachChildData *fec,
				const char       *directory)
{
	if (fec->current != NULL)
		g_object_unref (fec->current);
	fec->current = g_file_new_for_uri (directory);
}

static void
for_each_child_set_current (ForEachChildData *fec,
			    GFile            *directory)
{
	if (fec->current != NULL)
		g_object_unref (fec->current);
	fec->current = g_file_dup (directory);
}

static void
for_each_child_start_next_sub_directory (ForEachChildData *fec)
{
	char *sub_directory = NULL;

	if (fec->to_visit != NULL) {
		GList *tmp;

		sub_directory = (char*) fec->to_visit->data;
		tmp = fec->to_visit;
		fec->to_visit = g_list_remove_link (fec->to_visit, tmp);
		g_list_free (tmp);
	}

	if (sub_directory != NULL) {
		for_each_child_set_current_uri (fec, sub_directory);
		for_each_child_start (fec);
	}
	else
		for_each_child_done (fec);
}

static void
for_each_child_close_enumerator (GObject      *source_object,
	              		 GAsyncResult *result,
		      		 gpointer      user_data)
{
	ForEachChildData *fec = user_data;
	GError           *error = NULL;

	if (! g_file_enumerator_close_finish (fec->enumerator,
					      result,
					      &error))
	{
		if (fec->error == NULL)
			fec->error = g_error_copy (error);
		else
			g_clear_error (&error);
	}

	if ((fec->error == NULL) && fec->recursive)
		for_each_child_start_next_sub_directory (fec);
	else
		for_each_child_done (fec);
}

static void
for_each_child_next_files_ready (GObject      *source_object,
			         GAsyncResult *result,
			         gpointer      user_data)
{
	ForEachChildData *fec = user_data;
	GList            *children, *scan;

	children = g_file_enumerator_next_files_finish (fec->enumerator,
                                                        result,
                                                        &(fec->error));

	if (children == NULL) {
		g_file_enumerator_close_async (fec->enumerator,
					       G_PRIORITY_DEFAULT,
					       fec->cancellable,
					       for_each_child_close_enumerator,
					       fec);
		return;
	}

	for (scan = children; scan; scan = scan->next) {
		GFileInfo *child_info = scan->data;
		GFile     *f;
		char      *uri;

		f = g_file_get_child (fec->current, g_file_info_get_name (child_info));
		uri = g_file_get_uri (f);

		if (g_file_info_get_file_type (child_info) == G_FILE_TYPE_DIRECTORY) {
			/* avoid to visit a directory more than once */

			if (g_hash_table_lookup (fec->already_visited, uri) == NULL) {
				char *sub_directory;

				sub_directory = g_strdup (uri);
				g_hash_table_insert (fec->already_visited, sub_directory, GINT_TO_POINTER (1));
				fec->to_visit = g_list_append (fec->to_visit, sub_directory);
			}
		}

		fec->for_each_file_func (uri, child_info, fec->user_data);

		g_free (uri);
		g_object_unref (f);
	}

	g_file_enumerator_next_files_async (fec->enumerator,
                                            N_FILES_PER_REQUEST,
                                            G_PRIORITY_DEFAULT,
                                            fec->cancellable,
                                            for_each_child_next_files_ready,
                                            fec);
}

static void
for_each_child_ready (GObject      *source_object,
		      GAsyncResult *result,
		      gpointer      user_data)
{
	ForEachChildData *fec = user_data;

	fec->enumerator = g_file_enumerate_children_finish (fec->current, result, &(fec->error));
	if (fec->enumerator == NULL) {
		for_each_child_done (fec);
		return;
	}

	g_file_enumerator_next_files_async (fec->enumerator,
                                            N_FILES_PER_REQUEST,
                                            G_PRIORITY_DEFAULT,
                                            fec->cancellable,
                                            for_each_child_next_files_ready,
                                            fec);
}

static void
for_each_child_start_current (ForEachChildData *fec)
{
	if (fec->start_dir_func != NULL) {
		char  *directory;
		DirOp  op;

		directory = g_file_get_uri (fec->current);
		op = fec->start_dir_func (directory, &(fec->error), fec->user_data);
		g_free (directory);

		switch (op) {
		case DIR_OP_SKIP:
			for_each_child_start_next_sub_directory (fec);
			return;
		case DIR_OP_STOP:
			for_each_child_done (fec);
			return;
		case DIR_OP_CONTINUE:
			break;
		}
	}

	g_file_enumerate_children_async (fec->current,
					 "standard::name,standard::type",
					 fec->follow_links ? G_FILE_QUERY_INFO_NONE : G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
					 G_PRIORITY_DEFAULT,
                                         fec->cancellable,
					 for_each_child_ready,
					 fec);
}

/**
 * g_directory_foreach_child:
 * @directory: The directory to visit.
 * @recursive: Whether to traverse the @directory recursively.
 * @follow_links: Whether to dereference the symbolic links.
 * @cancellable: An optional @GCancellable object, used to cancel the process.
 * @start_dir_func: the function called for each sub-directory, or %NULL if
 *   not needed.
 * @for_each_file_func: the function called for each file.  Can't be %NULL.
 * @done_func: the function called at the end of the traversing process.
 *   Can't be %NULL.
 * @user_data: data to pass to @done_func
 *
 * Traverse the @directory's filesystem structure calling the
 * @for_each_file_func function for each file in the directory; the
 * @start_dir_func function on each directory before it's going to be
 * traversed, this includes @directory too; the @done_func function is
 * called at the end of the process.
 * Some traversing options are available: if @recursive is TRUE the
 * directory is traversed recursively; if @follow_links is TRUE, symbolic
 * links are dereferenced, otherwise they are returned as links.
 * Each callback uses the same @user_data additional parameter.
 */
void
g_directory_foreach_child (GFile                *directory,
			   gboolean              recursive,
			   gboolean              follow_links,
			   GCancellable         *cancellable,
			   StartDirCallback      start_dir_func,
			   ForEachChildCallback  for_each_file_func,
			   ForEachDoneCallback   done_func,
			   gpointer              user_data)
{
	ForEachChildData *fec;

	g_return_if_fail (for_each_file_func != NULL);

	fec = g_new0 (ForEachChildData, 1);

	fec->base_directory = g_object_ref (directory);
	fec->recursive = recursive;
	fec->follow_links = follow_links;
	fec->cancellable = cancellable;
	fec->start_dir_func = start_dir_func;
	fec->for_each_file_func = for_each_file_func;
	fec->done_func = done_func;
	fec->user_data = user_data;
	fec->already_visited = g_hash_table_new_full (g_str_hash,
						      g_str_equal,
						      g_free,
						      NULL);

	for_each_child_set_current (fec, fec->base_directory);
	for_each_child_start_current (fec);
}

/* -- get_file_list_data -- */

typedef struct {
	GList             *files;
	GList             *dirs;
	GFile             *directory;
	GFile             *base_dir;
	GCancellable      *cancellable;
	ListReadyCallback  done_func;
	gpointer           done_data;
	GList             *to_visit;
	GList             *current_dir;
	Filter            *include_filter;
	Filter            *exclude_filter;
	Filter            *exclude_folders_filter;
	guint              visit_timeout;
} GetFileListData;

static void
get_file_list_data_free (GetFileListData *gfl)
{
	if (gfl == NULL)
		return;

	filter_destroy (gfl->include_filter);
	filter_destroy (gfl->exclude_filter);
	filter_destroy (gfl->exclude_folders_filter);
	path_list_free (gfl->files);
	path_list_free (gfl->dirs);
	path_list_free (gfl->to_visit);
	if (gfl->directory != NULL)
		g_object_unref (gfl->directory);
	if (gfl->base_dir != NULL)
		g_object_unref (gfl->base_dir);
	g_free (gfl);
}

/* -- g_directory_list_async -- */

static GList*
get_relative_file_list (GList *rel_list,
			GList *file_list,
			GFile *base_dir)
{
	GList *scan;

	if (base_dir == NULL)
		return NULL;

	for (scan = file_list; scan; scan = scan->next) {
		char  *full_path = scan->data;
		GFile *f;
		char  *relative_path;

		f = g_file_new_for_commandline_arg (full_path);
		relative_path = g_file_get_relative_path (base_dir, f);
		if (relative_path != NULL)
			rel_list = g_list_prepend (rel_list, relative_path);
		g_object_unref (f);
	}

	return rel_list;
}

static GList*
get_dir_list_from_file_list (GHashTable *h_dirs,
			     const char *base_dir,
			     GList      *files,
			     gboolean    is_dir_list)
{
	GList *scan;
	GList *dir_list = NULL;
	size_t base_dir_len;

	if (base_dir == NULL)
		base_dir = "";
	base_dir_len = strlen (base_dir);

	for (scan = files; scan; scan = scan->next) {
		char *filename = scan->data;
		char *dir_name;

		if (strlen (filename) <= base_dir_len)
			continue;

		if (is_dir_list)
			dir_name = g_strdup (filename + base_dir_len + 1);
		else
			dir_name = remove_level_from_path (filename + base_dir_len + 1);

		while ((dir_name != NULL) && (dir_name[0] != '\0') && (strcmp (dir_name, "/") != 0)) {
			char *tmp;
			char *dir;

			/* avoid to insert duplicated folders */

			dir = g_strconcat (base_dir, "/", dir_name, NULL);
			if (g_hash_table_lookup (h_dirs, dir) == NULL) {
				g_hash_table_insert (h_dirs, dir, GINT_TO_POINTER (1));
				dir_list = g_list_prepend (dir_list, dir);
			}
			else
				g_free (dir);

			tmp = dir_name;
			dir_name = remove_level_from_path (tmp);
			g_free (tmp);
		}

		g_free (dir_name);
	}

	return dir_list;
}

static void
get_file_list_done (GError   *error,
		    gpointer  user_data)
{
	GetFileListData *gfl = user_data;
	GHashTable      *h_dirs;
	GList           *scan;
	char            *uri;

	gfl->files = g_list_reverse (gfl->files);
	gfl->dirs = g_list_reverse (gfl->dirs);

	if (! filter_empty (gfl->include_filter) || (gfl->exclude_filter->pattern != NULL)) {
		path_list_free (gfl->dirs);
		gfl->dirs = NULL;
	}

	h_dirs = g_hash_table_new (g_str_hash, g_str_equal);

	/* Always include the base directory, this way empty base
 	 * directories are added to the archive as well.  */

	if (gfl->base_dir != NULL) {
		char *dir;

		dir = g_file_get_uri (gfl->base_dir);
		gfl->dirs = g_list_prepend (gfl->dirs, dir);
		g_hash_table_insert (h_dirs, dir, GINT_TO_POINTER (1));
	}

	/* Add all the parent directories in gfl->files/gfl->dirs to the
	 * gfl->dirs list, the hash table is used to avoid duplicated
	 * entries. */

	for (scan = gfl->dirs; scan; scan = scan->next)
		g_hash_table_insert (h_dirs, (char*)scan->data, GINT_TO_POINTER (1));

	uri = g_file_get_uri (gfl->base_dir);
	gfl->dirs = g_list_concat (gfl->dirs, get_dir_list_from_file_list (h_dirs, uri, gfl->files, FALSE));

	if (filter_empty (gfl->include_filter))
		gfl->dirs = g_list_concat (gfl->dirs, get_dir_list_from_file_list (h_dirs, uri, gfl->dirs, TRUE));

	g_free (uri);
	/**/

	if (error == NULL) {
		GList *rel_files, *rel_dirs;

		if (gfl->base_dir != NULL) {
			rel_files = get_relative_file_list (NULL, gfl->files, gfl->base_dir);
			rel_dirs = get_relative_file_list (NULL, gfl->dirs, gfl->base_dir);
		}
		else {
			rel_files = gfl->files;
			rel_dirs = gfl->dirs;
			gfl->files = NULL;
			gfl->dirs = NULL;
		}

		/* rel_files/rel_dirs must be deallocated in done_func */
		gfl->done_func (rel_files, rel_dirs, NULL, gfl->done_data);
	}
	else
		gfl->done_func (NULL, NULL, error, gfl->done_data);

	g_hash_table_destroy (h_dirs);
	get_file_list_data_free (gfl);
}

static void
get_file_list_for_each_file (const char *uri,
			     GFileInfo  *info,
			     gpointer    user_data)
{
	GetFileListData *gfl = user_data;

	switch (g_file_info_get_file_type (info)) {
	case G_FILE_TYPE_REGULAR:
		if (filter_matches (gfl->include_filter, uri))
			if ((gfl->exclude_filter->pattern == NULL) || ! filter_matches (gfl->exclude_filter, uri))
				gfl->files = g_list_prepend (gfl->files, g_strdup (uri));
		break;
	default:
		break;
	}
}

static DirOp
get_file_list_start_dir (const char  *uri,
			 GError     **error,
			 gpointer     user_data)
{
	GetFileListData *gfl = user_data;

	if ((gfl->exclude_folders_filter->pattern == NULL) || ! filter_matches (gfl->exclude_folders_filter, uri)) {
		gfl->dirs = g_list_prepend (gfl->dirs, g_strdup (uri));
		return DIR_OP_CONTINUE;
	}
	else
		return DIR_OP_SKIP;
}

void
g_directory_list_async (const char        *directory,
		        const char        *base_dir,
		        gboolean           recursive,
		        gboolean           follow_links,
		        gboolean           no_backup_files,
		        gboolean           no_dot_files,
		        const char        *include_files,
		        const char        *exclude_files,
		        const char        *exclude_folders,
		        gboolean           ignorecase,
		        GCancellable      *cancellable,
		        ListReadyCallback  done_func,
		        gpointer           done_data)
{
	GetFileListData *gfl;
	FilterOptions    filter_options;

	gfl = g_new0 (GetFileListData, 1);
	gfl->directory = g_file_new_for_commandline_arg (directory);
	gfl->base_dir = g_file_new_for_commandline_arg (base_dir);
	gfl->done_func = done_func;
	gfl->done_data = done_data;

	filter_options = FILTER_DEFAULT;
	if (no_backup_files)
		filter_options |= FILTER_NOBACKUPFILES;
	if (no_dot_files)
		filter_options |= FILTER_NODOTFILES;
	if (ignorecase)
		filter_options |= FILTER_IGNORECASE;
	gfl->include_filter = filter_new (include_files, filter_options);
	gfl->exclude_filter = filter_new (exclude_files, ignorecase ? FILTER_IGNORECASE : FILTER_DEFAULT);
	gfl->exclude_folders_filter = filter_new (exclude_folders, ignorecase ? FILTER_IGNORECASE : FILTER_DEFAULT);

	g_directory_foreach_child (gfl->directory,
				   recursive,
				   follow_links,
				   cancellable,
				   get_file_list_start_dir,
				   get_file_list_for_each_file,
				   get_file_list_done,
				   gfl);
}

/* -- g_list_items_async -- */

static void get_items_for_current_dir (GetFileListData *gfl);

static gboolean
get_items_for_next_dir_idle_cb (gpointer data)
{
	GetFileListData *gfl = data;

	g_source_remove (gfl->visit_timeout);
	gfl->visit_timeout = 0;

	gfl->current_dir = g_list_next (gfl->current_dir);
	get_items_for_current_dir (gfl);

	return FALSE;
}

static void
get_items_for_current_dir_done (GList    *files,
			        GList    *dirs,
			        GError   *error,
			        gpointer  data)
{
	GetFileListData *gfl = data;

	if (error != NULL) {
		if (gfl->done_func)
			gfl->done_func (NULL, NULL, error, gfl->done_data);
		path_list_free (files);
		path_list_free (dirs);
		get_file_list_data_free (gfl);
		return;
	}

	gfl->files = g_list_concat (gfl->files, files);
	gfl->dirs = g_list_concat (gfl->dirs, dirs);

	gfl->visit_timeout = g_idle_add (get_items_for_next_dir_idle_cb, gfl);
}

static void
get_items_for_current_dir (GetFileListData *gfl)
{
	GFile *current_dir;
	char  *directory_name;
	GFile *directory_file;
	char  *directory_uri;
	char  *base_dir_uri;

	if (gfl->current_dir == NULL) {
		if (gfl->done_func) {
			/* gfl->files/gfl->dirs must be deallocated in gfl->done_func */
			gfl->done_func (gfl->files, gfl->dirs, NULL, gfl->done_data);
			gfl->files = NULL;
			gfl->dirs = NULL;
		}
		get_file_list_data_free (gfl);
		return;
	}

	current_dir = g_file_new_for_uri ((char*) gfl->current_dir->data);
	directory_name = g_file_get_basename (current_dir);
	directory_file = g_file_get_child (gfl->base_dir, directory_name);
	directory_uri = g_file_get_uri (directory_file);
	base_dir_uri = g_file_get_uri (gfl->base_dir);

	g_directory_list_all_async (directory_uri,
				    base_dir_uri,
				    TRUE,
				    gfl->cancellable,
				    get_items_for_current_dir_done,
				    gfl);

	g_free (base_dir_uri);
	g_free (directory_uri);
	g_object_unref (directory_file);
	g_free (directory_name);
	g_object_unref (current_dir);
}

void
g_list_items_async (GList             *items,
		    const char        *base_dir,
		    GCancellable      *cancellable,
		    ListReadyCallback  done_func,
		    gpointer           done_data)
{
	GetFileListData *gfl;
	int              base_len;
	GList           *scan;

	g_return_if_fail (base_dir != NULL);

	gfl = g_new0 (GetFileListData, 1);
	gfl->base_dir = g_file_new_for_commandline_arg (base_dir);
	gfl->cancellable = cancellable;
	gfl->done_func = done_func;
	gfl->done_data = done_data;

	base_len = 0;
	if (strcmp (base_dir, "/") != 0)
		base_len = strlen (base_dir);

	for (scan = items; scan; scan = scan->next) {
		char *uri = scan->data;

		/* FIXME: this is not async */
		if (uri_is_dir (uri)) {
			gfl->to_visit = g_list_prepend (gfl->to_visit, g_strdup (uri));
		}
		else {
			char *rel_path = g_uri_unescape_string (uri + base_len + 1, NULL);
			gfl->files = g_list_prepend (gfl->files, rel_path);
		}
	}

	gfl->current_dir = gfl->to_visit;
	get_items_for_current_dir (gfl);
}

/* -- g_copy_files_async -- */

typedef struct {
	GList                 *sources;
	GList                 *destinations;
	GFileCopyFlags         flags;
	int                    io_priority;
	GCancellable          *cancellable;
	CopyProgressCallback   progress_callback;
	gpointer               progress_callback_data;
	CopyDoneCallback       callback;
	gpointer               user_data;

	GList                 *source;
	GList                 *destination;
	int                    n_file;
	int                    tot_files;
} CopyFilesData;

static CopyFilesData*
copy_files_data_new (GList                 *sources,
		     GList                 *destinations,
		     GFileCopyFlags         flags,
		     int                    io_priority,
		     GCancellable          *cancellable,
		     CopyProgressCallback   progress_callback,
		     gpointer               progress_callback_data,
		     CopyDoneCallback       callback,
		     gpointer               user_data)
{
	CopyFilesData *cfd;

	cfd = g_new0 (CopyFilesData, 1);
	cfd->sources = gio_file_list_dup (sources);
	cfd->destinations = gio_file_list_dup (destinations);
	cfd->flags = flags;
	cfd->io_priority = io_priority;
	cfd->cancellable = cancellable;
	cfd->progress_callback = progress_callback;
	cfd->progress_callback_data = progress_callback_data;
	cfd->callback = callback;
	cfd->user_data = user_data;

	cfd->source = cfd->sources;
	cfd->destination = cfd->destinations;
	cfd->n_file = 1;
	cfd->tot_files = g_list_length (cfd->sources);

	return cfd;
}

static void
copy_files_data_free (CopyFilesData *cfd)
{
	if (cfd == NULL)
		return;
	gio_file_list_free (cfd->sources);
	gio_file_list_free (cfd->destinations);
	g_free (cfd);
}

static void g_copy_current_file (CopyFilesData *cfd);

static void
g_copy_next_file (CopyFilesData *cfd)
{
	cfd->source = g_list_next (cfd->source);
	cfd->destination = g_list_next (cfd->destination);
	cfd->n_file++;

	g_copy_current_file (cfd);
}

static void
g_copy_files_ready_cb (GObject      *source_object,
                       GAsyncResult *result,
                       gpointer      user_data)
{
	CopyFilesData *cfd = user_data;
	GFile         *source = cfd->source->data;
	GError        *error = NULL;

	if (! g_file_copy_finish (source, result, &error)) {
		/* source and target are directories, ignore the error */
		if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_WOULD_MERGE))
			g_clear_error (&error);
		/* source is directory, create target directory */
		if (g_error_matches (error, G_IO_ERROR,  G_IO_ERROR_WOULD_RECURSE)) {
			g_clear_error (&error);
			g_file_make_directory ((GFile*) cfd->destination->data,
					       cfd->cancellable,
					       &error);
		}
	}

	if (error) {
		if (cfd->callback)
			cfd->callback (error, cfd->user_data);
		g_clear_error (&error);
		copy_files_data_free (cfd);
		return;
	}

	g_copy_next_file (cfd);
}

static void
g_copy_files_progress_cb (goffset  current_num_bytes,
                          goffset  total_num_bytes,
                          gpointer user_data)
{
	CopyFilesData *cfd = user_data;

	if (cfd->progress_callback)
		cfd->progress_callback (cfd->n_file,
					cfd->tot_files,
					(GFile*) cfd->source->data,
					(GFile*) cfd->destination->data,
					current_num_bytes,
					total_num_bytes,
					cfd->progress_callback_data);
}

static void
g_copy_current_file (CopyFilesData *cfd)
{
	if ((cfd->source == NULL) || (cfd->destination == NULL)) {
		if (cfd->callback)
			cfd->callback (NULL, cfd->user_data);
		copy_files_data_free (cfd);
		return;
	}

	g_file_copy_async ((GFile*) cfd->source->data,
			   (GFile*) cfd->destination->data,
			   cfd->flags,
			   cfd->io_priority,
			   cfd->cancellable,
			   g_copy_files_progress_cb,
			   cfd,
			   g_copy_files_ready_cb,
			   cfd);
}

void
g_copy_files_async (GList                 *sources,
		    GList                 *destinations,
		    GFileCopyFlags         flags,
		    int                    io_priority,
		    GCancellable          *cancellable,
		    CopyProgressCallback   progress_callback,
		    gpointer               progress_callback_data,
		    CopyDoneCallback       callback,
		    gpointer               user_data)
{
	CopyFilesData *cfd;

	cfd = copy_files_data_new (sources,
				   destinations,
				   flags,
				   io_priority,
				   cancellable,
				   progress_callback,
				   progress_callback_data,
				   callback,
				   user_data);
	g_copy_current_file (cfd);
}

void
g_copy_file_async (GFile                 *source,
		   GFile                 *destination,
		   GFileCopyFlags         flags,
		   int                    io_priority,
		   GCancellable          *cancellable,
		   CopyProgressCallback   progress_callback,
		   gpointer               progress_callback_data,
		   CopyDoneCallback       callback,
		   gpointer               user_data)
{
	GList *source_files;
	GList *destination_files;

	source_files = g_list_append (NULL, (gpointer) source);
	destination_files = g_list_append (NULL, (gpointer) destination);

	g_copy_files_async (source_files,
			    destination_files,
			    flags,
			    io_priority,
			    cancellable,
			    progress_callback,
			    progress_callback_data,
			    callback,
			    user_data);

	g_list_free (source_files);
	g_list_free (destination_files);
}

/* -- g_directory_copy_async -- */

typedef struct {
	char      *uri;
	GFileInfo *info;
} ChildData;

static ChildData*
child_data_new (const char *uri,
		GFileInfo  *info)
{
	ChildData *data;

	data = g_new0 (ChildData, 1);
	data->uri = g_strdup (uri);
	data->info = g_file_info_dup (info);

	return data;
}

static void
child_data_free (ChildData *child)
{
	if (child == NULL)
		return;
	g_free (child->uri);
	g_object_unref (child->info);
	g_free (child);
}

typedef struct {
	GFile                 *source;
	GFile                 *destination;
	GFileCopyFlags         flags;
	int                    io_priority;
	GCancellable          *cancellable;
	CopyProgressCallback   progress_callback;
	gpointer               progress_callback_data;
	CopyDoneCallback       callback;
	gpointer               user_data;
	GError                *error;

	GList                 *to_copy;
	GList                 *current;
	GFile                 *current_source;
	GFile                 *current_destination;
	int                    n_file, tot_files;
	guint                  source_id;
} DirectoryCopyData;

static void
directory_copy_data_free (DirectoryCopyData *dcd)
{
	if (dcd == NULL)
		return;

	if (dcd->source != NULL)
		g_object_unref (dcd->source);
	if (dcd->destination != NULL)
		g_object_unref (dcd->destination);
	if (dcd->current_source != NULL) {
		g_object_unref (dcd->current_source);
		dcd->current_source = NULL;
	}
	if (dcd->current_destination != NULL) {
		g_object_unref (dcd->current_destination);
		dcd->current_destination = NULL;
	}
	g_list_free_full (dcd->to_copy, (GDestroyNotify) child_data_free);
	g_free (dcd);
}

static gboolean
g_directory_copy_done (gpointer user_data)
{
	DirectoryCopyData *dcd = user_data;

	g_source_remove (dcd->source_id);

	if (dcd->callback)
		dcd->callback (dcd->error, dcd->user_data);
	if (dcd->error != NULL)
		g_clear_error (&(dcd->error));
	directory_copy_data_free (dcd);

	return FALSE;
}

static GFile *
get_destination_for_uri (DirectoryCopyData *dcd,
		         const char        *uri)
{
	GFile *f_uri;
	GFile *destination_file;
	char  *relative_path;

	f_uri = g_file_new_for_uri (uri);
	relative_path = g_file_get_relative_path (dcd->source, f_uri);
	if (relative_path != NULL)
		destination_file = g_file_resolve_relative_path (dcd->destination, relative_path);
	else
		destination_file = g_file_dup (dcd->destination);

	g_free (relative_path);
	g_object_unref (f_uri);

	return destination_file;
}

static void g_directory_copy_current_child (DirectoryCopyData *dcd);

static gboolean
g_directory_copy_next_child (gpointer user_data)
{
	DirectoryCopyData *dcd = user_data;

	g_source_remove (dcd->source_id);

	dcd->current = g_list_next (dcd->current);
	dcd->n_file++;
	g_directory_copy_current_child (dcd);

	return FALSE;
}

static void
g_directory_copy_child_done_cb (GObject      *source_object,
                        	GAsyncResult *result,
 	                        gpointer      user_data)
{
	DirectoryCopyData *dcd = user_data;

	if (! g_file_copy_finish ((GFile*)source_object, result, &(dcd->error))) {
		dcd->source_id = g_idle_add (g_directory_copy_done, dcd);
		return;
	}

	dcd->source_id = g_idle_add (g_directory_copy_next_child, dcd);
}

static void
g_directory_copy_child_progress_cb (goffset  current_num_bytes,
                                    goffset  total_num_bytes,
                                    gpointer user_data)
{
	DirectoryCopyData *dcd = user_data;

	if (dcd->progress_callback)
		dcd->progress_callback (dcd->n_file,
					dcd->tot_files,
					dcd->current_source,
					dcd->current_destination,
					current_num_bytes,
					total_num_bytes,
					dcd->progress_callback_data);
}

static void
g_directory_copy_current_child (DirectoryCopyData *dcd)
{
	ChildData *child;
	gboolean   async_op = FALSE;

	if (dcd->current == NULL) {
		dcd->source_id = g_idle_add (g_directory_copy_done, dcd);
		return;
	}

	if (dcd->current_source != NULL) {
		g_object_unref (dcd->current_source);
		dcd->current_source = NULL;
	}
	if (dcd->current_destination != NULL) {
		g_object_unref (dcd->current_destination);
		dcd->current_destination = NULL;
	}

	child = dcd->current->data;
	dcd->current_source = g_file_new_for_uri (child->uri);
	dcd->current_destination = get_destination_for_uri (dcd, child->uri);
	if (dcd->current_destination == NULL) {
		dcd->source_id = g_idle_add (g_directory_copy_next_child, dcd);
		return;
	}

	switch (g_file_info_get_file_type (child->info)) {
	case G_FILE_TYPE_DIRECTORY:
		/* FIXME: how to make a directory asynchronously ? */

		/* doesn't check the returned error for now, because when an
		 * error occurs the code is not returned (for example when
		 * a directory already exists the G_IO_ERROR_EXISTS code is
		 * *not* returned), so we cannot discriminate between warnings
		 * and fatal errors. (see bug #525155) */

		g_file_make_directory (dcd->current_destination,
				       NULL,
				       NULL);

		/*if (! g_file_make_directory (dcd->current_destination,
					     dcd->cancellable,
					     &(dcd->error)))
		{
			dcd->source_id = g_idle_add (g_directory_copy_done, dcd);
			return;
		}*/
		break;
	case G_FILE_TYPE_SYMBOLIC_LINK:
		/* FIXME: how to make a link asynchronously ? */

		g_file_make_symbolic_link (dcd->current_destination,
					   g_file_info_get_symlink_target (child->info),
					   NULL,
					   NULL);

		/*if (! g_file_make_symbolic_link (dcd->current_destination,
						 g_file_info_get_symlink_target (child->info),
						 dcd->cancellable,
						 &(dcd->error)))
		{
			dcd->source_id = g_idle_add (g_directory_copy_done, dcd);
			return;
		}*/
		break;
	case G_FILE_TYPE_REGULAR:
		g_file_copy_async (dcd->current_source,
				   dcd->current_destination,
				   dcd->flags,
				   dcd->io_priority,
				   dcd->cancellable,
				   g_directory_copy_child_progress_cb,
				   dcd,
				   g_directory_copy_child_done_cb,
				   dcd);
		async_op = TRUE;
		break;
	default:
		break;
	}

	if (! async_op)
		dcd->source_id = g_idle_add (g_directory_copy_next_child, dcd);
}

static gboolean
g_directory_copy_start_copying (gpointer user_data)
{
	DirectoryCopyData *dcd = user_data;

	g_source_remove (dcd->source_id);

	dcd->to_copy = g_list_reverse (dcd->to_copy);
	dcd->current = dcd->to_copy;
	dcd->n_file = 1;
	g_directory_copy_current_child (dcd);

	return FALSE;
}

static void
g_directory_copy_list_ready (GError   *error,
			     gpointer  user_data)
{
	DirectoryCopyData *dcd = user_data;

	if (error != NULL) {
		dcd->error = g_error_copy (error);
		dcd->source_id = g_idle_add (g_directory_copy_done, dcd);
		return;
	}

	dcd->source_id = g_idle_add (g_directory_copy_start_copying, dcd);
}

static void
g_directory_copy_for_each_file (const char *uri,
				GFileInfo  *info,
				gpointer    user_data)
{
	DirectoryCopyData *dcd = user_data;

	dcd->to_copy = g_list_prepend (dcd->to_copy, child_data_new (uri, info));
	dcd->tot_files++;
}

static DirOp
g_directory_copy_start_dir (const char  *uri,
			    GError     **error,
			    gpointer     user_data)
{
	DirectoryCopyData *dcd = user_data;
	GFileInfo         *info;

	info = g_file_info_new ();
	g_file_info_set_file_type (info, G_FILE_TYPE_DIRECTORY);
	dcd->to_copy = g_list_prepend (dcd->to_copy, child_data_new (uri, info));
	g_object_unref (info);

	dcd->tot_files++;

	return DIR_OP_CONTINUE;
}

void
g_directory_copy_async (const char            *source,
			const char            *destination,
			GFileCopyFlags         flags,
			int                    io_priority,
			GCancellable          *cancellable,
			CopyProgressCallback   progress_callback,
			gpointer               progress_callback_data,
			CopyDoneCallback       callback,
			gpointer               user_data)
{
	DirectoryCopyData *dcd;

	/* Creating GFile objects here will save us lot of effort in path construction */
	dcd = g_new0 (DirectoryCopyData, 1);
	dcd->source = g_file_new_for_commandline_arg (source);
	dcd->destination = g_file_new_for_commandline_arg (destination);
	dcd->flags = flags;
	dcd->io_priority = io_priority;
	dcd->cancellable = cancellable;
	dcd->progress_callback = progress_callback;
	dcd->progress_callback_data = progress_callback_data;
	dcd->callback = callback;
	dcd->user_data = user_data;

	g_directory_foreach_child (dcd->source,
			           TRUE,
			           TRUE,
			           dcd->cancellable,
			           g_directory_copy_start_dir,
			           g_directory_copy_for_each_file,
			           g_directory_copy_list_ready,
			           dcd);
}

gboolean
g_load_file_in_buffer (GFile   *file,
	               void    *buffer,
	               gsize    size,
                       GError **error)
{
	GFileInputStream *istream;
	int               n;

	istream = g_file_read (file, NULL, error);
	if (istream == NULL)
		return FALSE;

	n = g_input_stream_read (G_INPUT_STREAM (istream), buffer, size, NULL, error);
	g_object_unref (istream);

	return (n >= 0);
}