/**
 * @file core.c Purple Core API
 * @ingroup core
 *
 * purple
 *
 * 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 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 of the License, 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
 */
#include "internal.h"
#include "cipher.h"
#include "connection.h"
#include "conversation.h"
#include "core.h"
#include "debug.h"
#include "dnsquery.h"
#include "ft.h"
#include "idle.h"
#include "imgstore.h"
#include "network.h"
#include "notify.h"
#include "plugin.h"
#include "pounce.h"
#include "prefs.h"
#include "privacy.h"
#include "proxy.h"
#include "savedstatuses.h"
#include "signals.h"
#include "sound.h"
#include "sslconn.h"
#include "status.h"
#include "stun.h"
#include "util.h"

#ifdef HAVE_DBUS
#  define DBUS_API_SUBJECT_TO_CHANGE
#  include <dbus/dbus.h>
#  include "dbus-purple.h"
#  include "dbus-server.h"
#  include "dbus-bindings.h"
#endif

struct PurpleCore
{
	char *ui;

	void *reserved;
};

static PurpleCoreUiOps *_ops  = NULL;
static PurpleCore      *_core = NULL;

STATIC_PROTO_INIT

gboolean
purple_core_init(const char *ui)
{
	PurpleCoreUiOps *ops;
	PurpleCore *core;

	g_return_val_if_fail(ui != NULL, FALSE);
	g_return_val_if_fail(purple_get_core() == NULL, FALSE);

#ifdef ENABLE_NLS
	bindtextdomain(PACKAGE, LOCALEDIR);
#endif
#ifdef _WIN32
	wpurple_init();
#endif

	_core = core = g_new0(PurpleCore, 1);
	core->ui = g_strdup(ui);
	core->reserved = NULL;

	ops = purple_core_get_ui_ops();

	/* The signals subsystem is important and should be first. */
	purple_signals_init();

	purple_signal_register(core, "uri-handler",
		purple_marshal_BOOLEAN__POINTER_POINTER_POINTER,
		purple_value_new(PURPLE_TYPE_BOOLEAN), 3,
		purple_value_new(PURPLE_TYPE_STRING), /* Protocol */
		purple_value_new(PURPLE_TYPE_STRING), /* Command */
		purple_value_new(PURPLE_TYPE_BOXED, "GHashTable *")); /* Parameters */

	purple_signal_register(core, "quitting", purple_marshal_VOID, NULL, 0);

	/* The prefs subsystem needs to be initialized before static protocols
	 * for protocol prefs to work. */
	purple_prefs_init();

	purple_debug_init();

	if (ops != NULL)
	{
		if (ops->ui_prefs_init != NULL)
			ops->ui_prefs_init();

		if (ops->debug_ui_init != NULL)
			ops->debug_ui_init();
	}

#ifdef HAVE_DBUS
	purple_dbus_init();
#endif

	purple_ciphers_init();

	/* Initialize all static protocols. */
	static_proto_init();

	/* Since plugins get probed so early we should probably initialize their
	 * subsystem right away too.
	 */
	purple_plugins_init();
	purple_plugins_probe(G_MODULE_SUFFIX);

	/* The buddy icon code uses the imgstore, so init it early. */
	purple_imgstore_init();

	/* Accounts use status and buddy icons, so initialize these before accounts */
	purple_status_init();
	purple_buddy_icons_init();

	purple_accounts_init();
	purple_savedstatuses_init();
	purple_notify_init();
	purple_connections_init();
	purple_conversations_init();
	purple_blist_init();
	purple_log_init();
	purple_network_init();
	purple_privacy_init();
	purple_pounces_init();
	purple_proxy_init();
	purple_dnsquery_init();
	purple_sound_init();
	purple_ssl_init();
	purple_stun_init();
	purple_xfers_init();
	purple_idle_init();

	/*
	 * Call this early on to try to auto-detect our IP address and
	 * hopefully save some time later.
	 */
	purple_network_get_my_ip(-1);

	if (ops != NULL && ops->ui_init != NULL)
		ops->ui_init();

	return TRUE;
}

void
purple_core_quit(void)
{
	PurpleCoreUiOps *ops;
	PurpleCore *core = purple_get_core();

	g_return_if_fail(core != NULL);

	/* The self destruct sequence has been initiated */
	purple_signal_emit(purple_get_core(), "quitting");

	/* Transmission ends */
	purple_connections_disconnect_all();

	/* Save .xml files, remove signals, etc. */
	purple_idle_uninit();
	purple_ssl_uninit();
	purple_pounces_uninit();
	purple_blist_uninit();
	purple_ciphers_uninit();
	purple_notify_uninit();
	purple_conversations_uninit();
	purple_connections_uninit();
	purple_buddy_icons_uninit();
	purple_accounts_uninit();
	purple_savedstatuses_uninit();
	purple_status_uninit();
	purple_prefs_uninit();
	purple_xfers_uninit();
	purple_proxy_uninit();
	purple_dnsquery_uninit();
	purple_imgstore_uninit();

	purple_debug_info("main", "Unloading all plugins\n");
	purple_plugins_destroy_all();

	ops = purple_core_get_ui_ops();
	if (ops != NULL && ops->quit != NULL)
		ops->quit();

	/*
	 * purple_sound_uninit() should be called as close to
	 * shutdown as possible.  This is because the call
	 * to ao_shutdown() can sometimes leave our
	 * environment variables in an unusable state, which
	 * can cause a crash when getenv is called (by gettext
	 * for example).  See the complete bug report at
	 * http://trac.xiph.org/cgi-bin/trac.cgi/ticket/701
	 *
	 * TODO: Eventually move this call higher up with the others.
	 */
	purple_sound_uninit();

	purple_plugins_uninit();
	purple_signals_uninit();

#ifdef HAVE_DBUS
	purple_dbus_uninit();
#endif

	g_free(core->ui);
	g_free(core);

#ifdef _WIN32
	wpurple_cleanup();
#endif

	_core = NULL;
}

gboolean
purple_core_quit_cb(gpointer unused)
{
	purple_core_quit();

	return FALSE;
}

const char *
purple_core_get_version(void)
{
	return VERSION;
}

const char *
purple_core_get_ui(void)
{
	PurpleCore *core = purple_get_core();

	g_return_val_if_fail(core != NULL, NULL);

	return core->ui;
}

PurpleCore *
purple_get_core(void)
{
	return _core;
}

void
purple_core_set_ui_ops(PurpleCoreUiOps *ops)
{
	_ops = ops;
}

PurpleCoreUiOps *
purple_core_get_ui_ops(void)
{
	return _ops;
}

#ifdef HAVE_DBUS
static char *purple_dbus_owner_user_dir(void)
{
	DBusMessage *msg = NULL, *reply = NULL;
	DBusConnection *dbus_connection = NULL;
	DBusError dbus_error;
	char *remote_user_dir = NULL;

	if ((dbus_connection = purple_dbus_get_connection()) == NULL)
		return NULL;

	if ((msg = dbus_message_new_method_call(DBUS_SERVICE_PURPLE, DBUS_PATH_PURPLE, DBUS_INTERFACE_PURPLE, "PurpleUserDir")) == NULL)
		return NULL;

	dbus_error_init(&dbus_error);
	reply = dbus_connection_send_with_reply_and_block(dbus_connection, msg, 5000, &dbus_error);
	dbus_message_unref(msg);
	dbus_error_free(&dbus_error);

	if (reply)
	{
		dbus_error_init(&dbus_error);
		dbus_message_get_args(reply, &dbus_error, DBUS_TYPE_STRING, &remote_user_dir, DBUS_TYPE_INVALID);
		remote_user_dir = g_strdup(remote_user_dir);
		dbus_error_free(&dbus_error);
		dbus_message_unref(reply);
	}

	return remote_user_dir;
}

static void purple_dbus_owner_show_buddy_list(void)
{
	DBusError dbus_error;
	DBusMessage *msg = NULL, *reply = NULL;
	DBusConnection *dbus_connection = NULL;

	if ((dbus_connection = purple_dbus_get_connection()) == NULL)
		return;

	if ((msg = dbus_message_new_method_call(DBUS_SERVICE_PURPLE, DBUS_PATH_PURPLE, DBUS_INTERFACE_PURPLE, "PurpleBlistShow")) == NULL)
		return;

	dbus_error_init(&dbus_error);
	if ((reply = dbus_connection_send_with_reply_and_block(dbus_connection, msg, 5000, &dbus_error)) != NULL)
	{
		dbus_message_unref(msg);
	}
	dbus_error_free(&dbus_error);
}
#endif /* HAVE_DBUS */

gboolean
purple_core_ensure_single_instance()
{
	gboolean is_single_instance = TRUE;
#ifdef HAVE_DBUS
	/* in the future, other mechanisms might have already set this to FALSE */
	if (is_single_instance)
	{
		if (!purple_dbus_is_owner())
		{
			const char *user_dir = purple_user_dir();
			char *dbus_owner_user_dir = purple_dbus_owner_user_dir();

			if (NULL == user_dir && NULL != dbus_owner_user_dir)
				is_single_instance = TRUE;
			else if (NULL != user_dir && NULL == dbus_owner_user_dir)
				is_single_instance = TRUE;
			else if (NULL == user_dir && NULL == dbus_owner_user_dir)
				is_single_instance = FALSE;
			else
				is_single_instance = strcmp(dbus_owner_user_dir, user_dir);

			if (!is_single_instance)
				purple_dbus_owner_show_buddy_list();

			g_free(dbus_owner_user_dir);
		}
	}
#endif /* HAVE_DBUS */

	return is_single_instance;
}

static gboolean
move_and_symlink_dir(const char *path, const char *basename, const char *old_base, const char *new_base, const char *relative)
{
	char *new_name = g_build_filename(new_base, basename, NULL);
#ifndef _WIN32
	char *old_name;
#endif
	if (g_rename(path, new_name))
	{
		purple_debug_error("core", "Error renaming %s to %s: %s. Please report this at http://developer.pidgin.im\n",
		                   path, new_name, strerror(errno));
		g_free(new_name);
		return FALSE;
	}
	g_free(new_name);

#ifndef _WIN32
	/* NOTE: This new_name is relative. */
	new_name = g_build_filename(relative, basename, NULL);
	old_name = g_build_filename(old_base, basename, NULL);
	if (symlink(new_name, old_name))
	{
		purple_debug_warning("core", "Error symlinking %s to %s: %s. Please report this at http://developer.pidgin.im\n",
		                     old_name, new_name, strerror(errno));
	}
	g_free(old_name);
	g_free(new_name);
#endif

	return TRUE;
}

gboolean
purple_core_migrate(void)
{
	const char *user_dir = purple_user_dir();
	char *old_user_dir = g_strconcat(purple_home_dir(),
	                                 G_DIR_SEPARATOR_S ".gaim", NULL);
	char *status_file;
	FILE *fp;
	GDir *dir;
	GError *err;
	const char *entry;
#ifndef _WIN32
	char *logs_dir;
#endif
	char *old_icons_dir;

	if (!g_file_test(old_user_dir, G_FILE_TEST_EXISTS))
	{
		/* ~/.gaim doesn't exist, so there's nothing to migrate. */
		g_free(old_user_dir);
		return TRUE;
	}

	status_file = g_strconcat(user_dir, G_DIR_SEPARATOR_S "migrating", NULL);

	if (g_file_test(user_dir, G_FILE_TEST_EXISTS))
	{
		/* If we're here, we have both ~/.gaim and .purple. */

		if (!g_file_test(status_file, G_FILE_TEST_EXISTS))
		{
			/* There's no "migrating" status file,
			 * so ~/.purple is all up to date. */
			g_free(status_file);
			g_free(old_user_dir);
			return TRUE;
		}
	}

	/* If we're here, it's time to migrate from ~/.gaim to ~/.purple. */

        /* Ensure the user directory exists */
	if (!g_file_test(user_dir, G_FILE_TEST_IS_DIR))
	{
		if (g_mkdir(user_dir, S_IRUSR | S_IWUSR | S_IXUSR) == -1)
		{
			purple_debug_error("core", "Error creating directory %s: %s. Please report this at http://developer.pidgin.im\n",
			                   user_dir, strerror(errno));
			g_free(status_file);
			g_free(old_user_dir);
			return FALSE;
		}
	}

	/* This writes ~/.purple/migrating, which allows us to detect
	 * incomplete migrations and properly retry. */
	if (!(fp = g_fopen(status_file, "w")))
	{
		purple_debug_error("core", "Error opening file %s for writing: %s. Please report this at http://developer.pidgin.im\n",
		                   status_file, strerror(errno));
		g_free(status_file);
		g_free(old_user_dir);
		return FALSE;
	}
	fclose(fp);

	/* Open ~/.gaim so we can loop over its contents. */
	err = NULL;
	if (!(dir = g_dir_open(old_user_dir, 0, &err)))
	{
		purple_debug_error("core", "Error opening directory %s: %s. Please report this at http://developer.pidgin.im\n",
		                   status_file,
		                   (err ? err->message : "Unknown error"));
		if (err)
			g_error_free(err);
		g_free(status_file);
		g_free(old_user_dir);
		return FALSE;
	}

	/* Loop over the contents of ~/.gaim */
	while ((entry = g_dir_read_name(dir)))
	{
		char *name = g_build_filename(old_user_dir, entry, NULL);

#ifndef _WIN32
		/* Deal with symlinks... */
		if (g_file_test(name, G_FILE_TEST_IS_SYMLINK))
		{
			/* We're only going to duplicate a logs symlink. */
			if (!strcmp(entry, "logs"))
			{
				char *link;
#if GLIB_CHECK_VERSION(2,4,0)
				GError *err = NULL;

				if ((link = g_file_read_link(name, &err)) == NULL)
				{
					char *name_utf8 = g_filename_to_utf8(name, -1, NULL, NULL, NULL);
					purple_debug_error("core", "Error reading symlink %s: %s. Please report this at http://developer.pidgin.im\n",
					                   name_utf8 ? name_utf8 : name, err->message);
					g_free(name_utf8);
					g_error_free(err);
					g_free(name);
					g_dir_close(dir);
					g_free(status_file);
					g_free(old_user_dir);
					return FALSE;
				}
#else
				char buf[MAXPATHLEN];
				size_t linklen;

				if ((linklen = readlink(name, buf, sizeof(buf) - 1) == -1))
				{
					char *name_utf8 = g_filename_to_utf8(name, -1, NULL, NULL, NULL);
					purple_debug_error("core", "Error reading symlink %s: %s. Please report this at http://developer.pidgin.im\n",
					                   name_utf8, strerror(errno));
					g_free(name_utf8);
					g_free(name);
					g_dir_close(dir);
					g_free(status_file);
					g_free(old_user_dir);
					return FALSE;
				}
				buf[linklen] = '\0';

				/* This way we don't have to GLIB_VERSION_CHECK every g_free(link) below. */
				link = g_strdup(buf);
#endif

				logs_dir = g_build_filename(user_dir, "logs", NULL);

				if (!strcmp(link, "../.purple/logs") || !strcmp(link, logs_dir))
				{
					/* If the symlink points to the new directory, we're
					 * likely just trying again after a failed migration,
					 * so there's no need to fail here. */
					g_free(link);
					g_free(logs_dir);
					continue;
				}

				/* In case we are trying again after a failed migration, we need
				 * to unlink any existing symlink.  If it's a directory, this
				 * will fail, and so will the symlink below, which is good
				 * because the user should sort things out. */
				g_unlink(logs_dir);

				/* Relative links will most likely still be
				 * valid from ~/.purple, though it's not
				 * guaranteed.  Oh well. */
				if (symlink(link, logs_dir))
				{
					purple_debug_error("core", "Error symlinking %s to %s: %s. Please report this at http://developer.pidgin.im\n",
					                   logs_dir, link, strerror(errno));
					g_free(link);
					g_free(name);
					g_free(logs_dir);
					g_dir_close(dir);
					g_free(status_file);
					g_free(old_user_dir);
					return FALSE;
				}

				g_free(link);
				g_free(logs_dir);
				continue;
			}

			/* Ignore all other symlinks. */
			continue;
		}
#endif

		/* Deal with directories... */
		if (g_file_test(name, G_FILE_TEST_IS_DIR))
		{
			if (!strcmp(entry, "icons"))
			{
				/* This is a special case for the Album plugin, which
				 * stores data in the icons folder.  We're not copying
				 * the icons directory over because previous bugs
				 * meant that it filled up with junk for many users.
				 * This is a great time to purge it. */

				GDir *icons_dir;
				char *new_icons_dir;
				const char *icons_entry;

				err = NULL;
				if (!(icons_dir = g_dir_open(name, 0, &err)))
				{
					purple_debug_error("core", "Error opening directory %s: %s. Please report this at http://developer.pidgin.im\n",
					                   name,
					                   (err ? err->message : "Unknown error"));
					if (err)
						g_error_free(err);
					g_free(name);
					g_dir_close(dir);
					g_free(status_file);
					g_free(old_user_dir);
					return FALSE;
				}

				new_icons_dir = g_build_filename(user_dir, "icons", NULL);
			        /* Ensure the new icon directory exists */
				if (!g_file_test(new_icons_dir, G_FILE_TEST_IS_DIR))
				{
					if (g_mkdir(new_icons_dir, S_IRUSR | S_IWUSR | S_IXUSR) == -1)
					{
						purple_debug_error("core", "Error creating directory %s: %s. Please report this at http://developer.pidgin.im\n",
						                   new_icons_dir, strerror(errno));
						g_free(new_icons_dir);
						g_dir_close(icons_dir);
						g_free(name);
						g_dir_close(dir);
						g_free(status_file);
						g_free(old_user_dir);
						return FALSE;
					}
				}

				while ((icons_entry = g_dir_read_name(icons_dir)))
				{
					char *icons_name = g_build_filename(name, icons_entry, NULL);

					if (g_file_test(icons_name, G_FILE_TEST_IS_DIR))
					{
						if (!move_and_symlink_dir(icons_name, icons_entry,
						                          name, new_icons_dir, "../../.purple/icons"))
						{
							g_free(icons_name);
							g_free(new_icons_dir);
							g_dir_close(icons_dir);
							g_free(name);
							g_dir_close(dir);
							g_free(status_file);
							g_free(old_user_dir);
							return FALSE;
						}
					}
					g_free(icons_name);
				}

				g_dir_close(icons_dir);
			}
			else if (!strcmp(entry, "plugins"))
			{
				/* Do nothing, because we broke plugin compatibility.
				 * This means that the plugins directory gets left behind. */
			}
			else
			{
				/* All other directories are moved and symlinked. */
				if (!move_and_symlink_dir(name, entry, old_user_dir, user_dir, "../.purple"))
				{
					g_free(name);
					g_dir_close(dir);
					g_free(status_file);
					g_free(old_user_dir);
					return FALSE;
				}
			}
		}
		else if (g_file_test(name, G_FILE_TEST_IS_REGULAR))
		{
			/* Regular files are copied. */

			char *new_name;
			FILE *new_file;

			if (!(fp = g_fopen(name, "rb")))
			{
				purple_debug_error("core", "Error opening file %s for reading: %s. Please report this at http://developer.pidgin.im\n",
				                   name, strerror(errno));
				g_free(name);
				g_dir_close(dir);
				g_free(status_file);
				g_free(old_user_dir);
				return FALSE;
			}

			new_name = g_build_filename(user_dir, entry, NULL);
			if (!(new_file = g_fopen(new_name, "wb")))
			{
				purple_debug_error("core", "Error opening file %s for writing: %s. Please report this at http://developer.pidgin.im\n",
				                   new_name, strerror(errno));
				fclose(fp);
				g_free(new_name);
				g_free(name);
				g_dir_close(dir);
				g_free(status_file);
				g_free(old_user_dir);
				return FALSE;
			}

			while (!feof(fp))
			{
				unsigned char buf[256];
				size_t size;

				size = fread(buf, 1, sizeof(buf), fp);
				if (size != sizeof(buf) && !feof(fp))
				{
					purple_debug_error("core", "Error reading %s: %s. Please report this at http://developer.pidgin.im\n",
					                   name, strerror(errno));
					fclose(new_file);
					fclose(fp);
					g_free(new_name);
					g_free(name);
					g_dir_close(dir);
					g_free(status_file);
					g_free(old_user_dir);
					return FALSE;
				}

				if (!fwrite(buf, size, 1, new_file) && ferror(new_file) != 0)
				{
					purple_debug_error("core", "Error writing %s: %s. Please report this at http://developer.pidgin.im\n",
					                   new_name, strerror(errno));
					fclose(new_file);
					fclose(fp);
					g_free(new_name);
					g_free(name);
					g_dir_close(dir);
					g_free(status_file);
					g_free(old_user_dir);
					return FALSE;
				}
			}

			if (fclose(new_file))
			{
				purple_debug_error("core", "Error writing: %s: %s. Please report this at http://developer.pidgin.im\n",
				                   new_name, strerror(errno));
			}
			if (fclose(fp))
			{
				purple_debug_warning("core", "Error closing %s: %s\n",
				                     name, strerror(errno));
			}
			g_free(new_name);
		}
		else
			purple_debug_warning("core", "Not a regular file or directory: %s\n", name);

		g_free(name);
	}

	/* The migration was successful, so delete the status file. */
	if (g_unlink(status_file))
	{
		purple_debug_error("core", "Error unlinking file %s: %s. Please report this at http://developer.pidgin.im\n",
		                   status_file, strerror(errno));
		g_free(status_file);
		return FALSE;
	}

	old_icons_dir = g_build_filename(old_user_dir, "icons", NULL);
	_purple_buddy_icon_set_old_icons_dir(old_icons_dir);
	g_free(old_icons_dir);

	g_free(old_user_dir);

	g_free(status_file);
	return TRUE;
}
