/* Evolution calendar factory
 *
 * Copyright (C) 2000-2003 Ximian, Inc.
 *
 * Authors: 
 *   Federico Mena-Quintero <federico@ximian.com>
 *   JP Rosevear <jpr@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.
 */

#include <bonobo-activation/bonobo-activation.h>
#include <bonobo/bonobo-exception.h>
#include <bonobo/bonobo-main.h>
#include "libedataserver/e-url.h"
#include "libedataserver/e-source.h"
#include "libedataserver/e-data-server-module.h"
#include "e-cal-backend.h"
#include "e-data-cal.h"
#include "e-data-cal-factory.h"

#define PARENT_TYPE                BONOBO_TYPE_OBJECT
#define DEFAULT_E_DATA_CAL_FACTORY_OAF_ID "OAFIID:GNOME_Evolution_DataServer_CalFactory:" BASE_VERSION

static BonoboObjectClass *parent_class;

/* Private part of the CalFactory structure */
struct _EDataCalFactoryPrivate {
	/* Hash table from URI method strings to GType * for backend class types */
	GHashTable *methods;

	/* Hash table from GnomeVFSURI structures to CalBackend objects */
	GHashTable *backends;

	/* OAFIID of the factory */
	char *iid;

	/* Whether we have been registered with OAF yet */
	guint registered : 1;
        
        int mode;
        
};

/* Signal IDs */
enum SIGNALS {
	LAST_CALENDAR_GONE,
	LAST_SIGNAL
};

static guint signals[LAST_SIGNAL];

/* Opening calendars */

static icalcomponent_kind
calobjtype_to_icalkind (const GNOME_Evolution_Calendar_CalObjType type)
{
	switch (type){
	case GNOME_Evolution_Calendar_TYPE_EVENT:
		return ICAL_VEVENT_COMPONENT;
	case GNOME_Evolution_Calendar_TYPE_TODO:
		return ICAL_VTODO_COMPONENT;
	case GNOME_Evolution_Calendar_TYPE_JOURNAL:
		return ICAL_VJOURNAL_COMPONENT;
	}
	
	return ICAL_NO_COMPONENT;
}

static ECalBackendFactory*
get_backend_factory (GHashTable *methods, const char *method, icalcomponent_kind kind)
{
	GHashTable *kinds;
	ECalBackendFactory *factory;
	
	kinds = g_hash_table_lookup (methods, method);
	if (!kinds)
		return NULL;

	factory = g_hash_table_lookup (kinds, GINT_TO_POINTER (kind));

	return factory;
}

/* Callback used when a backend loses its last connected client */
static void
backend_last_client_gone_cb (ECalBackend *backend, gpointer data)
{
	EDataCalFactory *factory;
	EDataCalFactoryPrivate *priv;
	ECalBackend *ret_backend;
	const char *uristr;
	char *uri;

	fprintf (stderr, "backend_last_client_gone_cb() called!\n");

	factory = E_DATA_CAL_FACTORY (data);
	priv = factory->priv;

	/* Remove the backend from the hash table */

	uristr = e_cal_backend_get_uri (backend);
	g_assert (uristr != NULL);
	uri = g_strdup_printf("%s:%d", uristr, (int)e_cal_backend_get_kind(backend));

	ret_backend = g_hash_table_lookup (factory->priv->backends, uri);
	g_assert (ret_backend != NULL);
	g_assert (ret_backend == backend);

	g_hash_table_remove (priv->backends, uri);
	g_free(uri);

	g_signal_handlers_disconnect_matched (backend, G_SIGNAL_MATCH_DATA,
					      0, 0, NULL, NULL, data);

	/* Notify upstream if there are no more backends */
	if (g_hash_table_size (priv->backends) == 0)
		g_signal_emit (G_OBJECT (factory), signals[LAST_CALENDAR_GONE], 0);
}



static GNOME_Evolution_Calendar_Cal
impl_CalFactory_getCal (PortableServer_Servant servant,
			const CORBA_char *source_xml,
			const GNOME_Evolution_Calendar_CalObjType type,
			const GNOME_Evolution_Calendar_CalListener listener,
			CORBA_Environment *ev)
{
	GNOME_Evolution_Calendar_Cal ret_cal = NULL;
	EDataCalFactory *factory;
	EDataCalFactoryPrivate *priv;
	EDataCal *cal = CORBA_OBJECT_NIL;
	ECalBackend *backend;
	CORBA_Environment ev2;
	GNOME_Evolution_Calendar_CalListener listener_copy;
	ECalBackendFactory *backend_factory;
	ESource *source;
	char *str_uri;
	EUri *uri;
	char *uri_type_string;
	
	factory = E_DATA_CAL_FACTORY (bonobo_object_from_servant (servant));
	priv = factory->priv;

	source = e_source_new_from_standalone_xml (source_xml);
	if (!source) {
		bonobo_exception_set (ev, ex_GNOME_Evolution_Calendar_CalFactory_InvalidURI);

		return CORBA_OBJECT_NIL;
	}

	/* Get the URI so we can extract the protocol */
	str_uri = e_source_get_uri (source);
	if (!str_uri) {
		g_object_unref (source);
		bonobo_exception_set (ev, ex_GNOME_Evolution_Calendar_CalFactory_InvalidURI);

		return CORBA_OBJECT_NIL;
	}

	/* Parse the uri */
	uri = e_uri_new (str_uri);
	if (!uri) {
		bonobo_exception_set (ev, ex_GNOME_Evolution_Calendar_CalFactory_InvalidURI);

		return CORBA_OBJECT_NIL;
	}

	uri_type_string = g_strdup_printf ("%s:%d", str_uri, (int)calobjtype_to_icalkind (type));
	g_free(str_uri);

	/* Find the associated backend factory (if any) */
	backend_factory = get_backend_factory (priv->methods, uri->protocol, calobjtype_to_icalkind (type));
	if (!backend_factory) {
		/* FIXME Distinguish between method and kind failures? */
		bonobo_exception_set (ev, ex_GNOME_Evolution_Calendar_CalFactory_UnsupportedMethod);
		goto cleanup;
	}
		
	/* Duplicate the listener object */
	CORBA_exception_init (&ev2);
	listener_copy = CORBA_Object_duplicate (listener, &ev2);

	if (BONOBO_EX (&ev2)) {
		g_warning (G_STRLOC ": could not duplicate the listener");
		bonobo_exception_set (ev, ex_GNOME_Evolution_Calendar_CalFactory_NilListener);
		CORBA_exception_free (&ev2);
		goto cleanup;
	}
	CORBA_exception_free (&ev2);

	/* Look for an existing backend */
	backend = g_hash_table_lookup (factory->priv->backends, uri_type_string);
	if (!backend) {
		/* There was no existing backend, create a new one */
		backend = e_cal_backend_factory_new_backend (backend_factory, source);

		if (!backend) {
			g_warning (G_STRLOC ": could not instantiate backend");
			bonobo_exception_set (ev, ex_GNOME_Evolution_Calendar_CalFactory_UnsupportedMethod);
			goto cleanup;
		}

		/* Track the backend */
		g_hash_table_insert (priv->backends, g_strdup (uri_type_string), backend);

		g_signal_connect (G_OBJECT (backend), "last_client_gone",
				  G_CALLBACK (backend_last_client_gone_cb),
				  factory);
	}
	
	/* Create the corba calendar */
	cal = e_data_cal_new (backend, listener);
	printf ("cal = %p\n", cal);
	if (!cal) {
		g_warning (G_STRLOC ": could not create the corba calendar");
		bonobo_exception_set (ev, ex_GNOME_Evolution_Calendar_CalFactory_UnsupportedMethod);
		goto cleanup;
	}

	/* Let the backend know about its clients corba clients */
	e_cal_backend_add_client (backend, cal);
	e_cal_backend_set_mode (backend, priv->mode);
	
	ret_cal = CORBA_Object_duplicate (BONOBO_OBJREF (cal), ev);
 cleanup:
	e_uri_free (uri);
	g_free (uri_type_string);
	g_object_unref (source);

	return ret_cal;
}



/**
 * e_data_cal_factory_new:
 * @void:
 *
 * Creates a new #EDataCalFactory object.
 *
 * Return value: A newly-created #EDataCalFactory, or NULL if its corresponding CORBA
 * object could not be created.
 **/
EDataCalFactory *
e_data_cal_factory_new (void)
{
	EDataCalFactory *factory;

	factory = g_object_new (E_TYPE_DATA_CAL_FACTORY, 
				"poa", bonobo_poa_get_threaded (ORBIT_THREAD_HINT_PER_REQUEST, NULL), 
				NULL);

	return factory;
}

/* Destroy handler for the calendar */
static void
e_data_cal_factory_finalize (GObject *object)
{
	EDataCalFactory *factory;
	EDataCalFactoryPrivate *priv;

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

	factory = E_DATA_CAL_FACTORY (object);
	priv = factory->priv;

	g_hash_table_destroy (priv->methods);
	priv->methods = NULL;

	/* Should we assert that there are no more backends? */
	g_hash_table_destroy (priv->backends);
	priv->backends = NULL;

	if (priv->registered) {
		bonobo_activation_active_server_unregister (priv->iid, BONOBO_OBJREF (factory));
		priv->registered = FALSE;
	}
	g_free (priv->iid);

	g_free (priv);
	factory->priv = NULL;

	if (G_OBJECT_CLASS (parent_class)->finalize)
		(* G_OBJECT_CLASS (parent_class)->finalize) (object);
}

/* Class initialization function for the calendar factory */
static void
e_data_cal_factory_class_init (EDataCalFactoryClass *klass)
{
	GObjectClass *object_class = (GObjectClass *) klass;
	POA_GNOME_Evolution_Calendar_CalFactory__epv *epv = &klass->epv;

	parent_class = g_type_class_peek_parent (klass);

	signals[LAST_CALENDAR_GONE] =
		g_signal_new ("last_calendar_gone",
			      G_TYPE_FROM_CLASS (klass),
			      G_SIGNAL_RUN_FIRST,
			      G_STRUCT_OFFSET (EDataCalFactoryClass, last_calendar_gone),
			      NULL, NULL,
			      g_cclosure_marshal_VOID__VOID,
			      G_TYPE_NONE, 0);

	/* Class method overrides */
	object_class->finalize = e_data_cal_factory_finalize;

	/* Epv methods */
	epv->getCal = impl_CalFactory_getCal;
}

static void 
set_backend_online_status (gpointer key, gpointer value, gpointer data)
{
	ECalBackend *backend = E_CAL_BACKEND (value);

	e_cal_backend_set_mode (backend,  GPOINTER_TO_INT (data));
}

/**
 * e_data_cal_factory_set_backend_mode:
 * @factory: A calendar factory.
 * @mode: Online mode to set.
 *
 * Sets the online mode for all backends created by the given factory.
 */
void 
e_data_cal_factory_set_backend_mode (EDataCalFactory *factory, int mode)
{
	EDataCalFactoryPrivate *priv = factory->priv;
	
	
	priv->mode = mode;
	g_hash_table_foreach (priv->backends, set_backend_online_status, GINT_TO_POINTER (priv->mode));
}


/* Object initialization function for the calendar factory */
static void
e_data_cal_factory_init (EDataCalFactory *factory, EDataCalFactoryClass *klass)
{
	EDataCalFactoryPrivate *priv;

	priv = g_new0 (EDataCalFactoryPrivate, 1);
	factory->priv = priv;

	priv->methods = g_hash_table_new_full (g_str_hash, g_str_equal, 
					       (GDestroyNotify) g_free, (GDestroyNotify) g_hash_table_destroy);
	priv->backends = g_hash_table_new_full (g_str_hash, g_str_equal, 
						(GDestroyNotify) g_free, (GDestroyNotify) g_object_unref);
	priv->registered = FALSE;
}

BONOBO_TYPE_FUNC_FULL (EDataCalFactory,
		       GNOME_Evolution_Calendar_CalFactory,
		       PARENT_TYPE,
		       e_data_cal_factory);

/**
 * e_data_cal_factory_register_storage:
 * @factory: A calendar factory.
 * @iid: OAFIID for the factory to be registered.
 * 
 * Registers a calendar factory with the OAF object activation daemon.  This
 * function must be called before any clients can activate the factory.
 * 
 * Return value: TRUE on success, FALSE otherwise.
 **/
gboolean
e_data_cal_factory_register_storage (EDataCalFactory *factory, const char *iid)
{
	EDataCalFactoryPrivate *priv;
	Bonobo_RegistrationResult result;
	char *tmp_iid;

	g_return_val_if_fail (factory != NULL, FALSE);
	g_return_val_if_fail (E_IS_DATA_CAL_FACTORY (factory), FALSE);

	priv = factory->priv;

	g_return_val_if_fail (!priv->registered, FALSE);

	/* if iid is NULL, use the default factory OAFIID */
	if (iid)
		tmp_iid = g_strdup (iid);
	else
		tmp_iid = g_strdup (DEFAULT_E_DATA_CAL_FACTORY_OAF_ID);

	result = bonobo_activation_active_server_register (tmp_iid, BONOBO_OBJREF (factory));

	switch (result) {
	case Bonobo_ACTIVATION_REG_SUCCESS:
		priv->registered = TRUE;
		priv->iid = tmp_iid;
		return TRUE;

	case Bonobo_ACTIVATION_REG_NOT_LISTED:
		g_warning (G_STRLOC ": cannot register the calendar factory %s (not listed)", tmp_iid);
		break;

	case Bonobo_ACTIVATION_REG_ALREADY_ACTIVE:
		g_warning (G_STRLOC ": cannot register the calendar factory (already active)");
		break;

	case Bonobo_ACTIVATION_REG_ERROR:
	default:
		g_warning (G_STRLOC ": cannot register the calendar factory (generic error)");
		break;
	}

	g_free (tmp_iid);

	return FALSE;
}

/**
 * e_data_cal_factory_register_backend:
 * @factory: A calendar factory.
 * @backend_factory: The object responsible for creating backends.
 * 
 * Registers an #ECalBackend subclass that will be used to handle URIs
 * with a particular method.  When the factory is asked to open a
 * particular URI, it will look in its list of registered methods and
 * create a backend of the appropriate type.
 **/
void
e_data_cal_factory_register_backend (EDataCalFactory *factory, ECalBackendFactory *backend_factory)
{
	EDataCalFactoryPrivate *priv;
	const char *method;
	char *method_str;
	GHashTable *kinds;
	GType type;
	icalcomponent_kind kind;

	g_return_if_fail (factory && E_IS_DATA_CAL_FACTORY (factory));
	g_return_if_fail (backend_factory && E_IS_CAL_BACKEND_FACTORY (backend_factory));

	priv = factory->priv;

	method = E_CAL_BACKEND_FACTORY_GET_CLASS (backend_factory)->get_protocol (backend_factory);
	kind = E_CAL_BACKEND_FACTORY_GET_CLASS (backend_factory)->get_kind (backend_factory);

	method_str = g_ascii_strdown (method, -1);

	kinds = g_hash_table_lookup (priv->methods, method_str);
	if (kinds) {
		type = GPOINTER_TO_INT (g_hash_table_lookup (kinds, GINT_TO_POINTER (kind)));
		if (type) {
			g_warning (G_STRLOC ": method `%s' already registered", method_str);
			g_free (method_str);

			return;
		}

		g_free (method_str);
	} else {
		kinds = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, NULL);
		g_hash_table_insert (priv->methods, method_str, kinds);
	}
	
	g_hash_table_insert (kinds, GINT_TO_POINTER (kind), backend_factory);
}

/**
 * e_data_cal_factory_register_backends:
 * @cal_factory: A calendar factory.
 *
 * Register all backends for the given factory.
 */
void
e_data_cal_factory_register_backends (EDataCalFactory *cal_factory)
{
	GList *factories, *f;

	factories = e_data_server_get_extensions_for_type (E_TYPE_CAL_BACKEND_FACTORY);
	for (f = factories; f; f = f->next) {
		ECalBackendFactory *backend_factory = f->data;

		e_data_cal_factory_register_backend (cal_factory, g_object_ref (backend_factory));
	}

	e_data_server_extension_list_free (factories);
}

/**
 * e_data_cal_factory_get_n_backends
 * @factory: A calendar factory.
 *
 * Get the number of backends currently active in the given factory.
 *
 * Returns: the number of backends.
 */
int
e_data_cal_factory_get_n_backends (EDataCalFactory *factory)
{
	EDataCalFactoryPrivate *priv;

	g_return_val_if_fail (E_IS_DATA_CAL_FACTORY (factory), 0);

	priv = factory->priv;
	return g_hash_table_size (priv->backends);
}

/* Frees a uri/backend pair from the backends hash table */
static void
dump_backend (gpointer key, gpointer value, gpointer data)
{
	char *uri;
	ECalBackend *backend;

	uri = key;
	backend = value;

	g_message ("  %s: %p", uri, backend);
}

/**
 * e_data_cal_factory_dump_active_backends:
 * @factory: A calendar factory.
 *
 * Dumps to standard output a list of all active backends for the given
 * factory.
 */
void
e_data_cal_factory_dump_active_backends (EDataCalFactory *factory)
{
	EDataCalFactoryPrivate *priv;

	g_message ("Active PCS backends");

	priv = factory->priv;
	g_hash_table_foreach (priv->backends, dump_backend, NULL);
}
