/*
 * Copyright © 2010 Codethink Limited
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the licence, or (at your option) any later version.
 *
 * This library 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 library; if not, see <http://www.gnu.org/licenses/>.
 *
 * Author: Ryan Lortie <desrt@desrt.ca>
 */

unowned Gvdb.Item get_parent (Gvdb.HashTable table, string name) {
	unowned Gvdb.Item parent;

	int end = 0;

	for (int i = 1; name[i] != '\0'; i++) {
		if (name[i - 1] == '/') {
			end = i;
		}
	}

	assert (end != 0);

	var parent_name = name.substring (0, end);
	parent = table.lookup (parent_name);

	if (parent == null) {
		parent = table.insert (parent_name);
		parent.set_parent (get_parent (table, parent_name));
	}

	return parent;
}

SList<string>? list_directory (string dirname, Posix.mode_t mode) throws GLib.Error {
	var list = new SList<string> ();
	var dir = Dir.open (dirname);
	unowned string? name;

	while ((name = dir.read_name ()) != null) {
		if (name.has_prefix (".")) {
			continue;
		}

		var filename = Path.build_filename (dirname, name);
		Posix.Stat buf;

		// only files of the requested type
		if (Posix.stat (filename, out buf) < 0 || (buf.st_mode & Posix.S_IFMT) != mode) {
			continue;
		}

		list.prepend (filename);
	}

	return list;
}

Gvdb.HashTable? read_locks_directory (string dirname) throws GLib.Error {
	SList<string>? files;

	try {
		files = list_directory (dirname, Posix.S_IFREG);
	} catch (FileError.NOENT e) {
		/* If locks directory is missing, there are just no locks... */
		return null;
	}

	var table = new Gvdb.HashTable ();

	foreach (var filename in files) {
		string contents;
		FileUtils.get_contents (filename, out contents, null);

		foreach (var line in contents.split ("\n")) {
			if (line.has_prefix ("/")) {
				table.insert_string (line, "");
			}
		}
	}

	return table;
}

Gvdb.HashTable read_directory (string dirname) throws GLib.Error {
	var table = new Gvdb.HashTable ();
	table.insert ("/");

	var files = list_directory (dirname, Posix.S_IFREG);
	files.sort (strcmp);
	files.reverse ();

	foreach (var filename in files) {
		var kf = new KeyFile ();

		try {
			kf.load_from_file (filename, KeyFileFlags.NONE);
		} catch (GLib.Error e) {
			e.message = "warning: Failed to read keyfile '%s': %s".printf (filename, e.message);
			throw e;
		}

		foreach (var group in kf.get_groups ()) {
			if (group.has_prefix ("/") || group.has_suffix ("/") || "//" in group) {
				throw new KeyFileError.PARSE ("%s: invalid group name: %s".printf (filename, group));
			}

			foreach (var key in kf.get_keys (group)) {
				if ("/" in key) {
					throw new KeyFileError.INVALID_VALUE ("%s: [%s]: invalid key name: %s",
																								filename, group, key);
				}

				var path = "/" + group + "/" + key;

				if (table.lookup (path) != null) {
					/* We process the files in reverse alphabetical order.  If the key is already set then
					 * it must have been set from a file with higher precedence so we should ignore this
					 * one.
					 */
					continue;
				}

				var text = kf.get_value (group, key);

				try {
					var value = Variant.parse (null, text);
					unowned Gvdb.Item item = table.insert (path);
					item.set_parent (get_parent (table, path));
					item.set_value (value);
				} catch (VariantParseError e) {
					e.message = "%s: [%s]: %s: invalid value: %s (%s)".printf (filename, group, key, text, e.message);
					throw e;
				}
			}
		}
	}

	var locks = read_locks_directory (dirname + "/locks");

	if (locks != null) {
		unowned Gvdb.Item item = table.insert (".locks");
		item.set_hash_table (locks);
	}

	return table;
}

time_t get_directory_mtime (string dirname, Posix.Stat dir_buf) throws GLib.Error {
	Posix.Stat lockdir_buf;
	Posix.Stat file_buf;
	time_t latest_mtime = dir_buf.st_mtime;

	var files = list_directory (dirname, Posix.S_IFREG);

	foreach (var filename in files) {
		if (Posix.stat (filename, out file_buf) == 0 && file_buf.st_mtime > latest_mtime)
			latest_mtime = file_buf.st_mtime;
	}

	if (Posix.stat (dirname + "/locks", out lockdir_buf) == 0 && Posix.S_ISDIR (lockdir_buf.st_mode)) {
		if (lockdir_buf.st_mtime > latest_mtime) {
			// if the lock directory has been updated more recently then consider its timestamp instead
			latest_mtime = lockdir_buf.st_mtime;
		}

		files = list_directory (dirname + "/locks", Posix.S_IFREG);

		foreach (var filename in files) {
			if (Posix.stat (filename, out file_buf) == 0 && file_buf.st_mtime > latest_mtime)
				latest_mtime = file_buf.st_mtime;
		}
	}

	return latest_mtime;
}

void maybe_update_from_directory (string dirname) throws GLib.Error {
	Posix.Stat dir_buf;

	if (Posix.stat (dirname, out dir_buf) == 0 && Posix.S_ISDIR (dir_buf.st_mode)) {
		Posix.Stat file_buf;

		var filename = dirname.substring (0, dirname.length - 2);

		if (Posix.stat (filename, out file_buf) == 0 && file_buf.st_mtime > get_directory_mtime (dirname, dir_buf)) {
			return;
		}

		var table = read_directory (dirname);

		var fd = Posix.open (filename, Posix.O_WRONLY);

		if (fd < 0 && errno != Posix.ENOENT) {
			var saved_error = errno;
			printerr ("warning: Failed to open '%s' for replacement: %s\n", filename, strerror (saved_error));
			return;
		}

		// We expect that fd < 0 here if ENOENT (ie: the db merely didn't exist yet)

		try {
			table.write_contents (filename);

			if (fd >= 0) {
				Posix.write (fd, "\0\0\0\0\0\0\0\0", 8);
			}
		} catch (Error e) {
			printerr ("warning: %s\n", e.message);
			return;
		} finally {
			if (fd >= 0) {
				Posix.close (fd);
			}
		}

		try {
			var system_bus = Bus.get_sync (BusType.SYSTEM);
			system_bus.emit_signal (null, "/ca/desrt/dconf/Writer/" + Path.get_basename (filename),
			                        "ca.desrt.dconf.Writer", "WritabilityNotify", new Variant ("(s)", "/"));
			system_bus.flush_sync ();
		} catch {
			/* if we can't, ... don't. */
		}
	}
}

void update_all (string dirname) throws GLib.Error {
	var failed = false;

	foreach (var name in list_directory (dirname, Posix.S_IFDIR)) {
		if (name.has_suffix (".d")) {
			try {
				maybe_update_from_directory (name);
			} catch (Error e) {
				failed = true;
				printerr ("unable to compile %s: %s\n", name, e.message);
			}
		}
	}

	if (failed) {
		Process.exit (2);
	}
}

void dconf_compile (string[] args) throws GLib.Error {
	if (args[2] == null || args[3] == null || args[4] != null) {
			throw new OptionError.FAILED ("must give output file and .d dir");
	}

	try {
		// We always write the result of "dconf compile" as little endian
		// so that it can be installed in /usr/share
		var table = read_directory (args[3]);
		var should_byteswap = (BYTE_ORDER == ByteOrder.BIG_ENDIAN);
		table.write_contents (args[2], should_byteswap);
	} catch (Error e) {
		printerr ("%s\n", e.message);
		Process.exit (1);
	}
}

[CCode (cname = "SYSCONFDIR")]
extern const string CONFIG_SYSCONFDIR;

void dconf_update (string[] args) throws GLib.Error {
	var index = 2;
	var dbpath = CONFIG_SYSCONFDIR + "/dconf/db";

	if (args[index] != null) {
		dbpath = args[index];
		index ++;
	}

	if (args[index] != null) {
		throw new OptionError.FAILED ("too many arguments");
	}

	update_all (dbpath);
}

// vim:noet ts=4 sw=4
