/*
 * 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 <glib/gi18n-lib.h>

#include "purpleconnection.h"

#include "debug.h"
#include "prefs.h"
#include "proxy.h"
#include "purpleaccount.h"
#include "purpleenums.h"
#include "request.h"
#include "util.h"

typedef struct {
	GObject gparent;

	char *id;

	GCancellable *cancellable;

	PurpleProtocol *protocol;     /* The protocol.                     */

	PurpleAccount *account;       /* The account being connected to.   */
	char *password;               /* The password used.                */
} PurpleConnectionPrivate;

enum {
	PROP_0,
	PROP_ID,
	PROP_CANCELLABLE,
	PROP_PROTOCOL,
	PROP_ACCOUNT,
	PROP_PASSWORD,
	N_PROPERTIES,
};

static GParamSpec *properties[N_PROPERTIES] = {NULL, };

G_DEFINE_TYPE_WITH_PRIVATE(PurpleConnection, purple_connection, G_TYPE_OBJECT)

/**************************************************************************
 * Connection API
 **************************************************************************/

/*
 * d:)->-<
 *
 * d:O-\-<
 *
 * d:D-/-<
 *
 * d8D->-< DANCE!
 */

PurpleAccount *
purple_connection_get_account(PurpleConnection *connection) {
	PurpleConnectionPrivate *priv = NULL;

	g_return_val_if_fail(PURPLE_IS_CONNECTION(connection), NULL);

	priv = purple_connection_get_instance_private(connection);

	return priv->account;
}

const char *
purple_connection_get_id(PurpleConnection *connection) {
	PurpleConnectionPrivate *priv = NULL;

	g_return_val_if_fail(PURPLE_IS_CONNECTION(connection), NULL);

	priv = purple_connection_get_instance_private(connection);

	return priv->id;
}

PurpleProtocol *
purple_connection_get_protocol(PurpleConnection *connection) {
	PurpleConnectionPrivate *priv = NULL;

	g_return_val_if_fail(PURPLE_IS_CONNECTION(connection), NULL);

	priv = purple_connection_get_instance_private(connection);

	return priv->protocol;
}

const char *
purple_connection_get_password(PurpleConnection *connection) {
	PurpleConnectionPrivate *priv = NULL;

	g_return_val_if_fail(PURPLE_IS_CONNECTION(connection), NULL);

	priv = purple_connection_get_instance_private(connection);

	return priv->password;
}

void
purple_connection_set_password(PurpleConnection *connection,
                               const char *password)
{
	PurpleConnectionPrivate *priv = NULL;

	g_return_if_fail(PURPLE_IS_CONNECTION(connection));

	priv = purple_connection_get_instance_private(connection);

	purple_str_wipe(priv->password);
	priv->password = g_strdup(password);

	g_object_notify_by_pspec(G_OBJECT(connection), properties[PROP_PASSWORD]);
}

gboolean
purple_connection_error_is_fatal(PurpleConnectionError reason) {
	switch (reason) {
	case PURPLE_CONNECTION_ERROR_NETWORK_ERROR:
	case PURPLE_CONNECTION_ERROR_ENCRYPTION_ERROR:
	case PURPLE_CONNECTION_ERROR_CUSTOM_TEMPORARY:
		return FALSE;
	case PURPLE_CONNECTION_ERROR_INVALID_USERNAME:
	case PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED:
	case PURPLE_CONNECTION_ERROR_AUTHENTICATION_IMPOSSIBLE:
	case PURPLE_CONNECTION_ERROR_NO_SSL_SUPPORT:
	case PURPLE_CONNECTION_ERROR_NAME_IN_USE:
	case PURPLE_CONNECTION_ERROR_INVALID_SETTINGS:
	case PURPLE_CONNECTION_ERROR_CERT_NOT_PROVIDED:
	case PURPLE_CONNECTION_ERROR_CERT_UNTRUSTED:
	case PURPLE_CONNECTION_ERROR_CERT_EXPIRED:
	case PURPLE_CONNECTION_ERROR_CERT_NOT_ACTIVATED:
	case PURPLE_CONNECTION_ERROR_CERT_HOSTNAME_MISMATCH:
	case PURPLE_CONNECTION_ERROR_CERT_FINGERPRINT_MISMATCH:
	case PURPLE_CONNECTION_ERROR_CERT_SELF_SIGNED:
	case PURPLE_CONNECTION_ERROR_CERT_OTHER_ERROR:
	case PURPLE_CONNECTION_ERROR_CUSTOM_FATAL:
	case PURPLE_CONNECTION_ERROR_OTHER_ERROR:
		return TRUE;
	default:
		g_return_val_if_reached(TRUE);
	}
}

/**************************************************************************
 * Helpers
 **************************************************************************/
static void
purple_connection_set_id(PurpleConnection *connection, const char *id) {
	PurpleConnectionPrivate *priv = NULL;

	priv = purple_connection_get_instance_private(connection);

	if(g_set_str(&priv->id, id)) {
		g_object_notify_by_pspec(G_OBJECT(connection), properties[PROP_ID]);
	}
}

static void
purple_connection_set_account(PurpleConnection *connection,
                              PurpleAccount *account)
{
	PurpleConnectionPrivate *priv = NULL;

	priv = purple_connection_get_instance_private(connection);

	if(g_set_object(&priv->account, account)) {
		g_object_notify_by_pspec(G_OBJECT(connection),
		                         properties[PROP_ACCOUNT]);
	}
}

/**************************************************************************
 * GObject Implementation
 **************************************************************************/
static void
purple_connection_set_property(GObject *obj, guint param_id,
                               const GValue *value, GParamSpec *pspec)
{
	PurpleConnection *connection = PURPLE_CONNECTION(obj);
	PurpleConnectionPrivate *priv = NULL;

	priv = purple_connection_get_instance_private(connection);

	switch (param_id) {
	case PROP_ID:
		purple_connection_set_id(connection, g_value_get_string(value));
		break;
	case PROP_PROTOCOL:
		priv->protocol = g_value_get_object(value);
		break;
	case PROP_ACCOUNT:
		purple_connection_set_account(connection, g_value_get_object(value));
		break;
	case PROP_PASSWORD:
		purple_connection_set_password(connection, g_value_get_string(value));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
		break;
	}
}

static void
purple_connection_get_property(GObject *obj, guint param_id, GValue *value,
                               GParamSpec *pspec)
{
	PurpleConnection *connection = PURPLE_CONNECTION(obj);

	switch (param_id) {
	case PROP_ID:
		g_value_set_string(value, purple_connection_get_id(connection));
		break;
	case PROP_CANCELLABLE:
		g_value_set_object(value,
		                   purple_connection_get_cancellable(connection));
		break;
	case PROP_PROTOCOL:
		g_value_set_object(value, purple_connection_get_protocol(connection));
		break;
	case PROP_ACCOUNT:
		g_value_set_object(value, purple_connection_get_account(connection));
		break;
	case PROP_PASSWORD:
		g_value_set_string(value, purple_connection_get_password(connection));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
		break;
	}
}

static void
purple_connection_init(PurpleConnection *connection) {
	PurpleConnectionPrivate *priv = NULL;

	priv = purple_connection_get_instance_private(connection);

	priv->cancellable = g_cancellable_new();
}

static void
purple_connection_constructed(GObject *object) {
	PurpleConnection *connection = PURPLE_CONNECTION(object);
	PurpleConnectionPrivate *priv = NULL;

	G_OBJECT_CLASS(purple_connection_parent_class)->constructed(object);

	priv = purple_connection_get_instance_private(connection);

	if(priv->id == NULL) {
		char *uuid = g_uuid_string_random();

		purple_connection_set_id(connection, uuid);

		g_free(uuid);
	}
}

static void
purple_connection_dispose(GObject *obj) {
	PurpleConnection *connection = PURPLE_CONNECTION(obj);
	PurpleConnectionPrivate *priv = NULL;

	priv = purple_connection_get_instance_private(connection);

	g_clear_object(&priv->account);

	G_OBJECT_CLASS(purple_connection_parent_class)->dispose(obj);
}

static void
purple_connection_finalize(GObject *object) {
	PurpleConnection *connection = PURPLE_CONNECTION(object);
	PurpleConnectionPrivate *priv = NULL;

	priv = purple_connection_get_instance_private(connection);

	purple_str_wipe(priv->password);
	g_free(priv->id);

	g_clear_object(&priv->cancellable);

	G_OBJECT_CLASS(purple_connection_parent_class)->finalize(object);
}

static void
purple_connection_class_init(PurpleConnectionClass *klass) {
	GObjectClass *obj_class = G_OBJECT_CLASS(klass);

	obj_class->get_property = purple_connection_get_property;
	obj_class->set_property = purple_connection_set_property;
	obj_class->dispose = purple_connection_dispose;
	obj_class->finalize = purple_connection_finalize;
	obj_class->constructed = purple_connection_constructed;

	/**
	 * PurpleConnection:id:
	 *
	 * The unique identifier for the connection.
	 *
	 * Since: 3.0
	 */
	properties[PROP_ID] = g_param_spec_string(
		"id", NULL, NULL,
		NULL,
		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConnection:cancellable:
	 *
	 * A [class@Gio.Cancellable] to be used with the connection.
	 *
	 * This can be passed function that require a cancellable for the
	 * connection.
	 *
	 * Since: 3.0
	 */
	properties[PROP_CANCELLABLE] = g_param_spec_object(
		"cancellable", NULL, NULL,
		G_TYPE_CANCELLABLE,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConnection:protocol:
	 *
	 * The protocol that this connection is for.
	 *
	 * Since: 3.0
	 */
	properties[PROP_PROTOCOL] = g_param_spec_object(
		"protocol", NULL, NULL,
		PURPLE_TYPE_PROTOCOL,
		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConnection:account:
	 *
	 * The account this connection belongs to.
	 *
	 * Since: 3.0
	 */
	properties[PROP_ACCOUNT] = g_param_spec_object(
		"account", NULL, NULL,
		PURPLE_TYPE_ACCOUNT,
		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConnection:password:
	 *
	 * The password for this connection.
	 *
	 * This is only stored for reconnections and may go away in the future.
	 *
	 * Since: 3.0
	 */
	properties[PROP_PASSWORD] = g_param_spec_string(
		"password", NULL, NULL,
		NULL,
		G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);

	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
}

gboolean
purple_connection_connect(PurpleConnection *connection, GError **error) {
	PurpleConnectionClass *klass = NULL;
	PurpleConnectionPrivate *priv = NULL;

	g_return_val_if_fail(PURPLE_IS_CONNECTION(connection), FALSE);

	priv = purple_connection_get_instance_private(connection);

	if(!purple_account_is_connecting(priv->account)) {
		g_set_error(error, PURPLE_CONNECTION_ERROR, 0,
		            "account %s is not in a connecting state",
		            purple_account_get_username(priv->account));

		return TRUE;
	}

	if(((priv->password == NULL) || (*priv->password == '\0')) &&
	   !(purple_protocol_get_options(priv->protocol) & OPT_PROTO_NO_PASSWORD) &&
	   !(purple_protocol_get_options(priv->protocol) & OPT_PROTO_PASSWORD_OPTIONAL))
	{
		g_set_error(error, PURPLE_CONNECTION_ERROR, 0,
		            "Cannot connect to account %s without a password.",
		            purple_account_get_username(priv->account));

		return FALSE;
	}

	purple_debug_info("connection", "Connecting. connection = %p",
	                  connection);

	klass = PURPLE_CONNECTION_GET_CLASS(connection);
	if(klass != NULL && klass->connect != NULL) {
		return klass->connect(connection, error);
	}

	g_set_error(error, PURPLE_CONNECTION_ERROR, 0,
	            "The connection for %s did not implement the connect method",
	            purple_account_get_username(priv->account));

	return FALSE;
}

gboolean
purple_connection_disconnect(PurpleConnection *connection, GError **error) {
	PurpleConnectionClass *klass = NULL;
	PurpleConnectionPrivate *priv = NULL;
	gboolean ret = TRUE;

	g_return_val_if_fail(PURPLE_IS_CONNECTION(connection), FALSE);

	/* We don't check if the connection's state is connected as everything
	 * should be idempotent when doing cleanup.
	 */

	priv = purple_connection_get_instance_private(connection);

	purple_debug_info("connection", "Disconnecting connection %p", connection);

	/* Dispatch to the connection's disconnect method. */
	klass = PURPLE_CONNECTION_GET_CLASS(connection);
	if(klass != NULL && klass->disconnect != NULL) {
		ret = klass->disconnect(connection, error);
	}

	purple_account_request_close_with_account(priv->account);
	purple_request_close_with_handle(connection);

	purple_debug_info("connection", "Destroying connection %p", connection);

	return ret;
}

GCancellable *
purple_connection_get_cancellable(PurpleConnection *connection) {
	PurpleConnectionPrivate *priv = NULL;

	g_return_val_if_fail(PURPLE_IS_CONNECTION(connection), NULL);

	priv = purple_connection_get_instance_private(connection);

	return priv->cancellable;
}
