/*-------------------------------------------------------------------------
 * C-Pluff, a plug-in framework for C
 * Copyright 2007 Johannes Lehtinen
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *-----------------------------------------------------------------------*/

/** @file
 * Core plug-in management functions
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <stddef.h>
#include "../kazlib/list.h"
#include "../kazlib/hash.h"
#include "cpluff.h"
#include "defines.h"
#include "util.h"
#include "internal.h"
#ifdef _WIN32
#include <windows.h>
#endif


/* ------------------------------------------------------------------------
 * Function definitions
 * ----------------------------------------------------------------------*/

// Plug-in control

#ifndef NDEBUG
static void assert_processed_zero(cp_context_t *context) {
	hscan_t scan;
	hnode_t *node;
	
	hash_scan_begin(&scan, context->env->plugins);
	while ((node = hash_scan_next(&scan)) != NULL) {
		cp_plugin_t *plugin = hnode_get(node);
		assert(plugin->processed == 0);
	}
}
#else
#define assert_processed_zero(c) assert(1)
#endif

static void unregister_extensions(cp_context_t *context, cp_plugin_info_t *plugin) {
	unsigned int i;
	
	for (i = 0; i < plugin->num_ext_points; i++) {
		cp_ext_point_t *ep = plugin->ext_points + i;
		hnode_t *hnode;
		
		if ((hnode = hash_lookup(context->env->ext_points, ep->identifier)) != NULL
			&& hnode_get(hnode) == ep) {
			hash_delete_free(context->env->ext_points, hnode);
		}
	}
	for (i = 0; i < plugin->num_extensions; i++) {
		cp_extension_t *e = plugin->extensions + i;
		hnode_t *hnode;
		
		if ((hnode = hash_lookup(context->env->extensions, e->ext_point_id)) != NULL) {
			list_t *el = hnode_get(hnode);
			lnode_t *lnode = list_first(el);
			
			while (lnode != NULL) {
				lnode_t *nn = list_next(el, lnode);
				if (lnode_get(lnode) == e) {
					list_delete(el, lnode);
					lnode_destroy(lnode);
					break;
				}
				lnode = nn;
			}
			if (list_isempty(el)) {
				char *epid = (char *) hnode_getkey(hnode);				
				hash_delete_free(context->env->extensions, hnode);
				free(epid);
				list_destroy(el);
			}
		}
	}
}

CP_C_API cp_status_t cp_install_plugin(cp_context_t *context, cp_plugin_info_t *plugin) {
	cp_plugin_t *rp = NULL;
	cp_status_t status = CP_OK;
	cpi_plugin_event_t event;
	unsigned int i;

	CHECK_NOT_NULL(context);
	CHECK_NOT_NULL(plugin);
	
	cpi_lock_context(context);
	cpi_check_invocation(context, CPI_CF_ANY, __func__);
	do {
		
		// Check that there is no conflicting plug-in already loaded 
		if (hash_lookup(context->env->plugins, plugin->identifier) != NULL) {
			cpi_errorf(context,
				N_("Plug-in %s could not be installed because a plug-in with the same identifier is already installed."), 
				plugin->identifier);
			status = CP_ERR_CONFLICT;
			break;
		}

		// Increase usage count for the plug-in descriptor
		cpi_use_info(context, plugin);

		// Allocate space for the plug-in state 
		if ((rp = malloc(sizeof(cp_plugin_t))) == NULL) {
			status = CP_ERR_RESOURCE;
			break;
		}
	
		// Initialize plug-in state 
		memset(rp, 0, sizeof(cp_plugin_t));
		rp->context = NULL;
		rp->plugin = plugin;
		rp->state = CP_PLUGIN_INSTALLED;
		rp->imported = NULL;
		rp->runtime_lib = NULL;
		rp->runtime_funcs = NULL;
		rp->plugin_data = NULL;
		rp->importing = list_create(LISTCOUNT_T_MAX);
		if (rp->importing == NULL) {
			status = CP_ERR_RESOURCE;
			break;
		}
		if (!hash_alloc_insert(context->env->plugins, plugin->identifier, rp)) {
			status = CP_ERR_RESOURCE;
			break;
		}
		
		// Register extension points
		for (i = 0; status == CP_OK && i < plugin->num_ext_points; i++) {
			cp_ext_point_t *ep = plugin->ext_points + i;
			hnode_t *hnode;
			
			if ((hnode = hash_lookup(context->env->ext_points, ep->identifier)) != NULL) {
				cpi_errorf(context, N_("Plug-in %s could not be installed because extension point %s conflicts with an already installed extension point."), plugin->identifier, ep->identifier);
				status = CP_ERR_CONFLICT;
			} else if (!hash_alloc_insert(context->env->ext_points, ep->identifier, ep)) {
				status = CP_ERR_RESOURCE;
			}
		}
		
		// Register extensions
		for (i = 0; status == CP_OK && i < plugin->num_extensions; i++) {
			cp_extension_t *e = plugin->extensions + i;
			hnode_t *hnode;
			lnode_t *lnode;
			list_t *el;
			
			if ((hnode = hash_lookup(context->env->extensions, e->ext_point_id)) == NULL) {
				char *epid;
				if ((el = list_create(LISTCOUNT_T_MAX)) != NULL
					&& (epid = strdup(e->ext_point_id)) != NULL) {
					if (!hash_alloc_insert(context->env->extensions, epid, el)) {
						list_destroy(el);
						status = CP_ERR_RESOURCE;
						break;
					}
				} else {
					if (el != NULL) {
						list_destroy(el);
					}
					status = CP_ERR_RESOURCE;
					break;
				}
			} else {
				el = hnode_get(hnode);
			}
			if ((lnode = lnode_create(e)) != NULL) {
				list_append(el, lnode);
			} else {
				status = CP_ERR_RESOURCE;
				break;
			}
		}

		// Break if previous loops failed
		if (status != CP_OK) {
			break;
		}
		
		// Plug-in installed 
		event.plugin_id = plugin->identifier;
		event.old_state = CP_PLUGIN_UNINSTALLED;
		event.new_state = rp->state;
		cpi_deliver_event(context, &event);

	} while (0);

	// Release resources on failure
	if (status != CP_OK) {
		if (rp != NULL) {
			if (rp->importing != NULL) {
				list_destroy(rp->importing);
			}
			free(rp);
		}
		unregister_extensions(context, plugin);
	}

	// Report possible resource error
	if (status == CP_ERR_RESOURCE) {
		cpi_errorf(context,
			N_("Plug-in %s could not be installed due to insufficient system resources."), plugin->identifier);
	}
	cpi_unlock_context(context);

	return status;
}

/**
 * Unresolves the plug-in runtime information.
 * 
 * @param plugin the plug-in to unresolve
 */
static void unresolve_plugin_runtime(cp_plugin_t *plugin) {

	// Destroy the plug-in instance, if necessary
	if (plugin->context != NULL) {
		plugin->context->env->in_destroy_func_invocation++;
		plugin->runtime_funcs->destroy(plugin->plugin_data);
		plugin->context->env->in_destroy_func_invocation--;
		plugin->plugin_data = NULL;
		cpi_free_context(plugin->context);
		plugin->context = NULL;
	}

	// Close plug-in runtime library	
	plugin->runtime_funcs = NULL;
	if (plugin->runtime_lib != NULL) {
		DLCLOSE(plugin->runtime_lib);
		plugin->runtime_lib = NULL;
	}	
}

/**
 * Loads and resolves the plug-in runtime library and initialization functions.
 * 
 * @param context the plug-in context
 * @param plugin the plugin
 * @return CP_OK (zero) on success or error code on failure
 */
static int resolve_plugin_runtime(cp_context_t *context, cp_plugin_t *plugin) {
	char *rlpath = NULL;
	int rlpath_len;
	cp_status_t status = CP_OK;
	
	assert(plugin->runtime_lib == NULL);
	if (plugin->plugin->runtime_lib_name == NULL) {
		return CP_OK;
	}
	
	do {
		int ppath_len, lname_len;
		int cpluff_compatibility = 1;
	
		// Check C-Pluff compatibility
		if (plugin->plugin->req_cpluff_version != NULL) {
#ifdef CP_ABI_COMPATIBILITY
			cpluff_compatibility = (
				cpi_vercmp(plugin->plugin->req_cpluff_version, CP_VERSION) <= 0
			 	&& cpi_vercmp(plugin->plugin->req_cpluff_version, CP_ABI_COMPATIBILITY) >= 0);
#else
			cpluff_compatibility = (cpi_vercmp(plugin->plugin->req_cpluff_version, CP_VERSION) == 0);
#endif
		}
		if (!cpluff_compatibility) {
			cpi_errorf(context, N_("Plug-in %s could not be resolved due to version incompatibility with C-Pluff."), plugin->plugin->identifier);
			status = CP_ERR_DEPENDENCY;
			break;
		}

		// Construct a path to plug-in runtime library.
		/// @todo Add platform specific prefix (for example, "lib")
		ppath_len = strlen(plugin->plugin->plugin_path);
		lname_len = strlen(plugin->plugin->runtime_lib_name);
		rlpath_len = ppath_len + lname_len + strlen(CP_SHREXT) + 2;
		if ((rlpath = malloc(rlpath_len * sizeof(char))) == NULL) {
			cpi_errorf(context, N_("Plug-in %s runtime library could not be loaded due to insufficient memory."), plugin->plugin->identifier);
			status = CP_ERR_RESOURCE;
			break;
		}
		memset(rlpath, 0, rlpath_len * sizeof(char));
		strcpy(rlpath, plugin->plugin->plugin_path);
		rlpath[ppath_len] = CP_FNAMESEP_CHAR;
		strcpy(rlpath + ppath_len + 1, plugin->plugin->runtime_lib_name);
		strcpy(rlpath + ppath_len + 1 + lname_len, CP_SHREXT);
		
		// Open the plug-in runtime library 
		plugin->runtime_lib = DLOPEN(rlpath);
		if (plugin->runtime_lib == NULL) {
			const char *error = DLERROR();
			if (error == NULL) {
				error = _("Unspecified error.");
			}
			cpi_errorf(context, N_("Plug-in %s runtime library %s could not be opened: %s"), plugin->plugin->identifier, rlpath, error);
			status = CP_ERR_RUNTIME;
			break;
		}
		
		// Resolve plug-in functions
		if (plugin->plugin->runtime_funcs_symbol != NULL) {
			plugin->runtime_funcs = (cp_plugin_runtime_t *) DLSYM(plugin->runtime_lib, plugin->plugin->runtime_funcs_symbol);
			if (plugin->runtime_funcs == NULL) {
				const char *error = DLERROR();
				if (error == NULL) {
					error = _("Unspecified error.");
				}
				cpi_errorf(context, N_("Plug-in %s symbol %s containing plug-in runtime information could not be resolved: %s"), plugin->plugin->identifier, plugin->plugin->runtime_funcs_symbol, error);
				status = CP_ERR_RUNTIME;
				break;
			}
			if (plugin->runtime_funcs->create == NULL
				|| plugin->runtime_funcs->destroy == NULL) {
				cpi_errorf(context, N_("Plug-in %s is missing a constructor or destructor function."), plugin->plugin->identifier);
				status = CP_ERR_RUNTIME;
				break;
			}
		}

	} while (0);
	
	// Release resources 
	free(rlpath);
	if (status != CP_OK) {
		unresolve_plugin_runtime(plugin);
	}
	
	return status;
}

/**
 * Resolves the specified plug-in import into a plug-in pointer. Does not
 * try to resolve the imported plug-in.
 * 
 * @param context the plug-in context
 * @param plugin the plug-in being resolved
 * @param import the plug-in import to resolve
 * @param ipptr filled with pointer to the resolved plug-in or NULL
 * @return CP_OK on success or error code on failure
 */
static int resolve_plugin_import(cp_context_t *context, cp_plugin_t *plugin, cp_plugin_import_t *import, cp_plugin_t **ipptr) {
	cp_plugin_t *ip = NULL;
	hnode_t *node;

	// Lookup the plug-in 
	node = hash_lookup(context->env->plugins, import->plugin_id);
	if (node != NULL) {
		ip = hnode_get(node);
	}
			
	// Check plug-in version
	if (ip != NULL
		&& import->version != NULL
		&& (ip->plugin->version == NULL
			|| (ip->plugin->abi_bw_compatibility == NULL
				&& cpi_vercmp(import->version, ip->plugin->version) != 0)
			|| (ip->plugin->abi_bw_compatibility != NULL
				&& (cpi_vercmp(import->version, ip->plugin->version) > 0
					|| cpi_vercmp(import->version, ip->plugin->abi_bw_compatibility) < 0)))) {
		cpi_errorf(context,
			N_("Plug-in %s could not be resolved due to version incompatibility with plug-in %s."),
			plugin->plugin->identifier,
			import->plugin_id);
		*ipptr = NULL;
		return CP_ERR_DEPENDENCY;
	}
	
	// Check if missing mandatory plug-in
	if (ip == NULL && !import->optional) {
		cpi_errorf(context,
			N_("Plug-in %s could not be resolved because it depends on plug-in %s which is not installed."),
			plugin->plugin->identifier,
			import->plugin_id);
		*ipptr = NULL;
		return CP_ERR_DEPENDENCY;
	}

	// Return imported plug-in
	*ipptr = ip;
	return CP_OK;
}

/**
 * Resolves the specified plug-in and its dependencies while leaving plug-ins
 * with circular dependencies in a preliminarily resolved state.
 * 
 * @param context the plug-in context
 * @param plugin the plug-in
 * @return CP_OK (zero) or CP_OK_PRELIMINARY or an error code
 */
static int resolve_plugin_prel_rec(cp_context_t *context, cp_plugin_t *plugin) {
	cp_status_t status = CP_OK;
	int error_reported = 0;
	lnode_t *node = NULL;
	unsigned int i;

	// Check if already resolved
	if (plugin->state >= CP_PLUGIN_RESOLVED) {
		return CP_OK;
	}
	
	// Check for dependency loops
	if (plugin->processed) {
		return CP_OK_PRELIMINARY;
	}
	plugin->processed = 1;

	do {

		// Recursively resolve the imported plug-ins
		assert(plugin->imported == NULL);
		if ((plugin->imported = list_create(LISTCOUNT_T_MAX)) == NULL) {
			status = CP_ERR_RESOURCE;
			break;
		}
		for (i = 0; i < plugin->plugin->num_imports; i++) {
			cp_plugin_t *ip;
			int s;
				
			if ((node = lnode_create(NULL)) == NULL) {
				status = CP_ERR_RESOURCE;
				break;
			}
			if ((s = resolve_plugin_import(context, plugin, plugin->plugin->imports + i, &ip)) != CP_OK) {
				error_reported = 1;
				status = s;
				break;
			}
			if (ip != NULL) {
				lnode_put(node, ip);
				list_append(plugin->imported, node);
				node = NULL;
				if (!cpi_ptrset_add(ip->importing, plugin)) {
					status = CP_ERR_RESOURCE;
					break;
				} else if ((s = resolve_plugin_prel_rec(context, ip)) != CP_OK && s != CP_OK_PRELIMINARY) {
					cpi_errorf(context, N_("Plug-in %s could not be resolved because it depends on plug-in %s which could not be resolved."), plugin->plugin->identifier, ip->plugin->identifier);
					error_reported = 1;
					status = s;
					break;
				}
			} else {
				lnode_destroy(node);
				node = NULL;
			}
		}
		if (status != CP_OK) {
			break;
		}
		
		// Resolve this plug-in
		assert(plugin->state == CP_PLUGIN_INSTALLED);
		if ((i = resolve_plugin_runtime(context, plugin)) != CP_OK) {
			status = i;
			error_reported = 1;
			break;
		}
		
		// Notify event listeners and update state if completely resolved
		if (status == CP_OK) {
			cpi_plugin_event_t event;
			
			plugin->processed = 0;
			event.plugin_id = plugin->plugin->identifier;
			event.old_state = plugin->state;
			event.new_state = plugin->state = CP_PLUGIN_RESOLVED;
			cpi_deliver_event(context, &event);
		}

	} while (0);

	// Clean up
	if (node != NULL) {
		lnode_destroy(node);
	}

	// Handle errors
	if (status == CP_ERR_RESOURCE && !error_reported) {
		cpi_errorf(context, N_("Plug-in %s could not be resolved because of insufficient memory."), plugin->plugin->identifier);
	}
	
	return status;
}

/**
 * Recursively commits the resolving process for the specified plug-in and
 * its dependencies.
 * 
 * @param context the plug-in context
 * @param plugin the plug-in
 */
static void resolve_plugin_commit_rec(cp_context_t *context, cp_plugin_t *plugin) {
	
	// Check if already committed
	if (!plugin->processed) {
		return;
	}
	plugin->processed = 0;
	
	// Commit if only preliminarily resolved
	if (plugin->state < CP_PLUGIN_RESOLVED) {
			cpi_plugin_event_t event;
			lnode_t *node;

			// Recursively commit dependencies
			node = list_first(plugin->imported);
			while (node != NULL) {
				resolve_plugin_commit_rec(context, (cp_plugin_t *) lnode_get(node));
				node = list_next(plugin->imported, node);
			}
			
			// Notify event listeners and update state
			event.plugin_id = plugin->plugin->identifier;
			event.old_state = plugin->state;
			event.new_state = plugin->state = CP_PLUGIN_RESOLVED;
			cpi_deliver_event(context, &event);		
	}
}

/**
 * Recursively cleans up the specified plug-in and its dependencies after
 * a failed resolving attempt.
 * 
 * @param plugin the plug-in
 */
static void resolve_plugin_failed_rec(cp_plugin_t *plugin) {
	
	// Check if already cleaned up
	if (!plugin->processed) {
		return;
	}
	plugin->processed = 0;
	
	// Clean up if only preliminarily resolved
	if (plugin->state < CP_PLUGIN_RESOLVED) {
		lnode_t *node;

		// Recursively clean up depedencies
		while ((node = list_first(plugin->imported)) != NULL) {
			cp_plugin_t *ip = lnode_get(node);
			
			resolve_plugin_failed_rec(ip);
			cpi_ptrset_remove(ip->importing, plugin);
			list_delete(plugin->imported, node);
			lnode_destroy(node);
		}
		list_destroy(plugin->imported);
		plugin->imported = NULL;
	}
}

/**
 * Resolves the specified plug-in and its dependencies.
 * 
 * @param context the plug-in context
 * @param plugin the plug-in to be resolved
 * @return CP_OK (zero) on success or an error code on failure
 */
static int resolve_plugin(cp_context_t *context, cp_plugin_t *plugin) {
	cp_status_t status;
	
	if ((status = resolve_plugin_prel_rec(context, plugin)) == CP_OK || status == CP_OK_PRELIMINARY) {
		status = CP_OK;
		resolve_plugin_commit_rec(context, plugin);
	} else {
		resolve_plugin_failed_rec(plugin);
	}
	assert_processed_zero(context);
	return status;
}

/**
 * Starts the plug-in runtime of the specified plug-in. This function does
 * not consider dependencies and assumes that the plug-in is resolved but
 * not yet started.
 * 
 * @param context the plug-in context
 * @param plugin the plug-in
 * @return CP_OK (zero) on success or an error code on failure
 */
static int start_plugin_runtime(cp_context_t *context, cp_plugin_t *plugin) {
	cp_status_t status = CP_OK;
	cpi_plugin_event_t event;
	lnode_t *node = NULL;

	event.plugin_id = plugin->plugin->identifier;
	do {

		// Allocate space for the list node 
		node = lnode_create(plugin);
		if (node == NULL) {
			status = CP_ERR_RESOURCE;
			break;
		}
		
		// Set up plug-in instance
		if (plugin->runtime_funcs != NULL) {

			// Create plug-in instance if necessary
			if (plugin->context == NULL) {
				if ((plugin->context = cpi_new_context(plugin, context->env, &status)) == NULL) {
					break;
				}
				context->env->in_create_func_invocation++;
				plugin->plugin_data = plugin->runtime_funcs->create(plugin->context);
				context->env->in_create_func_invocation--;
				if (plugin->plugin_data == NULL) {
					status = CP_ERR_RUNTIME;
					break;
				}
			}
			
			// Start plug-in
			if (plugin->runtime_funcs->start != NULL) {
				int s;
			
				// About to start the plug-in 
				event.old_state = plugin->state;
				event.new_state = plugin->state = CP_PLUGIN_STARTING;
				cpi_deliver_event(context, &event);
		
				// Start the plug-in
				context->env->in_start_func_invocation++;
				s = plugin->runtime_funcs->start(plugin->plugin_data);
				context->env->in_start_func_invocation--;

				if (s != CP_OK) {
			
					// Roll back plug-in state 
					if (plugin->runtime_funcs->stop != NULL) {

						// Update state					
						event.old_state = plugin->state;
						event.new_state = plugin->state = CP_PLUGIN_STOPPING;
						cpi_deliver_event(context, &event);
					
						// Call stop function
						context->env->in_stop_func_invocation++;
						plugin->runtime_funcs->stop(plugin->plugin_data);
						context->env->in_stop_func_invocation--;
					}
				
					// Destroy plug-in object
					context->env->in_destroy_func_invocation++;
					plugin->runtime_funcs->destroy(plugin->plugin_data);
					context->env->in_destroy_func_invocation--;
			
					status = CP_ERR_RUNTIME;
					break;
				}
			}
		}
		
		// Plug-in active 
		list_append(context->env->started_plugins, node);
		event.old_state = plugin->state;
		event.new_state = plugin->state = CP_PLUGIN_ACTIVE;
		cpi_deliver_event(context, &event);
		
	} while (0);

	// Release resources and roll back plug-in state on failure
	if (status != CP_OK) {
		if (node != NULL) {
			lnode_destroy(node);
		}
		if (plugin->context != NULL) {
			cpi_free_context(plugin->context);
			plugin->context = NULL;
		}
		if (plugin->state != CP_PLUGIN_RESOLVED) {
			event.old_state = plugin->state;
			event.new_state = plugin->state = CP_PLUGIN_RESOLVED;
			cpi_deliver_event(context, &event);
		}
		plugin->plugin_data = NULL;
	}

	// Report error on failure
	switch (status) {
		case CP_ERR_RESOURCE:
			cpi_errorf(context,
				N_("Plug-in %s could not be started due to insufficient memory."),
				plugin->plugin->identifier);
			break;
		case CP_ERR_RUNTIME:
			cpi_errorf(context,
				N_("Plug-in %s failed to start due to plug-in runtime error."),
				plugin->plugin->identifier);
			break;
		default:
			break;
	}	
	
	return status;
}

static void warn_dependency_loop(cp_context_t *context, cp_plugin_t *plugin, list_t *importing, int dynamic) {
	char *msgbase;
	char *msg;
	int msgsize;
	lnode_t *node;
	
	// Take the message base
	if (dynamic) {
		msgbase = N_("Detected a runtime plug-in dependency loop: %s");
	} else {
		msgbase = N_("Detected a static plug-in dependency loop: %s");
	}
	
	// Calculate the required message space
	msgsize = 0;
	msgsize += strlen(plugin->plugin->identifier);
	msgsize += 2;
	node = list_last(importing);
	while (node != NULL) {
		cp_plugin_t *p = lnode_get(node);
		if (p == plugin) {
			break;
		}
		msgsize += strlen(p->plugin->identifier);
		msgsize += 2;
		node = list_prev(importing, node);
	}
	msg = malloc(sizeof(char) * msgsize);
	if (msg != NULL) {
		strcpy(msg, plugin->plugin->identifier);
		node = list_last(importing);
		while (node != NULL) {
			cp_plugin_t *p = lnode_get(node);
			if (p == plugin) {
				break;
			}
			strcat(msg, ", ");
			strcat(msg, p->plugin->identifier);
			node = list_prev(importing, node);
		}
		strcat(msg, ".");
		cpi_infof(context, msgbase, msg);
		free(msg);
	} else {
		cpi_infof(context, msgbase, plugin->plugin->identifier);
	}
}

/**
 * Starts the specified plug-in and its dependencies.
 * 
 * @param context the plug-in context
 * @param plugin the plug-in
 * @param importing stack of importing plug-ins
 * @return CP_OK (zero) on success or an error code on failure
 */
static int start_plugin_rec(cp_context_t *context, cp_plugin_t *plugin, list_t *importing) {
	cp_status_t status = CP_OK;
	lnode_t *node;
	
	// Check if already started or starting
	if (plugin->state == CP_PLUGIN_ACTIVE) {
		return CP_OK;
	} else if (plugin->state == CP_PLUGIN_STARTING) {
		warn_dependency_loop(context, plugin, importing, 1);
		return CP_OK;
	}
	assert(plugin->state == CP_PLUGIN_RESOLVED);
	
	// Check for dependency loops
	if (cpi_ptrset_contains(importing, plugin)) {
		warn_dependency_loop(context, plugin, importing, 0);
		return CP_OK;
	}
	if (!cpi_ptrset_add(importing, plugin)) {
		cpi_errorf(context,
			N_("Plug-in %s could not be started due to insufficient memory."),
			plugin->plugin->identifier);
		return CP_ERR_RESOURCE;
	}

	// Start up dependencies
	node = list_first(plugin->imported);
	while (node != NULL) {
		cp_plugin_t *ip = lnode_get(node);
		
		if ((status = start_plugin_rec(context, ip, importing)) != CP_OK) {
			break;
		}
		node = list_next(plugin->imported, node);
	}
	cpi_ptrset_remove(importing, plugin);
	
	// Start up this plug-in
	if (status == CP_OK) {
		status = start_plugin_runtime(context, plugin);
	}

	return status;
}

CP_HIDDEN cp_status_t cpi_start_plugin(cp_context_t *context, cp_plugin_t *plugin) {
	cp_status_t status;
	
	if ((status = resolve_plugin(context, plugin)) == CP_OK) {
		list_t *importing = list_create(LISTCOUNT_T_MAX);
		if (importing != NULL) {
			status = start_plugin_rec(context, plugin, importing);
			assert(list_isempty(importing));
			list_destroy(importing);
		} else {
			cpi_errorf(context,
				N_("Plug-in %s could not be started due to insufficient memory."),
				plugin->plugin->identifier);
			status = CP_ERR_RESOURCE;
		}
	}
	return status;
}

CP_C_API cp_status_t cp_start_plugin(cp_context_t *context, const char *id) {
	hnode_t *node;
	cp_status_t status = CP_OK;

	CHECK_NOT_NULL(context);
	CHECK_NOT_NULL(id);

	// Look up and start the plug-in 
	cpi_lock_context(context);
	cpi_check_invocation(context, CPI_CF_ANY, __func__);
	node = hash_lookup(context->env->plugins, id);
	if (node != NULL) {
		status = cpi_start_plugin(context, hnode_get(node));
	} else {
		cpi_warnf(context, N_("Unknown plug-in %s could not be started."), id);
		status = CP_ERR_UNKNOWN;
	}
	cpi_unlock_context(context);

	return status;
}

/**
 * Stops the plug-in runtime of the specified plug-in. This function does
 * not consider dependencies and assumes that the plug-in is active.
 * 
 * @param context the plug-in context
 * @param plugin the plug-in
 */
static void stop_plugin_runtime(cp_context_t *context, cp_plugin_t *plugin) {
	cpi_plugin_event_t event;
	
	// Destroy plug-in instance
	event.plugin_id = plugin->plugin->identifier;
	if (plugin->context != NULL) {
	
		// Wait until possible run functions have stopped
		cpi_stop_plugin_run(plugin);

		// Stop the plug-in
		if (plugin->runtime_funcs->stop != NULL) {

			// About to stop the plug-in 
			event.old_state = plugin->state;
			event.new_state = plugin->state = CP_PLUGIN_STOPPING;
			cpi_deliver_event(context, &event);
	
			// Invoke stop function	
			context->env->in_stop_func_invocation++;
			plugin->runtime_funcs->stop(plugin->plugin_data);
			context->env->in_stop_func_invocation--;

		}

		// Unregister all logger functions
		cpi_unregister_loggers(plugin->context->env->loggers, plugin);

		// Unregister all plug-in listeners
		cpi_unregister_plisteners(plugin->context->env->plugin_listeners, plugin);	

		// Release resolved symbols
		if (plugin->context->resolved_symbols != NULL) {
			while (!hash_isempty(plugin->context->resolved_symbols)) {
				hscan_t scan;
				hnode_t *node;
				const void *ptr;
			
				hash_scan_begin(&scan, plugin->context->resolved_symbols);
				node = hash_scan_next(&scan);
				ptr = hnode_getkey(node);
				cp_release_symbol(context, ptr);
			}
			assert(hash_isempty(plugin->context->resolved_symbols));
		}
		if (plugin->context->symbol_providers != NULL) {
			assert(hash_isempty(plugin->context->symbol_providers));
		}

		// Release defined symbols
		if (plugin->defined_symbols != NULL) {
			hscan_t scan;
			hnode_t *node;
			
			hash_scan_begin(&scan, plugin->defined_symbols);
			while ((node = hash_scan_next(&scan)) != NULL) {
				char *n = (char *) hnode_getkey(node);
				hash_scan_delfree(plugin->defined_symbols, node);
				free(n);
			}
			hash_destroy(plugin->defined_symbols);
			plugin->defined_symbols = NULL;
		}
		
	}
	
	// Plug-in stopped 
	cpi_ptrset_remove(context->env->started_plugins, plugin);
	event.old_state = plugin->state;
	event.new_state = plugin->state = CP_PLUGIN_RESOLVED;
	cpi_deliver_event(context, &event);
}

/**
 * Stops the plug-in and all plug-ins depending on it.
 * 
 * @param context the plug-in context
 * @param plugin the plug-in
 */
static void stop_plugin_rec(cp_context_t *context, cp_plugin_t *plugin) {
	lnode_t *node;
	
	// Check if already stopped
	if (plugin->state < CP_PLUGIN_ACTIVE) {
		return;
	}
	
	// Check for dependency loops
	if (plugin->processed) {
		return;
	}
	plugin->processed = 1;
	
	// Stop the depending plug-ins
	node = list_first(plugin->importing);
	while (node != NULL) {
		stop_plugin_rec(context, lnode_get(node));
		node = list_next(plugin->importing, node);
	}

	// Stop this plug-in
	assert(plugin->state == CP_PLUGIN_ACTIVE);
	stop_plugin_runtime(context, plugin);
	assert(plugin->state < CP_PLUGIN_ACTIVE);
	
	// Clear processed flag
	plugin->processed = 0;
}

static void stop_plugin(cp_context_t *context, cp_plugin_t *plugin) {
	stop_plugin_rec(context, plugin);
	assert_processed_zero(context);
}

CP_C_API cp_status_t cp_stop_plugin(cp_context_t *context, const char *id) {
	hnode_t *node;
	cp_plugin_t *plugin;
	cp_status_t status = CP_OK;

	CHECK_NOT_NULL(context);
	CHECK_NOT_NULL(id);

	// Look up and stop the plug-in 
	cpi_lock_context(context);
	cpi_check_invocation(context, CPI_CF_ANY, __func__);
	node = hash_lookup(context->env->plugins, id);
	if (node != NULL) {
		plugin = hnode_get(node);
		stop_plugin(context, plugin);
	} else {
		cpi_warnf(context, N_("Unknown plug-in %s could not be stopped."), id);
		status = CP_ERR_UNKNOWN;
	}
	cpi_unlock_context(context);

	return status;
}

CP_C_API void cp_stop_plugins(cp_context_t *context) {
	lnode_t *node;
	
	CHECK_NOT_NULL(context);
	
	// Stop the active plug-ins in the reverse order they were started 
	cpi_lock_context(context);
	cpi_check_invocation(context, CPI_CF_ANY, __func__);
	while ((node = list_last(context->env->started_plugins)) != NULL) {
		stop_plugin(context, lnode_get(node));
	}
	cpi_unlock_context(context);
}

static void unresolve_plugin_rec(cp_context_t *context, cp_plugin_t *plugin) {
	lnode_t *node;
	cpi_plugin_event_t event;
	
	// Check if already unresolved
	if (plugin->state < CP_PLUGIN_RESOLVED) {
		return;
	}
	assert(plugin->state == CP_PLUGIN_RESOLVED);
	
	// Clear the list of imported plug-ins (also breaks dependency loops)
	while ((node = list_first(plugin->imported)) != NULL) {
		cp_plugin_t *ip = lnode_get(node);
		
		cpi_ptrset_remove(ip->importing, plugin);
		list_delete(plugin->imported, node);
		lnode_destroy(node);
	}
	assert(list_isempty(plugin->imported));
	list_destroy(plugin->imported);
	plugin->imported = NULL;

	// Unresolve depending plugins
	while ((node = list_first(plugin->importing)) != NULL) {
		unresolve_plugin_rec(context, lnode_get(node));
	}
	
	// Unresolve this plug-in
	unresolve_plugin_runtime(plugin);
	event.plugin_id = plugin->plugin->identifier;
	event.old_state = plugin->state;
	event.new_state = plugin->state = CP_PLUGIN_INSTALLED;
	cpi_deliver_event(context, &event);
}

/**
 * Unresolves a plug-in.
 * 
 * @param context the plug-in context
 * @param plug-in the plug-in to be unresolved
 */
static void unresolve_plugin(cp_context_t *context, cp_plugin_t *plugin) {
	stop_plugin(context, plugin);
	unresolve_plugin_rec(context, plugin);
}

static void free_plugin_import_content(cp_plugin_import_t *import) {
	assert(import != NULL);
	free(import->plugin_id);
	free(import->version);
}

static void free_ext_point_content(cp_ext_point_t *ext_point) {
	free(ext_point->name);
	free(ext_point->local_id);
	free(ext_point->identifier);
	free(ext_point->schema_path);
}

static void free_extension_content(cp_extension_t *extension) {
	free(extension->name);
	free(extension->local_id);
	free(extension->identifier);
	free(extension->ext_point_id);
}

static void free_cfg_element_content(cp_cfg_element_t *ce) {
	unsigned int i;

	assert(ce != NULL);
	free(ce->name);
	if (ce->atts != NULL) {
		free(ce->atts[0]);
		free(ce->atts);
	}
	free(ce->value);
	for (i = 0; i < ce->num_children; i++) {
		free_cfg_element_content(ce->children + i);
	}
	free(ce->children);
}

CP_HIDDEN void cpi_free_plugin(cp_plugin_info_t *plugin) {
	unsigned int i;
	
	assert(plugin != NULL);
	free(plugin->name);
	free(plugin->identifier);
	free(plugin->version);
	free(plugin->provider_name);
	free(plugin->plugin_path);
	free(plugin->abi_bw_compatibility);
	free(plugin->api_bw_compatibility);
	free(plugin->req_cpluff_version);
	for (i = 0; i < plugin->num_imports; i++) {
		free_plugin_import_content(plugin->imports + i);
	}
	free(plugin->imports);
	free(plugin->runtime_lib_name);
	free(plugin->runtime_funcs_symbol);
	for (i = 0; i < plugin->num_ext_points; i++) {
		free_ext_point_content(plugin->ext_points + i);
	}
	free(plugin->ext_points);
	for (i = 0; i < plugin->num_extensions; i++) {
		free_extension_content(plugin->extensions + i);
		if (plugin->extensions[i].configuration != NULL) {
			free_cfg_element_content(plugin->extensions[i].configuration);
			free(plugin->extensions[i].configuration);
		}
	}
	free(plugin->extensions);
	free(plugin);
}

/**
 * Frees any memory allocated for a registered plug-in.
 * 
 * @param context the plug-in context
 * @param plugin the plug-in to be freed
 */
static void free_registered_plugin(cp_context_t *context, cp_plugin_t *plugin) {
	assert(context != NULL);
	assert(plugin != NULL);

	// Release plug-in information
	cpi_release_info(context, plugin->plugin);

	// Release data structures 
	if (plugin->importing != NULL) {
		assert(list_isempty(plugin->importing));
		list_destroy(plugin->importing);
	}
	assert(plugin->imported == NULL);

	free(plugin);
}

/**
 * Uninstalls a plug-in associated with the specified hash node.
 * 
 * @param context the plug-in context
 * @param node the hash node of the plug-in to be uninstalled
 */
static void uninstall_plugin(cp_context_t *context, hnode_t *node) {
	cp_plugin_t *plugin;
	cpi_plugin_event_t event;
	
	// Check if already uninstalled 
	plugin = (cp_plugin_t *) hnode_get(node);
	if (plugin->state <= CP_PLUGIN_UNINSTALLED) {
		// TODO: Is this possible state?
		return;
	}
	
	// Make sure the plug-in is not in resolved state 
	unresolve_plugin(context, plugin);
	assert(plugin->state == CP_PLUGIN_INSTALLED);

	// Plug-in uninstalled 
	event.plugin_id = plugin->plugin->identifier;
	event.old_state = plugin->state;
	event.new_state = plugin->state = CP_PLUGIN_UNINSTALLED;
	cpi_deliver_event(context, &event);
	
	// Unregister extension objects
	unregister_extensions(context, plugin->plugin);

	// Unregister the plug-in 
	hash_delete_free(context->env->plugins, node);

	// Free the plug-in data structures
	free_registered_plugin(context, plugin);
}

CP_C_API cp_status_t cp_uninstall_plugin(cp_context_t *context, const char *id) {
	hnode_t *node;
	cp_status_t status = CP_OK;

	CHECK_NOT_NULL(context);
	CHECK_NOT_NULL(id);

	// Look up and unload the plug-in 
	cpi_lock_context(context);
	cpi_check_invocation(context, CPI_CF_ANY, __func__);
	node = hash_lookup(context->env->plugins, id);
	if (node != NULL) {
		uninstall_plugin(context, node);
	} else {
		cpi_warnf(context, N_("Unknown plug-in %s could not be uninstalled."), id);
		status = CP_ERR_UNKNOWN;
	}
	cpi_unlock_context(context);

	return status;
}

CP_C_API void cp_uninstall_plugins(cp_context_t *context) {
	hscan_t scan;
	hnode_t *node;
	
	CHECK_NOT_NULL(context);
	
	cpi_lock_context(context);
	cpi_check_invocation(context, CPI_CF_ANY, __func__);
	cp_stop_plugins(context);
	while (1) {
		hash_scan_begin(&scan, context->env->plugins);
		if ((node = hash_scan_next(&scan)) != NULL) {
			uninstall_plugin(context, node);
		} else {
			break;
		}
	}
	cpi_unlock_context(context);
}
