/*
  gyrus-connection.c - Abstract connection object with TLS support

  GYRUS -- GNOME Cyrus Administrator.

  Copyright (C) 2005 Mario Fuentes <mario@gnome.cl>

  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, see <http://www.gnu.org/licenses/>.

*/

#include <config.h>

#include <gnet.h>
#ifdef HAVE_GNUTLS
#include <gnutls/gnutls.h>
#endif
#include <string.h>

#include "gyrus-connection.h"
#include "gyrus-marshal.h"

#define COMMAND_ID "GyrusCommand"
#define MAX_BUFFER_SIZE 8192

#define GYRUS_CONNECTION_GET_PRIVATE(object)         \
	(G_TYPE_INSTANCE_GET_PRIVATE ((object), GYRUS_TYPE_CONNECTION, GyrusConnectionPrivate))

G_DEFINE_TYPE (GyrusConnection, gyrus_connection, G_TYPE_OBJECT)

struct _GyrusConnectionPrivate {
	GConn *gconn;
	GInetAddrNewAsyncID addr_id;
#ifdef HAVE_GNUTLS
	gnutls_session *session;
	/* TRUE while TLS is handshaking */
	gboolean handshaking;
#endif
	/* Data received */
	gchar *data;
	gsize data_length;
	/* Buffer received */
	GString *buffer;
	/* Data sending on the data_received Callback */
	GString *ubuffer;
	/* End of data indicator */
	gboolean eod;
	/* Request string */
	GString *command;

	/* Object properties */
	gchar *host;
	gint port;
	gboolean connected;
	gboolean tls;
};

enum {
	PROP_0,
	PROP_HOST,
	PROP_PORT,
	PROP_CONNECTED,
	PROP_TLS
};

enum {
	DATA_RECEIVED,
	CONNECT_RESPONSE,
	CONNECTION_CLOSE,
	LAST_SIGNAL
};

/* Private API Prototypes */

/* Fields */
static guint signals[LAST_SIGNAL] = { 0 };

/*** Private API ***/

static void
gyrus_connection_receive (GyrusConnection *conn)
{
	g_return_if_fail (GYRUS_IS_CONNECTION (conn));

	gnet_conn_read (conn->priv->gconn);
}

/* This callback is called for Decode data.
 * buffer: a buffer to copy the first "size" bytes.
 */
#ifdef HAVE_GNUTLS
static gssize
gyrus_connection_tls_read (GyrusConnection *conn,
                           gchar *buffer, gsize size)
{
	g_return_val_if_fail (GYRUS_IS_CONNECTION (conn), 0);

	/* GnuTLS is handshaking for the secure connection */
	if (conn->priv->handshaking)
	{
		gssize bytes;

		g_io_channel_read_chars (conn->priv->gconn->iochannel,
					 buffer, size, &bytes, NULL);

		return bytes;
	}

	/* GnuTLS requests the buffer in blocks.
	 */

	/* EOF */
	if (conn->priv->data_length == 0)
		return 0;

	g_print ("Decoding data, %d bytes of Encoded data...\n",
		 conn->priv->data_length);
	gsize len = (size > conn->priv->data_length) ? conn->priv->data_length : size;

	/* Error  */
	if (len == 0)
	{
		g_print ("LENGTH 0!!!\n");

		return -1;
	}

	/*g_memmove*/
	memcpy (buffer, conn->priv->data, len);

	/* If the block request is less than the real buffer, then remove
	 * all bytes already readed.
	 */
	if (len < conn->priv->data_length)
	{
		conn->priv->data_length -= len;

		gchar *tmp_buf = g_memdup (conn->priv->data + len,
					   conn->priv->data_length);

		g_free (conn->priv->data);

		conn->priv->data = tmp_buf;
	}
	else if (len == conn->priv->data_length)
	{
		conn->priv->data_length = 0;
		g_free (conn->priv->data);
		conn->priv->data = NULL;
	}

	g_print ("Decoded %d bytes...\n", len);

	return len;
}

/* This callback is called to send Encoded data.
 */
gssize
gyrus_connection_tls_write (GyrusConnection *conn,
                           const gchar *buffer, gsize size)
{
	gchar *buf;

	g_return_val_if_fail (GYRUS_IS_CONNECTION (conn), 0);

	if (conn->priv->handshaking)
	{
		gssize bytes;

		g_io_channel_write_chars (conn->priv->gconn->iochannel,
				buffer, size, &bytes, NULL);

		return bytes;
	}

	/* This copy is only for non warnings, because gnet_conn_write
	 * receive a char * and buffer is const char *.
	 */
	buf = (gchar *) g_memdup (buffer, size);

	gnet_conn_write (conn->priv->gconn, buf, size);

	g_free (buf);

	return size;
}
#endif

/* Writes in conn->priv->ubuffer the data to
   send to the application, without the COMMAND_ID
   prefix.
*/
static void
gyrus_connection_prepare_ubuffer (GyrusConnection *conn)
{
	gchar *buf;
	GString *line;

	g_return_if_fail (GYRUS_IS_CONNECTION (conn));
	g_return_if_fail (conn->priv->buffer != NULL);

	buf = conn->priv->buffer->str;
	line = g_string_new ("");

	g_string_set_size (conn->priv->ubuffer, 0);
	g_print ("pure buffer:\n%s\n", buf);
	while (*buf)
	{
		g_string_append_c (line, *buf);

		if (*buf == '\n')
		{
			/* End of buffer, check for command id */
			if  (!(*(buf + 1)))
			{
				gchar **token = g_strsplit (line->str, " ", 0);

				/* End of Data */
				if (g_ascii_strcasecmp (token[0], COMMAND_ID) == 0)
				{
					gint jump = strlen (COMMAND_ID) + 1;
					gchar *lf = g_strdup (line->str + jump);

					conn->priv->eod = TRUE;

					g_string_assign (line, lf);

					g_free (lf);
				}

				g_strfreev (token);
			}

			g_string_append (conn->priv->ubuffer, line->str);
			g_string_assign (line, "");
		}

		buf++;
	}

	/* Save non-complete line */
	if (line->len > 0)
		g_string_assign (conn->priv->buffer, line->str);
	else
		g_string_assign (conn->priv->buffer, "");

	g_string_free (line, TRUE);
}

static void
gyrus_connection_conn_cb (GConn *gconn, GConnEvent *event, gpointer data)
{
	GyrusConnection *conn;

	g_return_if_fail (GYRUS_IS_CONNECTION (data));

	conn = GYRUS_CONNECTION (data);

	switch (event->type)
	{
		case GNET_CONN_ERROR:
		{
			g_print ("GyrusConnection:: GNET_CONN_ERROR\n");

			break;
		}
		case GNET_CONN_CONNECT:
		{
#ifdef HAVE_GNUTLS
			if (conn->priv->tls)
			{
				gint priority [] = {GNUTLS_KX_RSA, 0};
				gnutls_certificate_credentials credentials;
				gint error;

				if (conn->priv->session)
					gnutls_deinit (*(conn->priv->session));

				conn->priv->session = g_malloc0 (sizeof (gnutls_session));

				/* Initialize a GnuTLS session */
				gnutls_init (conn->priv->session, GNUTLS_CLIENT);
				/* Setup user pointer to pass in callback, like gpointer data */
				gnutls_transport_set_ptr (*(conn->priv->session), conn);
				/* Set the push and pull functions, called when a event
				 * is occured, like g_signal_connect.
				 */
				gnutls_transport_set_pull_function (*(conn->priv->session),
						(gnutls_pull_func) gyrus_connection_tls_read);
				gnutls_transport_set_push_function (*(conn->priv->session),
						(gnutls_push_func) gyrus_connection_tls_write);
				/* Priority */
				gnutls_set_default_priority (*(conn->priv->session));
				/* Protocol priority */
				gnutls_kx_set_priority (*(conn->priv->session),	priority);
				/* Create credentials */
				gnutls_certificate_allocate_credentials (&credentials);

				/*gnutls_certificate_set_x509_trust_file (credentials,
						_GYRUS_CAFILE, GNUTLS_X509_FMT_PEM);
				*/
				/* Set the credentials to the session */
				gnutls_credentials_set (*(conn->priv->session),
							GNUTLS_CRD_CERTIFICATE, credentials);

				if ((error = gnutls_handshake (*(conn->priv->session))) < 0)
				{
					/* TODO: manage the error */

					g_critical ("GyrusConnection::connect: %s\n",
							gnutls_alert_get_name (
								gnutls_alert_get (*(conn->priv->session))));

					g_signal_emit (conn,
					               signals[CONNECT_RESPONSE],
						       0,
						       GYRUS_CONN_FAILED);

					return;
				}
				else
				{
					conn->priv->handshaking = FALSE;
				}
			}
#endif

			conn->priv->connected = TRUE;

			g_signal_emit (conn,
			               signals[CONNECT_RESPONSE],
				       0,
				       GYRUS_CONN_DONE);

			gyrus_connection_receive (conn);

			break;
		}
		case GNET_CONN_CLOSE:
		{
			g_signal_emit (conn,
			               signals[CONNECTION_CLOSE],
				       0,
				       NULL);
			break;
		}
		case GNET_CONN_TIMEOUT:
		{
			break;
		}
		case GNET_CONN_READ:
		{
			conn->priv->data_length = event->length;

#ifdef HAVE_GNUTLS
			/* If the connection uses a TLS session, then decode
			 * received data.
			 */
			if (conn->priv->tls)
			{
				conn->priv->data = g_memdup (event->buffer, event->length);
				g_print ("size: %i\n", event->length);
				/* A dynamic buffer */
				gchar *buf = (gchar *) g_malloc0 (MAX_BUFFER_SIZE);
				gssize len;

				len = gnutls_record_recv (*(conn->priv->session),
							  buf, MAX_BUFFER_SIZE);

				if ((len == GNUTLS_E_AGAIN) || (len == GNUTLS_E_INTERRUPTED)) {
					/* The gnutls decoder needs more data to finish decoding. */
					gyrus_connection_receive (conn);
					return;
				}

/*				g_free (conn->priv->data); */

				if (len < 0)
				{
					g_debug ("GnuTLS error");
					/* TODO: manage GnuTLS fatal errors */
					if (gnutls_error_is_fatal (len) == 1)
					{
						g_error ("GnuTLS fatal error: %s\n",
							 gnutls_strerror (len));
					}

					conn->priv->data = NULL;
				}

				conn->priv->data = g_strndup (buf, len);

				conn->priv->data_length = len;

				g_free (buf);
			}
			else
#endif
			{
				conn->priv->data = g_strndup (event->buffer, event->length);
			}

			if (conn->priv->data != NULL)
			{
				/* Append new data to the buffer */
				g_string_append (conn->priv->buffer, conn->priv->data);

				g_free (conn->priv->data);
				conn->priv->data = NULL;

				gyrus_connection_prepare_ubuffer (conn);

				g_signal_emit (conn,
				               signals[DATA_RECEIVED],
					       0,
					       conn->priv->ubuffer->str,
					       conn->priv->eod);
			}

			if (!conn->priv->eod)
				gyrus_connection_receive (conn);
			else
			{
				conn->priv->eod = FALSE;
				g_string_set_size (conn->priv->buffer, 0);
				g_string_set_size (conn->priv->ubuffer, 0);
			}

			break;
		}

		case GNET_CONN_WRITE:
		{
			gyrus_connection_receive (conn);

			break;
		}
		/*
		 */
		case GNET_CONN_READABLE:
		{
			break;
		}

		case GNET_CONN_WRITABLE:
		{
			break;
		}
	}
}

static void
gyrus_connection_inetaddr_cb (GInetAddr *inetaddr, gpointer data)
{
	GyrusConnection *conn;

	g_return_if_fail (GYRUS_IS_CONNECTION (data));

	conn = GYRUS_CONNECTION (data);

	if (inetaddr == NULL)
	{
		g_signal_emit (conn,
		               signals[CONNECT_RESPONSE],
							0,
							GYRUS_CONN_FAILED);

		return;
	}

	conn->priv->gconn = gnet_conn_new_inetaddr (inetaddr,
						    gyrus_connection_conn_cb,
						    data);

	gnet_conn_set_watch_error (conn->priv->gconn, TRUE);
	/*gnet_conn_set_watch_readable (conn->priv->gconn, TRUE);*/
	gnet_conn_connect (conn->priv->gconn);
}

static void
gyrus_connection_class_finalize (GObject *obj)
{
	GyrusConnection *conn;

	g_return_if_fail (GYRUS_IS_CONNECTION (obj));

	conn = GYRUS_CONNECTION (obj);


}

static void
gyrus_connection_set_property (GObject *object,
			       guint property_id, const GValue *value, GParamSpec *pspec)
{
	GyrusConnectionPrivate *priv = GYRUS_CONNECTION_GET_PRIVATE (object);

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

		case PROP_PORT:
		{
			priv->port = g_value_get_int (value);
			break;
		}

		case PROP_CONNECTED:
		{
			priv->connected = g_value_get_boolean (value);
			break;
		}

		case PROP_TLS:
		{
			priv->tls = g_value_get_boolean (value);
			break;
		}

		default:
		{
			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
			break;
		}
	}
}

static void
gyrus_connection_get_property (GObject *object,
		guint property_id, GValue *value, GParamSpec *pspec)
{
	GyrusConnection *conn = GYRUS_CONNECTION (object);

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

		case PROP_PORT:
		{
			g_value_set_int (value, conn->priv->port);
			break;
		}

		case PROP_CONNECTED:
		{
			g_value_set_boolean (value, conn->priv->connected);
			break;
		}

		case PROP_TLS:
		{
			g_value_set_boolean (value, conn->priv->tls);
			break;
		}

		default:
		{
			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
			break;
		}
	}

}

static void
gyrus_connection_class_init (GyrusConnectionClass *klass)
{
	GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
	GType data_received_param_types[2];
	GType connect_response_param_types[1];

	gyrus_connection_parent_class = g_type_class_peek_parent (klass);

	gobject_class->set_property = gyrus_connection_set_property;
	gobject_class->get_property = gyrus_connection_get_property;
	gobject_class->finalize = gyrus_connection_class_finalize;

	/* Properties */

	g_object_class_install_property (gobject_class,
	                                 PROP_HOST,
					 g_param_spec_string ("host",
							      "Host Name",
							      "Host of the server",
							      "127.0.0.1",
							      G_PARAM_READWRITE |
							      G_PARAM_CONSTRUCT_ONLY));

	g_object_class_install_property (gobject_class,
	                                 PROP_PORT,
					 g_param_spec_int ("port",
							   "Port number",
							   "Port of the server",
							   0,
							   65535,
							   143,
							   G_PARAM_READWRITE |
							   G_PARAM_CONSTRUCT_ONLY));

	g_object_class_install_property (gobject_class,
	                                 PROP_CONNECTED,
					 g_param_spec_boolean ("connected",
							       "Connection status",
							       "Is connected?",
							       FALSE,
							       G_PARAM_READWRITE));

	g_object_class_install_property (gobject_class,
	                                 PROP_TLS,
					 g_param_spec_boolean ("tls",
							       "TLS Connection",
							       "Is using a secure connection",
							       FALSE,
							       G_PARAM_READWRITE |
							       G_PARAM_CONSTRUCT_ONLY));

	/* Signals */

	data_received_param_types[0] = G_TYPE_STRING;
	data_received_param_types[1] = G_TYPE_BOOLEAN;

	signals[DATA_RECEIVED] =
		g_signal_newv ("data_received",
			       G_TYPE_FROM_CLASS (klass),
			       G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS,
			       NULL, NULL, NULL,
			       gyrus_VOID__STRING_BOOLEAN,
			       /*g_cclosure_marshal_VOID__STRING,*/
			       G_TYPE_NONE, 2,
			       data_received_param_types);

	connect_response_param_types[0] = G_TYPE_INT;

	signals[CONNECT_RESPONSE] =
		g_signal_newv ("connect_response",
		               G_TYPE_FROM_CLASS (klass),
			       G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS,
			       NULL, NULL, NULL,
			       g_cclosure_marshal_VOID__INT,
			       G_TYPE_NONE, 1,
			       connect_response_param_types);

	signals[CONNECTION_CLOSE] =
		g_signal_newv ("connection_close",
		               G_TYPE_FROM_CLASS (klass),
			       G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS,
			       NULL, NULL, NULL,
			       g_cclosure_marshal_VOID__VOID,
			       G_TYPE_NONE, 0, NULL);

	g_type_class_add_private (klass, sizeof (GyrusConnectionPrivate));
}

static void
gyrus_connection_init (GyrusConnection *conn)
{
	conn->priv = GYRUS_CONNECTION_GET_PRIVATE (conn);

	conn->priv->gconn = NULL;
#ifdef HAVE_GNUTLS
	conn->priv->session = NULL;
	conn->priv->handshaking = TRUE;
#endif
	conn->priv->data = NULL;
	/* Crate the buffer for the request results */
	conn->priv->buffer = g_string_new ("");
	conn->priv->ubuffer = g_string_new ("");
	conn->priv->command = NULL;
	conn->priv->eod = FALSE;
}

/*** Public API ***/
GyrusConnection *
gyrus_connection_new (gchar *host, gint port, gboolean use_tls)
{
	GyrusConnection *conn;

	conn = g_object_new (GYRUS_TYPE_CONNECTION,
	                     "host", host,
			     "port", port,
			     "tls", use_tls,
	                     NULL);

	return conn;
}

void
gyrus_connection_destroy (GyrusConnection *conn)
{
	/* TODO: free all memory allocated and close connection if it's open */
}

void
gyrus_connection_connect (GyrusConnection *conn)
{
#ifdef HAVE_GNUTLS
	static gboolean gnutls_initialized = FALSE;
#endif
	g_return_if_fail (GYRUS_IS_CONNECTION (conn));

	if (gyrus_connection_get_connected (conn) == TRUE)
		return;

#ifdef HAVE_GNUTLS
	if (!gnutls_initialized)
	{
		gnutls_global_init ();
		gnutls_initialized = TRUE;
	}
#endif

	conn->priv->addr_id = gnet_inetaddr_new_async (
			gyrus_connection_get_host (conn),
			gyrus_connection_get_port (conn),
			gyrus_connection_inetaddr_cb,
			conn);
}

void
gyrus_connection_disconnect (GyrusConnection *conn)
{
	g_return_if_fail (GYRUS_IS_CONNECTION (conn));
	g_return_if_fail (gyrus_connection_get_connected (conn));

	gnet_conn_disconnect (conn->priv->gconn);
}

void
gyrus_connection_send (GyrusConnection *conn, const gchar *buffer)
{
	GString *cmd;

	g_return_if_fail (GYRUS_IS_CONNECTION (conn));
	g_return_if_fail (buffer != NULL);
	g_return_if_fail (gyrus_connection_get_connected (conn));

	/* Clean the buffer for save the answer */
	g_string_set_size (conn->priv->buffer, 0);

	/* Prepare the request string */
	cmd = conn->priv->command = g_string_new ("");
	g_string_printf (cmd, "%s %s\n", COMMAND_ID, buffer);

	g_print ("Sending command <%s> %d bytes\n", cmd->str, cmd->len);

#ifdef HAVE_GNUTLS
	if (conn->priv->tls)
		gnutls_record_send (*(conn->priv->session), cmd->str, cmd->len);
	else
#endif
		gnet_conn_write (conn->priv->gconn, cmd->str, cmd->len);
}

gboolean
gyrus_connection_get_connected (GyrusConnection *conn)
{
	g_return_val_if_fail (GYRUS_IS_CONNECTION (conn), FALSE);

	return conn->priv->connected;
}

G_CONST_RETURN gchar *
gyrus_connection_get_host (GyrusConnection *conn)
{
	g_return_val_if_fail (GYRUS_IS_CONNECTION (conn), NULL);

	return conn->priv->host;
}

gint
gyrus_connection_get_port (GyrusConnection *conn)
{
	g_return_val_if_fail (GYRUS_IS_CONNECTION (conn), -1);

	return conn->priv->port;
}

