/* -*- mode: c; style: linux -*- */

/* archive.c
 * Copyright (C) 2000-2001 Ximian, Inc.
 *
 * Written by Bradford Hovinen (hovinen@ximian.com)
 *
 * 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, 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., 59 Temple Place - Suite 330, Boston, MA
 * 02111-1307, USA.
 */

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

#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>
#include <errno.h>

#include "archive.h"
#include "util.h"

static GtkObjectClass *parent_class;

enum {
	ARG_0,
	ARG_PREFIX,
	ARG_IS_GLOBAL
};

#define ARCHIVE_FROM_SERVANT(servant) (ARCHIVE (bonobo_object_from_servant (servant)))

static void archive_init          (Archive *archive);
static void archive_class_init    (ArchiveClass *klass);

static void archive_destroy       (GtkObject *object);

static void archive_set_arg       (GtkObject *object,
				   GtkArg *arg,
				   guint arg_id);

static void archive_get_arg       (GtkObject *object,
				   GtkArg *arg,
				   guint arg_id);

static void load_all_locations    (Archive *archive);

/* CORBA interface methods */

static ConfigArchiver_Location
impl_ConfigArchiver_Archive_getLocation (PortableServer_Servant  servant,
					 const CORBA_char       *locid,
					 CORBA_Environment      *ev) 
{
	Location *loc;

	loc = archive_get_location (ARCHIVE_FROM_SERVANT (servant), locid);

	if (loc == NULL) {
		bonobo_exception_set (ev, ex_ConfigArchiver_Archive_LocationNotFound);
		return CORBA_OBJECT_NIL;
	} else {
		return CORBA_Object_duplicate (BONOBO_OBJREF (loc), ev);
	}
}

static ConfigArchiver_Location
impl_ConfigArchiver_Archive_createLocation (PortableServer_Servant         servant,
					    const CORBA_char              *locid,
					    const CORBA_char              *label,
					    const ConfigArchiver_Location  parent_ref,
					    CORBA_Environment             *ev) 
{
	Location *loc;

	loc = archive_create_location (ARCHIVE_FROM_SERVANT (servant), locid, label,
				       LOCATION (bonobo_object_from_servant (parent_ref->servant)));

	return CORBA_Object_duplicate (BONOBO_OBJREF (loc), ev);
}

static ConfigArchiver_LocationSeq *
impl_ConfigArchiver_Archive_getChildLocations (PortableServer_Servant   servant,
					       ConfigArchiver_Location  location_ref,
					       CORBA_Environment       *ev) 
{
	ConfigArchiver_LocationSeq *ret;
	Archive *archive;
	Location *location;
	GList *locs, *tmp;
	guint i = 0;

	archive = ARCHIVE_FROM_SERVANT (servant);

	if (location_ref == CORBA_OBJECT_NIL)
		location = NULL;
	else
		location = LOCATION (bonobo_object_from_servant (location_ref->servant));

	locs = archive_get_child_locations (archive, location);

	ret = ConfigArchiver_LocationSeq__alloc ();
	ret->_length = g_list_length (locs);
	ret->_buffer = CORBA_sequence_ConfigArchiver_Location_allocbuf (ret->_length);

	for (tmp = locs; tmp != NULL; tmp = tmp->next)
		ret->_buffer[i++] = CORBA_Object_duplicate (BONOBO_OBJREF (tmp->data), ev);

	g_list_free (locs);
	return ret;
}

static CORBA_char *
impl_ConfigArchiver_Archive__get_prefix (PortableServer_Servant  servant,
					 CORBA_Environment      *ev) 
{
	return CORBA_string_dup (ARCHIVE_FROM_SERVANT (servant)->prefix);
}

static CORBA_boolean
impl_ConfigArchiver_Archive__get_isGlobal (PortableServer_Servant  servant,
					   CORBA_Environment      *ev) 
{
	return ARCHIVE_FROM_SERVANT (servant)->is_global;
}

static ConfigArchiver_BackendList
impl_ConfigArchiver_Archive__get_backendList (PortableServer_Servant  servant,
					      CORBA_Environment      *ev) 
{
	return bonobo_object_dup_ref (BONOBO_OBJREF (ARCHIVE_FROM_SERVANT (servant)->backend_list), ev);
}

static ConfigArchiver_Location
impl_ConfigArchiver_Archive__get_currentLocation (PortableServer_Servant  servant,
						  CORBA_Environment      *ev)
{
	return CORBA_Object_duplicate (BONOBO_OBJREF (archive_get_current_location (ARCHIVE_FROM_SERVANT (servant))), ev);
}

static void
impl_ConfigArchiver_Archive__set_currentLocation (PortableServer_Servant   servant,
						  ConfigArchiver_Location  location_ref,
						  CORBA_Environment       *ev) 
{
	Archive *archive = ARCHIVE_FROM_SERVANT (servant);
	Location *location;

	location = LOCATION (bonobo_object_from_servant (location_ref->servant));

	if (location == NULL)
		bonobo_exception_set (ev, ex_ConfigArchiver_Archive_LocationNotFound);
	else
		archive_set_current_location (archive, location);
}

static void
impl_ConfigArchiver_Archive__set_currentLocationId (PortableServer_Servant   servant,
						    const CORBA_char        *locid,
						    CORBA_Environment       *ev) 
{
	archive_set_current_location_id (ARCHIVE_FROM_SERVANT (servant), locid);
}

CORBA_char *
impl_ConfigArchiver_Archive__get_currentLocationId (PortableServer_Servant  servant,
						    CORBA_Environment      *ev) 
{
	return CORBA_string_dup (archive_get_current_location_id (ARCHIVE_FROM_SERVANT (servant)));
}

BONOBO_X_TYPE_FUNC_FULL (Archive, ConfigArchiver_Archive, BONOBO_X_OBJECT_TYPE, archive);

static void
archive_init (Archive *archive) 
{
	archive->prefix = NULL;
	archive->locations = g_tree_new ((GCompareFunc) strcmp);
	archive->current_location_id = NULL;
}

static void
archive_class_init (ArchiveClass *klass) 
{
	GtkObjectClass *object_class;

	object_class = GTK_OBJECT_CLASS (klass);

	object_class->destroy = archive_destroy;
	object_class->set_arg = archive_set_arg;
	object_class->get_arg = archive_get_arg;

	gtk_object_add_arg_type ("Archive::prefix",
				 GTK_TYPE_POINTER,
				 GTK_ARG_READWRITE | GTK_ARG_CONSTRUCT_ONLY,
				 ARG_PREFIX);
	gtk_object_add_arg_type ("Archive::is-global",
				 GTK_TYPE_INT,
				 GTK_ARG_READWRITE | GTK_ARG_CONSTRUCT_ONLY,
				 ARG_IS_GLOBAL);

	klass->epv.getLocation            = impl_ConfigArchiver_Archive_getLocation;
	klass->epv.createLocation         = impl_ConfigArchiver_Archive_createLocation;
	klass->epv.getChildLocations      = impl_ConfigArchiver_Archive_getChildLocations;

	klass->epv._get_prefix            = impl_ConfigArchiver_Archive__get_prefix;
	klass->epv._get_isGlobal          = impl_ConfigArchiver_Archive__get_isGlobal;
	klass->epv._get_backendList       = impl_ConfigArchiver_Archive__get_backendList;
	klass->epv._get_currentLocation   = impl_ConfigArchiver_Archive__get_currentLocation;
	klass->epv._get_currentLocationId = impl_ConfigArchiver_Archive__get_currentLocationId;

	klass->epv._set_currentLocation   = impl_ConfigArchiver_Archive__set_currentLocation;
	klass->epv._set_currentLocationId = impl_ConfigArchiver_Archive__set_currentLocationId;

	parent_class = gtk_type_class (BONOBO_X_OBJECT_TYPE);
}

static void
archive_set_arg (GtkObject *object, GtkArg *arg, guint arg_id) 
{
	Archive *archive;

	g_return_if_fail (object != NULL);
	g_return_if_fail (IS_ARCHIVE (object));
	g_return_if_fail (arg != NULL);

	archive = ARCHIVE (object);

	switch (arg_id) {
	case ARG_PREFIX:
		if (GTK_VALUE_POINTER (*arg) != NULL)
			archive->prefix = g_strdup (GTK_VALUE_POINTER (*arg));
		break;

	case ARG_IS_GLOBAL:
		archive->is_global = GTK_VALUE_INT (*arg);
		archive->backend_list = 
			BACKEND_LIST (backend_list_new (archive->is_global));
		break;

	default:
		break;
	}
}

static void 
archive_get_arg (GtkObject *object, GtkArg *arg, guint arg_id) 
{
	Archive *archive;

	g_return_if_fail (object != NULL);
	g_return_if_fail (IS_ARCHIVE (object));
	g_return_if_fail (arg != NULL);

	archive = ARCHIVE (object);

	switch (arg_id) {
	case ARG_PREFIX:
		GTK_VALUE_POINTER (*arg) = archive->prefix;
		break;

	case ARG_IS_GLOBAL:
		GTK_VALUE_INT (*arg) = archive->is_global;
		break;

	default:
		arg->type = GTK_TYPE_INVALID;
		break;
	}
}

/**
 * archive_construct:
 * @archive:
 * @is_new: TRUE iff this is a new archive
 *
 * Load the archive information from disk
 *
 * Returns: TRUE on success and FALSE on failure
 */

gboolean
archive_construct (Archive *archive, gboolean is_new)
{
	gint ret = 0;

	g_return_val_if_fail (archive != NULL, FALSE);
	g_return_val_if_fail (IS_ARCHIVE (archive), FALSE);
	g_return_val_if_fail (archive->prefix != NULL, FALSE);

	if (is_new) {
		if (g_file_exists (archive->prefix))
			return FALSE;

		ret = mkdir (archive->prefix, S_IREAD | S_IWRITE | S_IEXEC);
		if (ret == -1) return FALSE;
	} else {
		if (!g_file_test (archive->prefix, G_FILE_TEST_ISDIR))
			return FALSE;
	}

	return TRUE;
}

/**
 * archive_load:
 * @is_global: TRUE iff we should load the global archive
 * 
 * Load either the global or per-user configuration archive
 * 
 * Return value: Reference to archive
 **/

BonoboObject *
archive_load (gboolean is_global) 
{
	BonoboObject *object;
	gchar *prefix;

	if (is_global)
		prefix = "/var/ximian-setup-tools";
	else
		prefix = g_concat_dir_and_file (g_get_home_dir (),
						".gnome/capplet-archive");

	object = BONOBO_OBJECT (gtk_object_new (archive_get_type (),
						"prefix", prefix,
						"is-global", is_global,
						NULL));

	if (!is_global)
		g_free (prefix);

	if (archive_construct (ARCHIVE (object), FALSE) == FALSE &&
	    archive_construct (ARCHIVE (object), TRUE) == FALSE)
	{
		bonobo_object_unref (object);
		return NULL;
	}

	return object;
}

static gint
free_location_cb (gchar *locid, BonoboObject *location) 
{
	bonobo_object_unref (location);
	g_free (locid);

	return FALSE;
}

static void 
archive_destroy (GtkObject *object) 
{
	Archive *archive;

	g_return_if_fail (object != NULL);
	g_return_if_fail (IS_ARCHIVE (object));

	DEBUG_MSG ("Enter");

	archive = ARCHIVE (object);

	g_tree_traverse (archive->locations, 
			 (GTraverseFunc) free_location_cb,
			 G_IN_ORDER,
			 NULL);

	g_tree_destroy (archive->locations);

	if (archive->current_location_id != NULL)
		g_free (archive->current_location_id);

	if (archive->backend_list != NULL)
		bonobo_object_unref (BONOBO_OBJECT (archive->backend_list));

	GTK_OBJECT_CLASS (parent_class)->destroy (GTK_OBJECT (archive));
}

/**
 * archive_get_location:
 * @locid: 
 * 
 * Get a reference to the location with the given name. 
 * 
 * Return value: Reference to location, NULL if no such location exists
 **/

Location *
archive_get_location (Archive       *archive,
		      const gchar   *locid) 
{
	BonoboObject *loc_obj;
	gchar *tmp;

	g_return_val_if_fail (archive != NULL, NULL);
	g_return_val_if_fail (IS_ARCHIVE (archive), NULL);
	g_return_val_if_fail (locid != NULL, NULL);

	/* Stupid borken glib... */
	tmp = g_strdup (locid);
	loc_obj = g_tree_lookup (archive->locations, tmp);
	g_free (tmp);

	if (loc_obj == NULL) {
		loc_obj = location_open (archive, locid);

		if (loc_obj == NULL)
			return NULL;
		else
			g_tree_insert (archive->locations, 
				       g_strdup (locid), loc_obj);
	} else {
		bonobo_object_ref (loc_obj);
	}

	return LOCATION (loc_obj);
}

/**
 * archive_create_location:
 * @archive:
 * @location:
 *
 * Creates a new location
 */

Location *
archive_create_location (Archive     *archive,
			 const gchar *locid,
			 const gchar *label,
			 Location    *parent)
{
	Location *location;

	g_return_val_if_fail (archive != NULL, NULL);
	g_return_val_if_fail (IS_ARCHIVE (archive), NULL);
	g_return_val_if_fail (locid != NULL, NULL);
	g_return_val_if_fail (label != NULL, NULL);
	g_return_val_if_fail (parent == NULL || IS_LOCATION (parent), NULL);

	location = LOCATION (location_new (archive, locid, label, parent));

	if (location == NULL)
		return NULL;

	g_tree_insert (archive->locations, g_strdup (locid), location);
	return location;
}

/**
 * archive_unregister_location:
 * @archive: 
 * @location: 
 * 
 * Unregisters a location from the archive
 **/

void
archive_unregister_location (Archive *archive, Location *location) 
{
	gchar *tmp;

	g_return_if_fail (archive != NULL);
	g_return_if_fail (IS_ARCHIVE (archive));
	g_return_if_fail (location != NULL);
	g_return_if_fail (IS_LOCATION (location));

	if (GTK_OBJECT_DESTROYED (archive)) return;

	tmp = g_strdup (location_get_id (location));
	g_tree_remove (archive->locations, tmp);
	g_free (tmp);
}

/**
 * archive_get_current_location:
 * 
 * Convenience function to get a pointer to the current location
 * 
 * Return value: Pointer to current location, or NULL if the current location
 * does not exist and a default location could not be created
 **/

Location *
archive_get_current_location (Archive *archive)
{
	const gchar *locid;

	g_return_val_if_fail (archive != NULL, NULL);
	g_return_val_if_fail (IS_ARCHIVE (archive), NULL);

	locid = archive_get_current_location_id (archive);

	if (locid == NULL)
		return NULL;
	else
		return archive_get_location (archive, locid);
}

/**
 * archive_set_current_location:
 * @location: Location to which to set archive
 * 
 * Set the current location in an archive to the location given; apply
 * configuration for the new location to all backends necessary
 **/

void
archive_set_current_location (Archive  *archive,
			      Location *location) 
{
	Location *old_location;
	GList *backends;

	g_return_if_fail (archive != NULL);
	g_return_if_fail (IS_ARCHIVE (archive));
	g_return_if_fail (location != NULL);
	g_return_if_fail (IS_LOCATION (location));

	old_location = archive_get_current_location (archive);

	archive_set_current_location_id (archive, location_get_id (location));

	backends = location_get_changed_backends (location, old_location);
	location_rollback_backends_to (location, NULL, 0, backends, TRUE);
}

/**
 * archive_set_current_location_id: 
 * @name: 
 * 
 * Sets the current location's name, but does not invoke any rollback
 **/

void
archive_set_current_location_id (Archive       *archive,
				 const gchar   *locid) 
{
	g_return_if_fail (archive != NULL);
	g_return_if_fail (IS_ARCHIVE (archive));
	g_return_if_fail (locid != NULL);

	if (archive->current_location_id != NULL)
		g_free (archive->current_location_id);

	archive->current_location_id = g_strdup (locid);

	if (archive->is_global)
		gnome_config_set_string
			("/ximian-setup-tools/config/current/global-location",
			 archive->current_location_id);
	else
		gnome_config_set_string
			("/capplet-archive/config/current/location",
			 archive->current_location_id);

	gnome_config_sync ();
}

/**
 * archive_get_current_location_id:
 * 
 * Get the name of the current location
 * 
 * Return value: String containing current location, should not be freed, or
 * NULL if no current location exists and the default location could not be
 * created
 **/

const gchar *
archive_get_current_location_id (Archive *archive) 
{
	gboolean def;
	Location *loc;
	Location *current_location;

	g_return_val_if_fail (archive != NULL, NULL);
	g_return_val_if_fail (IS_ARCHIVE (archive), NULL);

	if (archive->current_location_id == NULL) {
		if (archive->is_global)
			archive->current_location_id =
				gnome_config_get_string_with_default
				("/ximian-setup-tools/config/current/global-location=default", &def);
		else
			archive->current_location_id =
				gnome_config_get_string_with_default
				("/capplet-archive/config/current/location=default", &def);

		/* Create default location if it does not exist */
		if (def) {
			current_location =
				archive_get_location (archive, archive->current_location_id);

			if (current_location == NULL) {
				loc = archive_create_location (archive, archive->current_location_id,
							       _("Default location"), NULL);
				if (archive->is_global &&
				    location_store_full_snapshot (loc) < 0)
				{
					location_delete (loc);
					return NULL;
				}

				bonobo_object_unref (BONOBO_OBJECT (loc));
			} else {
				bonobo_object_unref (BONOBO_OBJECT (current_location));
			}
		}
	}

	return archive->current_location_id;
}

/**
 * archive_get_prefix:
 * @archive: 
 * 
 * Get the prefix for locations in this archive
 * 
 * Return value: String containing prefix; should not be freed
 **/

const gchar *
archive_get_prefix (Archive *archive) 
{
	g_return_val_if_fail (archive != NULL, FALSE);
	g_return_val_if_fail (IS_ARCHIVE (archive), FALSE);

	return archive->prefix;
}

/**
 * archive_is_global:
 * @archive: 
 * 
 * Tell whether the archive is global or per-user
 * 
 * Return value: TRUE if global, FALSE if per-user
 **/

gboolean
archive_is_global (Archive *archive) 
{
	g_return_val_if_fail (archive != NULL, FALSE);
	g_return_val_if_fail (IS_ARCHIVE (archive), FALSE);

	return archive->is_global;
}

/**
 * archive_get_backend_list:
 * @archive: 
 * 
 * Get the master backend list for this archive
 * 
 * Return value: Reference to the master backend list
 **/

BackendList *
archive_get_backend_list (Archive *archive)
{
	g_return_val_if_fail (archive != NULL, FALSE);
	g_return_val_if_fail (IS_ARCHIVE (archive), FALSE);

	return archive->backend_list;
}

/**
 * archive_foreach_child_location:
 * @archive:
 * @callback: Callback to invoke
 * @parent: Iterate through the children of this location; iterate through
 * toplevel locations if this is NULL
 * @data: Arbitrary data to pass to the callback
 *
 * Invoke the given callback for each location that inherits the given
 * location, or for each toplevel location if the parent given is
 * NULL. Terminate the iteration if any child returns a nonzero value
 **/

static gint
foreach_build_list_cb (gchar *key, Location *value, GList **node) 
{
	if (!location_is_deleted (value))
		*node = g_list_prepend (*node, value);

	return 0;
}

GList *
archive_get_child_locations (Archive *archive,
			     Location *parent)
{
	GList *list = NULL, *node, *tmp;
	Location *loc;

	g_return_val_if_fail (archive != NULL, NULL);
	g_return_val_if_fail (IS_ARCHIVE (archive), NULL);

	load_all_locations (archive);

	g_tree_traverse (archive->locations,
			 (GTraverseFunc) foreach_build_list_cb,
			 G_IN_ORDER,
			 &list);

	node = list;

	while (node != NULL) {
		loc = node->data;
		tmp = node->next;

		if (location_get_parent (loc) != parent) {
			list = g_list_remove_link (list, node);
			g_list_free_1 (node);
			bonobo_object_unref (BONOBO_OBJECT (loc));
		}

		node = tmp;
	}

	return list;
}

/* Load and register all the locations for this archive */

static void
load_all_locations (Archive *archive) 
{
	DIR *archive_dir;
	struct dirent entry, *entryp;
	gchar *filename;
	Location *location;

	archive_dir = opendir (archive->prefix);

	if (archive_dir == NULL) {
		g_warning ("load_all_locations: %s", g_strerror (errno));
		return;
	}

	while (1) {
		if (readdir_r (archive_dir, &entry, &entryp)) {
			g_warning ("load_all_locations: %s",
				   g_strerror (errno));
			break;
		}

		if (entryp == NULL) break;

		if (strcmp (entry.d_name, ".") &&
		    strcmp (entry.d_name, ".."))
		{
			filename = g_concat_dir_and_file (archive->prefix,
							  entry.d_name);
			if (g_file_test (filename, G_FILE_TEST_ISDIR)) {
				location = archive_get_location (archive, entry.d_name);
				if (location_is_deleted (location))
					bonobo_object_unref (BONOBO_OBJECT (location));
			}
		}
	}
}
