/*
 * Purple - Internet Messaging Library
 * Copyright (C) Pidgin Developers <devel@pidgin.im>
 *
 * Purple is the legal property of its developers, whose names are too numerous
 * to list here. Please refer to the COPYRIGHT file distributed with this
 * source distribution.
 *
 * This library 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 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 General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this library; if not, see <https://www.gnu.org/licenses/>.
 */

#include <purpleconfig.h>

#include <glib/gi18n-lib.h>

#include "debug.h"
#include "plugins.h"
#include "purplecore.h"
#include "purpleenums.h"
#include "purplenotification.h"
#include "purplenotificationmanager.h"
#include "purpleplugininfo.h"
#include "request.h"
#include "util.h"
#ifdef _WIN32
#include "win32/win32dep.h"
#endif

#ifdef G_LOG_DOMAIN
# undef G_LOG_DOMAIN
#endif /* G_LOG_DOMAIN */
#define G_LOG_DOMAIN "PurplePlugins"

/**************************************************************************
 * Globals
 **************************************************************************/
static gboolean purple_plugin_is_loaded(GPluginPlugin *plugin);
static gboolean purple_plugin_load(GPluginPlugin *plugin, GError **error);
static PurplePluginInfo *purple_plugin_get_info(GPluginPlugin *plugin);
static GList *purple_plugins_find_all(void);

/**************************************************************************
 * Plugin API
 **************************************************************************/
static gboolean
plugin_loading_cb(G_GNUC_UNUSED GObject *manager, GPluginPlugin *plugin,
                  GError **error, G_GNUC_UNUSED gpointer data)
{
	PurplePluginInfo *info;
	const gchar *info_error = NULL;

	g_return_val_if_fail(GPLUGIN_IS_PLUGIN(plugin), FALSE);

	info = purple_plugin_get_info(plugin);
	if (!info)
		return TRUE; /* a GPlugin internal plugin */

	info_error = purple_plugin_info_get_error(info);
	if(info_error != NULL) {
		gchar *filename = gplugin_plugin_get_filename(plugin);
		g_critical("Failed to load plugin %s: %s", filename, info_error);

		g_set_error(error, PURPLE_PLUGINS_DOMAIN, 0,
				    "Plugin is not loadable: %s", info_error);

		g_free(filename);
		return FALSE;
	}

	return TRUE;
}

static void
plugin_loaded_cb(G_GNUC_UNUSED GObject *manager, GPluginPlugin *plugin)
{
	PurplePluginInfo *info;
	gchar *filename;

	g_return_if_fail(GPLUGIN_IS_PLUGIN(plugin));

	info = purple_plugin_get_info(plugin);
	if (!info)
		return; /* a GPlugin internal plugin */

	filename = gplugin_plugin_get_filename(plugin);

	g_message("Loaded plugin %s", filename);

	g_free(filename);
}

static gboolean
plugin_unloading_cb(G_GNUC_UNUSED GObject *manager, GPluginPlugin *plugin,
                    G_GNUC_UNUSED GError **error, G_GNUC_UNUSED gpointer data)
{
	PurplePluginInfo *info;
	gchar *filename;

	g_return_val_if_fail(GPLUGIN_IS_PLUGIN(plugin), FALSE);

	info = purple_plugin_get_info(plugin);
	if (info) {
		filename = gplugin_plugin_get_filename(plugin);
		g_message("Unloading plugin %s", filename);
		g_free(filename);
	}

	return TRUE;
}

static void
plugin_unloaded_cb(G_GNUC_UNUSED GObject *manager, GPluginPlugin *plugin)
{
	PurplePluginInfo *info;

	g_return_if_fail(GPLUGIN_IS_PLUGIN(plugin));

	info = purple_plugin_get_info(plugin);
	if (!info)
		return; /* a GPlugin internal plugin */

	/* cancel any pending dialogs the plugin has */
	purple_request_close_with_handle(plugin);

	purple_plugin_info_set_unloaded(info, TRUE);
}

static gboolean
purple_plugin_load(GPluginPlugin *plugin, GError **error)
{
	GPluginManager *manager = NULL;
	GError *err = NULL;
	gchar *filename;

	g_return_val_if_fail(GPLUGIN_IS_PLUGIN(plugin), FALSE);

	if (purple_plugin_is_loaded(plugin))
		return TRUE;

	manager = gplugin_manager_get_default();

	if (!gplugin_manager_load_plugin(manager, plugin, &err)) {
		filename = gplugin_plugin_get_filename(plugin);
		g_critical("Failed to load plugin %s: %s", filename,
		           err ? err->message : "Unknown reason");

		g_propagate_error(error, err);

		g_free(filename);
		return FALSE;
	}

	return TRUE;
}

static gboolean
purple_plugin_is_loaded(GPluginPlugin *plugin)
{
	g_return_val_if_fail(GPLUGIN_IS_PLUGIN(plugin), FALSE);

	return (gplugin_plugin_get_state(plugin) == GPLUGIN_PLUGIN_STATE_LOADED);
}

static PurplePluginInfo *
purple_plugin_get_info(GPluginPlugin *plugin)
{
	GPluginPluginInfo *info;

	g_return_val_if_fail(GPLUGIN_IS_PLUGIN(plugin), NULL);

	info = gplugin_plugin_get_info(plugin);

	/* GPlugin refs the plugin info object before returning it. This workaround
	 * is to avoid managing the reference counts everywhere in our codebase
	 * where we use the plugin info. The plugin info instance is guaranteed to
	 * exist as long as the plugin exists. */
	g_object_unref(info);

	if (PURPLE_IS_PLUGIN_INFO(info))
		return PURPLE_PLUGIN_INFO(info);
	else
		return NULL;
}

/**************************************************************************
 * Plugins API
 **************************************************************************/
static GList *
purple_plugins_find_all(void)
{
	GList *ret = NULL, *ids, *l;
	GSList *plugins, *ll;
	GPluginManager *manager = gplugin_manager_get_default();

	ids = gplugin_manager_list_plugins(manager);

	for (l = ids; l; l = l->next) {
		plugins = gplugin_manager_find_plugins(manager, l->data);

		for (ll = plugins; ll; ll = ll->next) {
			GPluginPlugin *plugin = GPLUGIN_PLUGIN(ll->data);
			if (purple_plugin_get_info(plugin))
				ret = g_list_append(ret, plugin);
		}

		g_slist_free_full(plugins, g_object_unref);
	}
	g_list_free(ids);

	return ret;
}

void
purple_plugins_refresh(void)
{
	GList *plugins, *l;
	GPluginManager *manager = gplugin_manager_get_default();

	gplugin_manager_refresh(manager);

	plugins = purple_plugins_find_all();
	for (l = plugins; l != NULL; l = l->next) {
		GPluginPlugin *plugin = GPLUGIN_PLUGIN(l->data);
		PurplePluginInfo *info;
		PurplePluginInfoFlags flags;
		gboolean unloaded;

		if (purple_plugin_is_loaded(plugin))
			continue;

		info = purple_plugin_get_info(plugin);

		unloaded = purple_plugin_info_get_unloaded(info);
		flags = purple_plugin_info_get_flags(info);
		if (!unloaded && flags & PURPLE_PLUGIN_INFO_FLAGS_AUTO_LOAD) {
			gchar *filename = gplugin_plugin_get_filename(plugin);
			g_message("Auto-loading plugin %s", filename);
			purple_plugin_load(plugin, NULL);
			g_free(filename);
		}
	}

	g_list_free(plugins);
}

void
purple_plugins_save_loaded(void) {
	PurpleCore *core = NULL;
	GPluginManager *manager = NULL;
	GSettings *settings = NULL;
	GSList *loaded = NULL;
	GStrv plugins = NULL;
	GStrvBuilder *builder = NULL;

	core = purple_core_get_default();

	builder = g_strv_builder_new();

	manager = gplugin_manager_get_default();
	loaded = gplugin_manager_find_plugins_with_state(manager,
	                                                 GPLUGIN_PLUGIN_STATE_LOADED);
	while(loaded != NULL) {
		GPluginPlugin *plugin = GPLUGIN_PLUGIN(loaded->data);
		PurplePluginInfo *info = purple_plugin_get_info(plugin);

		if(PURPLE_IS_PLUGIN_INFO(info)) {
			PurplePluginInfoFlags flags = purple_plugin_info_get_flags(info);
			if((flags & PURPLE_PLUGIN_INFO_FLAGS_AUTO_LOAD) == 0) {
				g_strv_builder_add(builder,
				                   gplugin_plugin_info_get_id(GPLUGIN_PLUGIN_INFO(info)));
			}
		}

		g_clear_object(&plugin);

		loaded = g_slist_delete_link(loaded, loaded);
	}

	settings = purple_core_new_settings(core, "im.pidgin.Purple.Core");

	plugins = g_strv_builder_end(builder);
	g_strv_builder_unref(builder);

	g_settings_set_strv(settings, "saved-plugins", (const char **)plugins);
	g_strfreev(plugins);

	g_clear_object(&settings);
}

void
purple_plugins_load_saved(void){
	PurpleCore *core = NULL;
	GPluginManager *manager = NULL;
	GSettings *settings = NULL;
	GStrv plugins = NULL;

	core = purple_core_get_default();

	manager = gplugin_manager_get_default();

	settings = purple_core_new_settings(core, "im.pidgin.Purple.Core");
	plugins = g_settings_get_strv(settings, "saved-plugins");

	for(int i = 0; plugins[i] != NULL; i++) {
		GPluginPlugin *plugin = NULL;
		GError *error = NULL;
		const char *plugin_id = plugins[i];

		plugin = gplugin_manager_find_plugin_with_newest_version(manager,
		                                                         plugin_id);

		if(GPLUGIN_IS_PLUGIN(plugin)) {
			g_message("loading saved plugin %s from %s",
			          plugin_id,
			          gplugin_plugin_get_filename(plugin));
			gplugin_manager_load_plugin(manager, plugin, &error);
		} else {
			error = g_error_new(PURPLE_PLUGINS_DOMAIN, 0,
			                    _("Unable to find saved plugin %s"),
			                    plugin_id);
			g_warning("Unable to find saved plugin %s", plugin_id);
		}

		if(error != NULL) {
			PurpleNotification *notification = NULL;
			PurpleNotificationManager *manager = NULL;
			char *title = NULL;
			const char *subtitle = "Unknown error";

			title = g_strdup_printf(_("Failed to load saved plugin %s"),
			                        plugin_id);

			if(error->message != NULL) {
				subtitle = error->message;
			}

			notification = purple_notification_new(NULL, title);
			purple_notification_set_subtitle(notification, subtitle);
			purple_notification_set_icon_name(notification,
			                                  "dialog-error-symbolic");

			manager = purple_core_get_notification_manager(core);
			purple_notification_manager_add(manager, notification);
			g_clear_object(&notification);

			g_free(title);
			g_clear_error(&error);
		}
	}

	g_strfreev(plugins);
}

/**************************************************************************
 * Plugins Subsystem API
 **************************************************************************/
void
purple_plugins_init(void)
{
	GPluginManager *manager = NULL;
	gboolean skip_default = FALSE;

	gplugin_init(GPLUGIN_CORE_FLAGS_NONE);

	manager = gplugin_manager_get_default();

	gplugin_manager_append_paths_from_environment(manager,
	                                              "PURPLE_PLUGIN_PATH");

	/* Don't add the default paths if we're being run from a unit test. */
	if(!birb_str_is_empty(g_getenv("MESON_TEST_ITERATION"))) {
		skip_default = TRUE;
	}

	if(!birb_str_is_empty(g_getenv("PURPLE_PLUGINS_SKIP"))) {
		skip_default = TRUE;
	}

	if(!skip_default) {
		gplugin_manager_add_default_paths(manager);
		gplugin_manager_append_path(manager, PURPLE_LIBDIR);
	} else {
		g_message("skipping normal plugin paths due to environment variable.");
	}

	g_signal_connect(manager, "loading-plugin",
	                 G_CALLBACK(plugin_loading_cb), NULL);
	g_signal_connect(manager, "loaded-plugin",
	                 G_CALLBACK(plugin_loaded_cb), NULL);
	g_signal_connect(manager, "unloading-plugin",
	                 G_CALLBACK(plugin_unloading_cb), NULL);
	g_signal_connect(manager, "unloaded-plugin",
	                 G_CALLBACK(plugin_unloaded_cb), NULL);

	purple_plugins_refresh();
}

void
purple_plugins_uninit(void)
{
	gplugin_uninit();
}
