/*
 * This file is part of telepathy-idle
 *
 * Copyright (C) 2006-2007 Collabora Limited
 * Copyright (C) 2006-2007 Nokia Corporation
 * Copyright (C) 2011      Debarshi Ray <rishi@gnu.org>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * version 2.1 as published by the Free Software Foundation.
 *
 * 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include "config.h"
#include "idle-server-connection.h"

#include <string.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sys/socket.h>

#include <telepathy-glib/telepathy-glib.h>

#define IDLE_DEBUG_FLAG IDLE_DEBUG_NETWORK
#include "idle-connection.h"
#include "server-tls-manager.h"
#include "idle-debug.h"

typedef struct _IdleServerConnectionPrivate IdleServerConnectionPrivate;

#define IDLE_SERVER_CONNECTION_GET_PRIVATE(conn) (G_TYPE_INSTANCE_GET_PRIVATE((conn), IDLE_TYPE_SERVER_CONNECTION, IdleServerConnectionPrivate))

G_DEFINE_TYPE(IdleServerConnection, idle_server_connection, G_TYPE_OBJECT)

enum {
	DISCONNECTED,
	RECEIVED,
	LAST_SIGNAL
};

enum {
	PROP_HOST = 1,
	PROP_PORT,
	PROP_TLS_MANAGER
};

typedef enum {
	SERVER_CONNECTION_STATE_NOT_CONNECTED,
	SERVER_CONNECTION_STATE_CONNECTING,
	SERVER_CONNECTION_STATE_CONNECTED
} IdleServerConnectionState;

struct _IdleServerConnectionPrivate {
	gchar *host;
	guint16 port;

	gchar input_buffer[IRC_MSG_MAXLEN + 3];
	gchar output_buffer[IRC_MSG_MAXLEN + 2]; /* No need for a trailing '\0' */
	gsize count;
	gsize nwritten;

	guint reason;

	GSocketClient *socket_client;
	GIOStream *io_stream;
	GCancellable *read_cancellable;
	GCancellable *cancellable;

	IdleServerConnectionState state;
	IdleServerTLSManager *tls_manager;
	GAsyncQueue *certificate_queue;
};

static GObject *idle_server_connection_constructor(GType type, guint n_props, GObjectConstructParam *props);

static guint signals[LAST_SIGNAL] = {0};

static void idle_server_connection_init(IdleServerConnection *conn) {
	IdleServerConnectionPrivate *priv = IDLE_SERVER_CONNECTION_GET_PRIVATE(conn);

	priv->host = NULL;
	priv->port = 0;

	priv->socket_client = g_socket_client_new();

	priv->state = SERVER_CONNECTION_STATE_NOT_CONNECTED;
	priv->certificate_queue = g_async_queue_new ();
}

static GObject *idle_server_connection_constructor(GType type, guint n_props, GObjectConstructParam *props) {
	GObject *ret;

	ret = G_OBJECT_CLASS(idle_server_connection_parent_class)->constructor(type, n_props, props);

	return ret;
}

static void idle_server_connection_dispose(GObject *obj) {
	IdleServerConnection *conn = IDLE_SERVER_CONNECTION(obj);
	IdleServerConnectionPrivate *priv = IDLE_SERVER_CONNECTION_GET_PRIVATE(conn);

        g_clear_object (&priv->socket_client);
        g_clear_object (&priv->io_stream);
        g_clear_object (&priv->tls_manager);
        g_clear_object (&priv->read_cancellable);
}

static void idle_server_connection_finalize(GObject *obj) {
	IdleServerConnection *conn = IDLE_SERVER_CONNECTION(obj);
	IdleServerConnectionPrivate *priv = IDLE_SERVER_CONNECTION_GET_PRIVATE(conn);

	g_async_queue_unref (priv->certificate_queue);
	g_free(priv->host);
}

static void idle_server_connection_get_property(GObject 	*obj, guint prop_id, GValue *value, GParamSpec *pspec) {
	IdleServerConnection *conn = IDLE_SERVER_CONNECTION(obj);
	IdleServerConnectionPrivate *priv = IDLE_SERVER_CONNECTION_GET_PRIVATE(conn);

	switch (prop_id) {
		case PROP_HOST:
			g_value_set_string(value, priv->host);
			break;

		case PROP_PORT:
			g_value_set_uint(value, priv->port);
			break;

		case PROP_TLS_MANAGER:
			g_value_set_object(value, priv->tls_manager);
			break;

		default:
			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, pspec);
			break;
	}
}

static void idle_server_connection_set_property(GObject 	*obj,
												guint 		 prop_id,
												const GValue 		*value,
												GParamSpec 	*pspec) {
	IdleServerConnection *conn = IDLE_SERVER_CONNECTION(obj);
	IdleServerConnectionPrivate *priv = IDLE_SERVER_CONNECTION_GET_PRIVATE(conn);

	switch (prop_id) {
		case PROP_HOST:
			g_free(priv->host);
			priv->host = g_value_dup_string(value);
			break;

		case PROP_PORT:
			priv->port = (guint16) g_value_get_uint(value);
			break;

		case PROP_TLS_MANAGER:
			priv->tls_manager = g_value_dup_object(value);
			break;

		default:
			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, pspec);
			break;
	}
}

static void idle_server_connection_class_init(IdleServerConnectionClass *klass) {
	GObjectClass *object_class = G_OBJECT_CLASS(klass);
	GParamSpec *pspec;

	g_type_class_add_private(klass, sizeof(IdleServerConnectionPrivate));

	object_class->constructor = idle_server_connection_constructor;
	object_class->dispose = idle_server_connection_dispose;
	object_class->finalize = idle_server_connection_finalize;

	object_class->get_property = idle_server_connection_get_property;
	object_class->set_property = idle_server_connection_set_property;

	pspec = g_param_spec_string("host", "Remote host",
								"Hostname of the remote service to connect to.",
								NULL,
								G_PARAM_READABLE|
								G_PARAM_WRITABLE|
								G_PARAM_STATIC_NICK|
								G_PARAM_STATIC_BLURB);

	g_object_class_install_property(object_class, PROP_HOST, pspec);

	pspec = g_param_spec_uint("port", "Remote port",
							  "Port number of the remote service to connect to.",
							  0, G_MAXUINT16, 0,
							  G_PARAM_READABLE|
							  G_PARAM_WRITABLE|
							  G_PARAM_STATIC_NICK|
							  G_PARAM_STATIC_BLURB);

	g_object_class_install_property(object_class, PROP_PORT, pspec);

	pspec = g_param_spec_object("tls-manager", "TLS Manager",
							  "TLS manager for interactive certificate checking",
							  IDLE_TYPE_SERVER_TLS_MANAGER,
							  G_PARAM_READWRITE|
							  G_PARAM_STATIC_STRINGS);

	g_object_class_install_property(object_class, PROP_TLS_MANAGER, pspec);

	signals[DISCONNECTED] = g_signal_new("disconnected",
						G_OBJECT_CLASS_TYPE(klass),
						G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED,
						0,
						NULL, NULL,
						g_cclosure_marshal_generic,
						G_TYPE_NONE, 1, G_TYPE_UINT);

	signals[RECEIVED] = g_signal_new("received",
						G_OBJECT_CLASS_TYPE(klass),
						G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED,
						0,
						NULL, NULL,
						g_cclosure_marshal_VOID__STRING,
						G_TYPE_NONE, 1, G_TYPE_STRING);

}

static void change_state(IdleServerConnection *conn, IdleServerConnectionState state, guint reason) {
	IdleServerConnectionPrivate *priv = IDLE_SERVER_CONNECTION_GET_PRIVATE(conn);

	if (state == priv->state)
		return;

	IDLE_DEBUG("moving to state %u, reason %u", state, reason);
	priv->state = state;

	if (state == SERVER_CONNECTION_STATE_NOT_CONNECTED)
		g_signal_emit(conn, signals[DISCONNECTED], 0, reason);
}

static void _input_stream_read(IdleServerConnection *conn, GInputStream *input_stream, GAsyncReadyCallback callback) {
	IdleServerConnectionPrivate *priv = IDLE_SERVER_CONNECTION_GET_PRIVATE(conn);

	if (priv->read_cancellable == NULL)
		priv->read_cancellable = g_cancellable_new ();


	memset(priv->input_buffer, '\0', sizeof(priv->input_buffer));
	g_input_stream_read_async (input_stream, &priv->input_buffer, sizeof(priv->input_buffer) - 1, G_PRIORITY_DEFAULT, priv->read_cancellable, callback, conn);
}

static void _input_stream_read_ready(GObject *source_object, GAsyncResult *res, gpointer user_data) {
	GInputStream *input_stream = G_INPUT_STREAM(source_object);
	IdleServerConnection *conn = IDLE_SERVER_CONNECTION(user_data);
	IdleServerConnectionPrivate *priv = IDLE_SERVER_CONNECTION_GET_PRIVATE(conn);
	gssize ret;
	GError *error = NULL;

	if (priv->io_stream == NULL) /* ie. we are in the process of disconnecting */
		goto cleanup;

	ret = g_input_stream_read_finish(input_stream, res, &error);

	if (ret == -1) {
		IDLE_DEBUG("g_input_stream_read failed: %s", error->message);
		g_error_free(error);
		goto disconnect;
	} else if (ret == 0) {
		IDLE_DEBUG("g_input_stream_read returned end-of-file");
		goto disconnect;
	}

	g_signal_emit(conn, signals[RECEIVED], 0, priv->input_buffer);

	_input_stream_read(conn, input_stream, _input_stream_read_ready);
	return;

disconnect:
	if (priv->state == SERVER_CONNECTION_STATE_CONNECTED)
		idle_server_connection_disconnect_full_async(conn, SERVER_CONNECTION_STATE_REASON_ERROR, NULL, NULL, NULL);
cleanup:
	g_object_unref(conn);
}

static void _connect_to_host_ready(GObject *source_object, GAsyncResult *res, gpointer user_data) {
	GSimpleAsyncResult *task = G_SIMPLE_ASYNC_RESULT (res);
	GSimpleAsyncResult *result = G_SIMPLE_ASYNC_RESULT(user_data);
	IdleServerConnection *conn = IDLE_SERVER_CONNECTION(g_async_result_get_source_object(G_ASYNC_RESULT(result)));
	IdleServerConnectionPrivate *priv = IDLE_SERVER_CONNECTION_GET_PRIVATE(conn);
	GInputStream *input_stream;
	GSocket *socket_;
	GSocketConnection *socket_connection;
	gint nodelay = 1;
	gint socket_fd;
	GError *error = NULL;

	if (g_simple_async_result_propagate_error (task, &error)) {
		IDLE_DEBUG("g_socket_client_connect_to_host failed: %s", error->message);
		g_simple_async_result_set_error(result, TP_ERROR, TP_ERROR_NETWORK_ERROR, "%s", error->message);
		g_error_free(error);
		change_state(conn, SERVER_CONNECTION_STATE_NOT_CONNECTED, SERVER_CONNECTION_STATE_REASON_ERROR);
		g_object_unref(conn);
		goto cleanup;
	}

	socket_connection = g_object_ref (g_simple_async_result_get_op_res_gpointer (task));
	socket_ = g_socket_connection_get_socket(socket_connection);
	g_socket_set_keepalive(socket_, TRUE);

	socket_fd = g_socket_get_fd(socket_);
	setsockopt(socket_fd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay));

	g_tcp_connection_set_graceful_disconnect(G_TCP_CONNECTION(socket_connection), TRUE);

	priv->io_stream = G_IO_STREAM(socket_connection);

	input_stream = g_io_stream_get_input_stream(priv->io_stream);
	_input_stream_read(conn, input_stream, _input_stream_read_ready);
	change_state(conn, SERVER_CONNECTION_STATE_CONNECTED, SERVER_CONNECTION_STATE_REASON_REQUESTED);

cleanup:
	g_simple_async_result_complete(result);
	g_object_unref(result);
	g_object_unref(task);
}


#define CERT_ACCEPTED 1
#define CERT_REJECTED 2
struct CertData {
	IdleServerConnection *self;
	GTlsCertificate *certificate;
};

static void _certificate_verified (GObject *source, GAsyncResult *res, gpointer user_data)
{
	IdleServerConnection *self = user_data;
	IdleServerConnectionPrivate *priv = IDLE_SERVER_CONNECTION_GET_PRIVATE(self);
	gboolean ret;

	ret = idle_server_tls_manager_verify_finish (IDLE_SERVER_TLS_MANAGER (source), res, NULL);
	g_async_queue_push (priv->certificate_queue, GINT_TO_POINTER (ret ?  CERT_ACCEPTED : CERT_REJECTED));
}

static gboolean
_check_certificate_interactively (gpointer data) {
	struct CertData *d = data;
	IdleServerConnectionPrivate *priv = IDLE_SERVER_CONNECTION_GET_PRIVATE(d->self);

	idle_server_tls_manager_verify_async (priv->tls_manager, d->certificate, priv->host, _certificate_verified, d->self);
	return FALSE;
}

static gboolean _accept_certificate_request (GTlsConnection *tls_connection, GTlsCertificate *peer_cert, GTlsCertificateFlags errors, IdleServerConnection *conn)
{
	IdleServerConnectionPrivate *priv = IDLE_SERVER_CONNECTION_GET_PRIVATE(conn);
	struct CertData d;
	gpointer result;

	/* Requested to check the extra errors from an ssl certificate,
	 * Need to bounce this back into the main thread so we can ask the UI
	 * on dbus */
	IDLE_DEBUG ("Requested to validate certificate");

	d.self = conn;
	d.certificate = peer_cert;

	g_idle_add (_check_certificate_interactively, &d);
	result = g_async_queue_pop (priv->certificate_queue);

	return GPOINTER_TO_INT (result) == CERT_ACCEPTED;
}

static void _connect_event_cb (GSocketClient *client, GSocketClientEvent event, GSocketConnectable *connectable, GIOStream *connection, gpointer user_data)
{
	if (event != G_SOCKET_CLIENT_TLS_HANDSHAKING)
		return;

	g_signal_connect (connection, "accept-certificate", G_CALLBACK (_accept_certificate_request), user_data);
}

static void _connect_in_thread (GSimpleAsyncResult *task, GObject *source_object, GCancellable *cancellable)
{
	IdleServerConnection *conn = IDLE_SERVER_CONNECTION (source_object);
	IdleServerConnectionPrivate *priv = IDLE_SERVER_CONNECTION_GET_PRIVATE(conn);
	GError *error = NULL;
	GSocketConnection *socket_connection;
	gulong event_id;

	event_id = g_signal_connect (priv->socket_client, "event",
		G_CALLBACK (_connect_event_cb), conn);
	socket_connection = g_socket_client_connect_to_host (priv->socket_client, priv->host, priv->port, cancellable, &error);
	g_signal_handler_disconnect (priv->socket_client, event_id);

	if (socket_connection != NULL)
		g_simple_async_result_set_op_res_gpointer (task, socket_connection, g_object_unref);
	else
		g_simple_async_result_take_error (task, error);
}

void idle_server_connection_connect_async(IdleServerConnection *conn, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) {
	IdleServerConnectionPrivate *priv = IDLE_SERVER_CONNECTION_GET_PRIVATE(conn);
	GSimpleAsyncResult *result;
	GSimpleAsyncResult *task;

	if (priv->state != SERVER_CONNECTION_STATE_NOT_CONNECTED) {
		IDLE_DEBUG("already connecting or connected!");
		g_simple_async_report_error_in_idle(G_OBJECT(conn),
			callback, user_data,
			TP_ERROR, TP_ERROR_NOT_AVAILABLE,
			"already connecting or connected!");
		return;
	}

	if ((priv->host == NULL) || (priv->host[0] == '\0')) {
		IDLE_DEBUG("host not set!");
		g_simple_async_report_error_in_idle(G_OBJECT(conn),
			callback, user_data,
			TP_ERROR, TP_ERROR_NOT_AVAILABLE,
			"host not set!");
		return;
	}

	if (priv->port == 0) {
		IDLE_DEBUG("port not set!");
		g_simple_async_report_error_in_idle(G_OBJECT(conn),
			callback, user_data,
			TP_ERROR, TP_ERROR_NOT_AVAILABLE,
			"port not set!");
		return;
	}

	result = g_simple_async_result_new(G_OBJECT(conn), callback, user_data, idle_server_connection_connect_async);

	task = g_simple_async_result_new (G_OBJECT (conn), _connect_to_host_ready, result, NULL);
	g_simple_async_result_run_in_thread (task, _connect_in_thread, G_PRIORITY_DEFAULT, cancellable);

	change_state(conn, SERVER_CONNECTION_STATE_CONNECTING, SERVER_CONNECTION_STATE_REASON_REQUESTED);
}

gboolean idle_server_connection_connect_finish(IdleServerConnection *conn, GAsyncResult *result, GError **error) {
	g_return_val_if_fail(g_simple_async_result_is_valid(result, G_OBJECT(conn), idle_server_connection_connect_async), FALSE);
	return !g_simple_async_result_propagate_error (G_SIMPLE_ASYNC_RESULT(result), error);
}

static void _close_ready(GObject *source_object, GAsyncResult *res, gpointer user_data) {
	GIOStream *io_stream = G_IO_STREAM(source_object);
	GSimpleAsyncResult *result = G_SIMPLE_ASYNC_RESULT(user_data);
	IdleServerConnection *conn = IDLE_SERVER_CONNECTION(g_async_result_get_source_object(G_ASYNC_RESULT(result)));
	IdleServerConnectionPrivate *priv = IDLE_SERVER_CONNECTION_GET_PRIVATE(conn);
	GError *error = NULL;

	change_state(conn, SERVER_CONNECTION_STATE_NOT_CONNECTED, priv->reason);
	g_object_unref(conn);

	if (!g_io_stream_close_finish(io_stream, res, &error)) {
		IDLE_DEBUG("g_io_stream_close failed: %s", error->message);
		g_simple_async_result_set_error(result, TP_ERROR, TP_ERROR_NETWORK_ERROR, "%s", error->message);
		g_error_free(error);
	}

	g_simple_async_result_complete(result);
	g_object_unref(result);
}

void idle_server_connection_disconnect_async(IdleServerConnection *conn, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) {
	idle_server_connection_disconnect_full_async(conn,
						     SERVER_CONNECTION_STATE_REASON_REQUESTED,
						     cancellable,
						     callback,
						     user_data);
}

void idle_server_connection_force_disconnect(IdleServerConnection *conn) {
	/* Passing a cancelled cancellable to g_io_stream_close_async() stops us
	 * waiting for a TCP-level reply from the server which we already know
	 * has gone away. Quoth the docs for g_io_stream_close():
	 *
	 *     Cancelling a close will still leave the stream closed, but some
	 *     streams can use a faster close that doesn't block to e.g. check
	 *     errors.
	 */
	GCancellable *kill_me_now = g_cancellable_new();

	g_cancellable_cancel(kill_me_now);
	idle_server_connection_disconnect_full_async(conn,
		SERVER_CONNECTION_STATE_REASON_ERROR, kill_me_now, NULL, NULL);
	g_object_unref(kill_me_now);
}

void idle_server_connection_disconnect_full_async(IdleServerConnection *conn, guint reason, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) {
	IdleServerConnectionPrivate *priv = IDLE_SERVER_CONNECTION_GET_PRIVATE(conn);
	GSimpleAsyncResult *result;

	g_assert(priv != NULL);

	if (priv->state != SERVER_CONNECTION_STATE_CONNECTED) {
		IDLE_DEBUG("the connection was not open");
		g_simple_async_report_error_in_idle(G_OBJECT(conn),
			callback, user_data,
			TP_ERROR, TP_ERROR_NOT_AVAILABLE,
			"the connection was not open");
		return;
	}

	if (priv->io_stream == NULL) {
		IDLE_DEBUG("We were exploding anyway");
		g_simple_async_report_error_in_idle(G_OBJECT(conn),
			callback, user_data,
			TP_ERROR, TP_ERROR_NOT_AVAILABLE,
			"We were exploding anyway");
		return;
	}

	priv->reason = reason;

	g_cancellable_cancel (priv->read_cancellable);
	g_clear_object (&priv->read_cancellable);

	result = g_simple_async_result_new(G_OBJECT(conn), callback, user_data, idle_server_connection_disconnect_full_async);
	g_io_stream_close_async(priv->io_stream, G_PRIORITY_DEFAULT, cancellable, _close_ready, result);
	g_object_unref(priv->io_stream);
	priv->io_stream = NULL;
}

gboolean idle_server_connection_disconnect_finish(IdleServerConnection *conn, GAsyncResult *result, GError **error) {
	g_return_val_if_fail(g_simple_async_result_is_valid(result, G_OBJECT(conn), idle_server_connection_disconnect_full_async), FALSE);
	return !g_simple_async_result_propagate_error (G_SIMPLE_ASYNC_RESULT(result), error);
}

static void _write_ready(GObject *source_object, GAsyncResult *res, gpointer user_data) {
	GOutputStream *output_stream = G_OUTPUT_STREAM(source_object);
	GSimpleAsyncResult *result = G_SIMPLE_ASYNC_RESULT(user_data);
	IdleServerConnection *conn = IDLE_SERVER_CONNECTION(g_async_result_get_source_object(G_ASYNC_RESULT(result)));
	IdleServerConnectionPrivate *priv = IDLE_SERVER_CONNECTION_GET_PRIVATE(conn);
	gssize nwrite;
	GError *error = NULL;

	g_object_unref(conn);

	nwrite = g_output_stream_write_finish(output_stream, res, &error);
	if (nwrite == -1) {
		IDLE_DEBUG("g_output_stream_write failed : %s", error->message);
		g_simple_async_result_set_error(result, TP_ERROR, TP_ERROR_NETWORK_ERROR, "%s", error->message);
		g_error_free(error);
		goto cleanup;
	}

	priv->nwritten += nwrite;
	if (priv->nwritten < priv->count) {
		g_output_stream_write_async(output_stream, priv->output_buffer + priv->nwritten, priv->count - priv->nwritten, G_PRIORITY_DEFAULT, priv->cancellable, _write_ready, result);
		return;
	}

cleanup:
	if (priv->cancellable != NULL) {
		g_object_unref(priv->cancellable);
		priv->cancellable = NULL;
	}
	g_simple_async_result_complete(result);
	g_object_unref(result);
}

void idle_server_connection_send_async(IdleServerConnection *conn, const gchar *cmd, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) {
	IdleServerConnectionPrivate *priv = IDLE_SERVER_CONNECTION_GET_PRIVATE(conn);
	GOutputStream *output_stream;
	GSimpleAsyncResult *result;
	gsize output_buffer_size = sizeof(priv->output_buffer);

	if (priv->state != SERVER_CONNECTION_STATE_CONNECTED
            || priv->io_stream == NULL) {
		IDLE_DEBUG("connection was not open!");
		g_simple_async_report_error_in_idle(G_OBJECT(conn),
			callback, user_data,
			TP_ERROR, TP_ERROR_NOT_AVAILABLE,
			"connection was not open!");
		return;
	}

	priv->count = strlen(cmd);
	if (priv->count > output_buffer_size)
		priv->count = output_buffer_size;

	/* We only need to copy priv->count bytes, but padding the rest
         * with null bytes gives us cleaner debug messages, without
         * affecting the readability of the code.
         */
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wstringop-truncation"
	strncpy(priv->output_buffer, cmd, output_buffer_size);
#pragma GCC diagnostic pop

	priv->nwritten = 0;

	if (cancellable != NULL) {
		priv->cancellable = cancellable;
		g_object_ref(priv->cancellable);
	}

	output_stream = g_io_stream_get_output_stream(priv->io_stream);
	result = g_simple_async_result_new(G_OBJECT(conn), callback, user_data, idle_server_connection_send_async);
	g_output_stream_write_async(output_stream, priv->output_buffer, priv->count, G_PRIORITY_DEFAULT, cancellable, _write_ready, result);

	IDLE_DEBUG("sending \"%s\" to OutputStream %p", priv->output_buffer, output_stream);
}

gboolean idle_server_connection_send_finish(IdleServerConnection *conn, GAsyncResult *result, GError **error) {
	/* we can't check the source tag here because we use
	 * report_error_in_idle which never sets a source tag */
	g_return_val_if_fail(g_simple_async_result_is_valid(result, G_OBJECT(conn), NULL), FALSE);
	return !g_simple_async_result_propagate_error (G_SIMPLE_ASYNC_RESULT(result), error);
}

gboolean idle_server_connection_is_connected(IdleServerConnection *conn) {
	IdleServerConnectionPrivate *priv = IDLE_SERVER_CONNECTION_GET_PRIVATE(conn);

	return priv->state == SERVER_CONNECTION_STATE_CONNECTED;
}

void idle_server_connection_set_tls(IdleServerConnection *conn, gboolean tls) {
	IdleServerConnectionPrivate *priv = IDLE_SERVER_CONNECTION_GET_PRIVATE(conn);
	g_socket_client_set_tls(priv->socket_client, tls);
}
