/*
 * Copyright (C) 2003-2011 The Music Player Daemon Project
 * http://www.musicpd.org
 *
 * 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 "config.h" /* must be first for large file support */
#include "inotify_update.h"
#include "inotify_source.h"
#include "inotify_queue.h"
#include "database.h"
#include "mapper.h"
#include "path.h"

#include <assert.h>
#include <sys/inotify.h>
#include <sys/stat.h>
#include <stdbool.h>
#include <string.h>
#include <dirent.h>
#include <errno.h>

#undef G_LOG_DOMAIN
#define G_LOG_DOMAIN "inotify"

enum {
	IN_MASK = IN_ATTRIB|IN_CLOSE_WRITE|IN_CREATE|IN_DELETE|IN_DELETE_SELF
	|IN_MOVE|IN_MOVE_SELF
#ifdef IN_ONLYDIR
	|IN_ONLYDIR
#endif
};

struct watch_directory {
	struct watch_directory *parent;

	char *name;

	int descriptor;

	GList *children;
};

static struct mpd_inotify_source *inotify_source;

static unsigned inotify_max_depth;
static struct watch_directory inotify_root;
static GTree *inotify_directories;

static gint
compare(gconstpointer a, gconstpointer b)
{
	if (a < b)
		return -1;
	else if (a > b)
		return 1;
	else
		return 0;
}

static void
tree_add_watch_directory(struct watch_directory *directory)
{
	g_tree_insert(inotify_directories,
		      GINT_TO_POINTER(directory->descriptor), directory);
}

static void
tree_remove_watch_directory(struct watch_directory *directory)
{
	G_GNUC_UNUSED
	bool found = g_tree_remove(inotify_directories,
				   GINT_TO_POINTER(directory->descriptor));
	assert(found);
}

static struct watch_directory *
tree_find_watch_directory(int wd)
{
	return g_tree_lookup(inotify_directories, GINT_TO_POINTER(wd));
}

static void
remove_watch_directory(struct watch_directory *directory)
{
	assert(directory != NULL);

	if (directory->parent == NULL) {
		g_warning("music directory was removed - "
			  "cannot continue to watch it");
		return;
	}

	assert(directory->parent->children != NULL);

	tree_remove_watch_directory(directory);

	while (directory->children != NULL)
		remove_watch_directory(directory->children->data);

	directory->parent->children =
		g_list_remove(directory->parent->children, directory);

	mpd_inotify_source_rm(inotify_source, directory->descriptor);
	g_free(directory->name);
	g_slice_free(struct watch_directory, directory);
}

static char *
watch_directory_get_uri_fs(const struct watch_directory *directory)
{
	char *parent_uri, *uri;

	if (directory->parent == NULL)
		return NULL;

	parent_uri = watch_directory_get_uri_fs(directory->parent);
	if (parent_uri == NULL)
		return g_strdup(directory->name);

	uri = g_strconcat(parent_uri, "/", directory->name, NULL);
	g_free(parent_uri);

	return uri;
}

/* we don't look at "." / ".." nor files with newlines in their name */
static bool skip_path(const char *path)
{
	return (path[0] == '.' && path[1] == 0) ||
		(path[0] == '.' && path[1] == '.' && path[2] == 0) ||
		strchr(path, '\n') != NULL;
}

static void
recursive_watch_subdirectories(struct watch_directory *directory,
			       const char *path_fs, unsigned depth)
{
	GError *error = NULL;
	DIR *dir;
	struct dirent *ent;

	assert(directory != NULL);
	assert(depth <= inotify_max_depth);
	assert(path_fs != NULL);

	++depth;

	if (depth > inotify_max_depth)
		return;

	dir = opendir(path_fs);
	if (dir == NULL) {
		g_warning("Failed to open directory %s: %s",
			  path_fs, g_strerror(errno));
		return;
	}

	while ((ent = readdir(dir))) {
		char *child_path_fs;
		struct stat st;
		int ret;
		struct watch_directory *child;

		if (skip_path(ent->d_name))
			continue;

		child_path_fs = g_strconcat(path_fs, "/", ent->d_name, NULL);
		ret = stat(child_path_fs, &st);
		if (ret < 0) {
			g_warning("Failed to stat %s: %s",
				  child_path_fs, g_strerror(errno));
			g_free(child_path_fs);
			continue;
		}

		if (!S_ISDIR(st.st_mode)) {
			g_free(child_path_fs);
			continue;
		}

		ret = mpd_inotify_source_add(inotify_source, child_path_fs,
					     IN_MASK, &error);
		if (ret < 0) {
			g_warning("Failed to register %s: %s",
				  child_path_fs, error->message);
			g_error_free(error);
			error = NULL;
			g_free(child_path_fs);
			continue;
		}

		child = tree_find_watch_directory(ret);
		if (child != NULL) {
			/* already being watched */
			g_free(child_path_fs);
			continue;
		}

		child = g_slice_new(struct watch_directory);
		child->parent = directory;
		child->name = g_strdup(ent->d_name);
		child->descriptor = ret;
		child->children = NULL;

		directory->children = g_list_prepend(directory->children,
						     child);

		tree_add_watch_directory(child);

		recursive_watch_subdirectories(child, child_path_fs, depth);
		g_free(child_path_fs);
	}

	closedir(dir);
}

G_GNUC_PURE
static unsigned
watch_directory_depth(const struct watch_directory *d)
{
	assert(d != NULL);

	unsigned depth = 0;
	while ((d = d->parent) != NULL)
		++depth;

	return depth;
}

static void
mpd_inotify_callback(int wd, unsigned mask,
		     G_GNUC_UNUSED const char *name, G_GNUC_UNUSED void *ctx)
{
	struct watch_directory *directory;
	char *uri_fs;

	/*g_debug("wd=%d mask=0x%x name='%s'", wd, mask, name);*/

	directory = tree_find_watch_directory(wd);
	if (directory == NULL)
		return;

	uri_fs = watch_directory_get_uri_fs(directory);

	if ((mask & (IN_DELETE_SELF|IN_MOVE_SELF)) != 0) {
		g_free(uri_fs);
		remove_watch_directory(directory);
		return;
	}

	if ((mask & (IN_ATTRIB|IN_CREATE|IN_MOVE)) != 0 &&
	    (mask & IN_ISDIR) != 0) {
		/* a sub directory was changed: register those in
		   inotify */
		const char *root = mapper_get_music_directory_fs();
		const char *path_fs;
		char *allocated = NULL;

		if (uri_fs != NULL)
			path_fs = allocated =
				g_strconcat(root, "/", uri_fs, NULL);
		else
			path_fs = root;

		recursive_watch_subdirectories(directory, path_fs,
					       watch_directory_depth(directory));
		g_free(allocated);
	}

	if ((mask & (IN_CLOSE_WRITE|IN_MOVE|IN_DELETE)) != 0 ||
	    /* at the maximum depth, we watch out for newly created
	       directories */
	    (watch_directory_depth(directory) == inotify_max_depth &&
	     (mask & (IN_CREATE|IN_ISDIR)) == (IN_CREATE|IN_ISDIR))) {
		/* a file was changed, or a directory was
		   moved/deleted: queue a database update */
		char *uri_utf8 = uri_fs != NULL
			? fs_charset_to_utf8(uri_fs)
			: g_strdup("");

		if (uri_utf8 != NULL)
			/* this function will take care of freeing
			   uri_utf8 */
			mpd_inotify_enqueue(uri_utf8);
	}

	g_free(uri_fs);
}

void
mpd_inotify_init(unsigned max_depth)
{
	GError *error = NULL;

	g_debug("initializing inotify");

	const char *path = mapper_get_music_directory_fs();
	if (path == NULL) {
		g_debug("no music directory configured");
		return;
	}

	inotify_source = mpd_inotify_source_new(mpd_inotify_callback, NULL,
						&error);
	if (inotify_source == NULL) {
		g_warning("%s", error->message);
		g_error_free(error);
		return;
	}

	inotify_max_depth = max_depth;

	inotify_root.name = g_strdup(path);
	inotify_root.descriptor = mpd_inotify_source_add(inotify_source, path,
							 IN_MASK, &error);
	if (inotify_root.descriptor < 0) {
		g_warning("%s", error->message);
		g_error_free(error);
		mpd_inotify_source_free(inotify_source);
		inotify_source = NULL;
		return;
	}

	inotify_directories = g_tree_new(compare);
	tree_add_watch_directory(&inotify_root);

	recursive_watch_subdirectories(&inotify_root, path, 0);

	mpd_inotify_queue_init();

	g_debug("watching music directory");
}

static gboolean
free_watch_directory(G_GNUC_UNUSED gpointer key, gpointer value,
		     G_GNUC_UNUSED gpointer data)
{
	struct watch_directory *directory = value;

	g_free(directory->name);
	g_list_free(directory->children);

	if (directory != &inotify_root)
		g_slice_free(struct watch_directory, directory);

	return false;
}

void
mpd_inotify_finish(void)
{
	if (inotify_source == NULL)
		return;

	mpd_inotify_queue_finish();
	mpd_inotify_source_free(inotify_source);

	g_tree_foreach(inotify_directories, free_watch_directory, NULL);
	g_tree_destroy(inotify_directories);
}
