#include <gnutls/gnutls.h>
#include <string.h>
#include <errno.h>
#include <gio/gio.h>

#include "gtlsconnection.h"

enum
{
  PROP_S_NONE,
  PROP_S_STREAM
};

enum
{
  PROP_C_NONE,
  PROP_C_SESSION,
  PROP_C_INPUT,
  PROP_C_OUTPUT
};

enum
{
  PROP_O_NONE,
  PROP_O_SESSION
};

enum
{
  PROP_I_NONE,
  PROP_I_SESSION
};

typedef struct
{
  gboolean active;

  gint io_priority;
  GCancellable *cancellable;
  GObject *source_object;
  GAsyncReadyCallback callback;
  gpointer user_data;
  gpointer source_tag;
  GError *error;
} GTLSJob;

typedef enum
{
  G_TLS_OP_READ,
  G_TLS_OP_WRITE
} GTLSOperation;

typedef enum
{
  G_TLS_OP_STATE_IDLE,
  G_TLS_OP_STATE_ACTIVE,
  G_TLS_OP_STATE_DONE
} GTLSOpState;

typedef struct
{
  GTLSJob job;
} GTLSJobHandshake;

typedef struct
{
  GTLSJob job;

  gconstpointer buffer;
  gsize count;
} GTLSJobWrite;

typedef struct
{
  GTLSJob job;

  gpointer buffer;
  gsize count;
} GTLSJobRead;

typedef struct
{
  GTLSOpState state;

  gpointer buffer;
  gsize requested;
  gssize result;
  GError *error;
} GTLSOp;

typedef GObjectClass GTLSSessionClass;
typedef GInputStreamClass GTLSInputStreamClass;
typedef GOutputStreamClass GTLSOutputStreamClass;

struct OPAQUE_TYPE__GTLSSession
{
  GObject parent;

  GIOStream *stream;
  GCancellable *cancellable;
  GError *error;
  gboolean async;

  /* frontend jobs */
  GTLSJobHandshake handshake_job;
  GTLSJobRead      read_job;
  GTLSJobWrite     write_job;

  /* backend jobs */
  GTLSOp           read_op;
  GTLSOp           write_op;

  gnutls_session_t session;

  gnutls_certificate_credentials gnutls_cert_cred;
};

typedef struct
{
  GInputStream parent;
  GTLSSession *session;
} GTLSInputStream;

typedef struct
{
  GOutputStream parent;
  GTLSSession *session;
} GTLSOutputStream;

struct _GTLSConnection
{
  GIOStream parent_instance;

  GTLSSession *session;
  GTLSInputStream *input;
  GTLSOutputStream *output;
};

static GType g_tls_input_stream_get_type (void);
static GType g_tls_output_stream_get_type (void);

G_DEFINE_TYPE (GTLSConnection, g_tls_connection, G_TYPE_IO_STREAM);

G_DEFINE_TYPE (GTLSSession, g_tls_session, G_TYPE_OBJECT);
G_DEFINE_TYPE (GTLSInputStream, g_tls_input_stream, G_TYPE_INPUT_STREAM);
G_DEFINE_TYPE (GTLSOutputStream, g_tls_output_stream, G_TYPE_OUTPUT_STREAM);

#define G_TYPE_TLS_INPUT_STREAM (g_tls_input_stream_get_type ())
#define G_TYPE_TLS_OUTPUT_STREAM (g_tls_output_stream_get_type ())
#define G_TLS_INPUT_STREAM(inst)   (G_TYPE_CHECK_INSTANCE_CAST ((inst),   \
                                    G_TYPE_TLS_INPUT_STREAM,              \
                                    GTLSInputStream))
#define G_TLS_OUTPUT_STREAM(inst)  (G_TYPE_CHECK_INSTANCE_CAST ((inst),   \
                                    G_TYPE_TLS_OUTPUT_STREAM,             \
                                    GTLSOutputStream))

static gboolean
g_tls_set_error (GError **error,
                 gssize   result)
{
  if (result < 0)
    g_set_error (error, 0, 0, "got error code %" G_GSSIZE_FORMAT, result);

  return result < 0;
}

static GSimpleAsyncResult *
g_tls_job_make_result (GTLSJob *job,
                       gssize   result)
{
  if (result != GNUTLS_E_AGAIN)
    {
      GSimpleAsyncResult *simple;
      GError *error = NULL;

      simple = g_simple_async_result_new (job->source_object,
                                          job->callback,
                                          job->user_data,
                                          job->source_tag);

      if (job->error)
        {
          g_assert (result == GNUTLS_E_PUSH_ERROR ||
                    result == GNUTLS_E_PULL_ERROR);

          g_simple_async_result_set_from_error (simple, job->error);
          g_error_free (error);
        }
      else if (g_tls_set_error (&error, result))
        {
          g_simple_async_result_set_from_error (simple, error);
          g_error_free (error);
        }

      g_object_unref (job->source_object);
      job->active = FALSE;

      return simple;
    }
  else
    {
      g_assert (job->active);
      return NULL;
    }
}

static void
g_tls_job_result_gssize (GTLSJob *job,
                         gssize   result)
{
  GSimpleAsyncResult *simple;

  if ((simple = g_tls_job_make_result (job, result)))
    {
      if (result >= 0)
        g_simple_async_result_set_op_res_gssize (simple, result);

      g_simple_async_result_complete (simple);
      g_object_unref (simple);
    }
}

static void
g_tls_job_result_boolean (GTLSJob *job,
                          gint     result)
{
  GSimpleAsyncResult *simple;

  if ((simple = g_tls_job_make_result (job, result)))
    {
      g_simple_async_result_complete (simple);
      g_object_unref (simple);
    }
}

static void
g_tls_session_try_operation (GTLSSession   *session,
                             GTLSOperation  operation)
{
  if (session->handshake_job.job.active)
    {
      gint result;

      session->async = TRUE;
      result = gnutls_handshake (session->session);
      g_assert (result != GNUTLS_E_INTERRUPTED);
      session->async = FALSE;

      g_tls_job_result_boolean (&session->handshake_job.job, result);
    }

  else if (operation == G_TLS_OP_READ)
    {
      gssize result;

      g_assert (session->read_job.job.active);

      session->async = TRUE;
      result = gnutls_record_recv (session->session,
                                   session->read_job.buffer,
                                   session->read_job.count);
      g_assert (result != GNUTLS_E_INTERRUPTED);
      session->async = FALSE;

      g_tls_job_result_gssize (&session->read_job.job, result);
    }

  else
    {
      gssize result;

      g_assert (operation == G_TLS_OP_WRITE);
      g_assert (session->write_job.job.active);


      session->async = TRUE;
      result = gnutls_record_send (session->session,
                                   session->write_job.buffer,
                                   session->write_job.count);
      g_assert (result != GNUTLS_E_INTERRUPTED);
      session->async = FALSE;

      g_tls_job_result_gssize (&session->write_job.job, result);
    }
}

static void
g_tls_job_start (GTLSJob             *job,
                 gpointer             source_object,
                 gint                 io_priority,
                 GCancellable        *cancellable,
                 GAsyncReadyCallback  callback,
                 gpointer             user_data,
                 gpointer             source_tag)
{
  g_assert (job->active == FALSE);

  /* this is always a circular reference, so it will keep the
   * session alive for as long as the job is running.
   */
  job->source_object = g_object_ref (source_object);

  job->io_priority = io_priority;
  job->cancellable = cancellable;
  if (cancellable)
    g_object_ref (cancellable);
  job->callback = callback;
  job->user_data = user_data;
  job->source_tag = source_tag;
  job->error = NULL;
  job->active = TRUE;
}

GTLSConnection *
g_tls_session_handshake (GTLSSession   *session,
                         GCancellable  *cancellable,
                         GError       **error)
{
  gint result;

  session->error = NULL;
  session->cancellable = cancellable;
  result = gnutls_handshake (session->session);
  g_assert (result != GNUTLS_E_INTERRUPTED);
  g_assert (result != GNUTLS_E_AGAIN);
  session->cancellable = NULL;

  if (session->error)
    {
      g_assert (result == GNUTLS_E_PULL_ERROR ||
                result == GNUTLS_E_PUSH_ERROR);

      g_propagate_error (error, session->error);
      return NULL;
    }
  else if (g_tls_set_error (error, result))
    return NULL;

  return g_object_new (G_TYPE_TLS_CONNECTION, "session", session, NULL);
}

void
g_tls_session_handshake_async (GTLSSession         *session,
                               gint                 io_priority,
                               GCancellable        *cancellable,
                               GAsyncReadyCallback  callback,
                               gpointer             user_data)
{
  g_tls_job_start (&session->handshake_job.job, session,
                   io_priority, cancellable, callback, user_data,
                   g_tls_session_handshake_async);
  g_tls_session_try_operation (session, 0);
}

GTLSConnection *
g_tls_session_handshake_finish (GTLSSession   *session,
                                GAsyncResult  *result,
                                GError       **error)
{
  GSimpleAsyncResult *simple = G_SIMPLE_ASYNC_RESULT (result);

  {
    GObject *source_object;

    source_object = g_async_result_get_source_object (result);
    g_object_unref (source_object);
    g_return_val_if_fail (G_OBJECT (session) == source_object, NULL);
  }

  g_return_val_if_fail (g_tls_session_handshake_async ==
                        g_simple_async_result_get_source_tag (simple), NULL);

  if (g_simple_async_result_propagate_error (simple, error))
    return NULL;

  return g_object_new (G_TYPE_TLS_CONNECTION, "session", session, NULL);
}

static gssize
g_tls_input_stream_read (GInputStream  *stream,
                         void          *buffer,
                         gsize          count,
                         GCancellable  *cancellable,
                         GError       **error)
{
  GTLSSession *session = G_TLS_INPUT_STREAM (stream)->session;
  gssize result;

  session->cancellable = cancellable;
  result = gnutls_record_recv (session->session, buffer, count);
  g_assert (result != GNUTLS_E_INTERRUPTED);
  g_assert (result != GNUTLS_E_AGAIN);
  session->cancellable = NULL;

  if (session->error)
    {
      g_assert (result == GNUTLS_E_PULL_ERROR);
      g_propagate_error (error, session->error);
      return -1;
    }
  else if (g_tls_set_error (error, result))
    return -1;

  return result;
}

static void
g_tls_input_stream_read_async (GInputStream        *stream,
                               void                *buffer,
                               gsize                count,
                               gint                 io_priority,
                               GCancellable        *cancellable,
                               GAsyncReadyCallback  callback,
                               gpointer             user_data)
{
  GTLSSession *session = G_TLS_INPUT_STREAM (stream)->session;

  g_tls_job_start (&session->read_job.job, stream,
                   io_priority, cancellable, callback, user_data,
                   g_tls_input_stream_read_async);

  session->read_job.buffer = buffer;
  session->read_job.count = count;

  g_tls_session_try_operation (session, G_TLS_OP_READ);
}

static gssize
g_tls_input_stream_read_finish (GInputStream  *stream,
                                GAsyncResult  *result,
                                GError       **error)
{
  GSimpleAsyncResult *simple = G_SIMPLE_ASYNC_RESULT (result);

  {
    GObject *source_object;

    source_object = g_async_result_get_source_object (result);
    g_object_unref (source_object);
    g_return_val_if_fail (G_OBJECT (stream) == source_object, -1);
  }

  g_return_val_if_fail (g_tls_input_stream_read_async ==
                        g_simple_async_result_get_source_tag (simple), -1);

  if (g_simple_async_result_propagate_error (simple, error))
    return -1;

  return g_simple_async_result_get_op_res_gssize (simple);
}

static gssize
g_tls_output_stream_write (GOutputStream  *stream,
                           const void     *buffer,
                           gsize           count,
                           GCancellable   *cancellable,
                           GError        **error)
{
  GTLSSession *session = G_TLS_OUTPUT_STREAM (stream)->session;
  gssize result;

  session->cancellable = cancellable;
  result = gnutls_record_send (session->session, buffer, count);
  g_assert (result != GNUTLS_E_INTERRUPTED);
  g_assert (result != GNUTLS_E_AGAIN);
  session->cancellable = NULL;

  if (session->error)
    {
      g_assert (result == GNUTLS_E_PUSH_ERROR);
      g_propagate_error (error, session->error);
      return -1;
    }
  else if (g_tls_set_error (error, result))
    return -1;

  return result;
}

static void
g_tls_output_stream_write_async (GOutputStream       *stream,
                                 const void          *buffer,
                                 gsize                count,
                                 gint                 io_priority,
                                 GCancellable        *cancellable,
                                 GAsyncReadyCallback  callback,
                                 gpointer             user_data)
{
  GTLSSession *session = G_TLS_OUTPUT_STREAM (stream)->session;

  g_tls_job_start (&session->write_job.job, stream,
                   io_priority, cancellable, callback, user_data,
                   g_tls_output_stream_write_async);

  session->write_job.buffer = buffer;
  session->write_job.count = count;

  g_tls_session_try_operation (session, G_TLS_OP_WRITE);
}

static gssize
g_tls_output_stream_write_finish (GOutputStream   *stream,
                                  GAsyncResult   *result,
                                  GError        **error)
{
  GSimpleAsyncResult *simple = G_SIMPLE_ASYNC_RESULT (result);

  {
    GObject *source_object;

    source_object = g_async_result_get_source_object (result);
    g_object_unref (source_object);
    g_return_val_if_fail (G_OBJECT (stream) == source_object, -1);
  }

  g_return_val_if_fail (g_tls_output_stream_write_async ==
                        g_simple_async_result_get_source_tag (simple), -1);

  if (g_simple_async_result_propagate_error (simple, error))
    return -1;

  return g_simple_async_result_get_op_res_gssize (simple);
}

static void
g_tls_output_stream_init (GTLSOutputStream *stream)
{
}

static void
g_tls_input_stream_init (GTLSInputStream *stream)
{
}

static void
g_tls_output_stream_set_property (GObject *object, guint prop_id,
                                  const GValue *value, GParamSpec *pspec)
{
  GTLSOutputStream *stream = G_TLS_OUTPUT_STREAM (object);

  switch (prop_id)
    {
     case PROP_C_SESSION:
      stream->session = g_value_dup_object (value);
      break;

     default:
      g_assert_not_reached ();
    }
}

static void
g_tls_output_stream_constructed (GObject *object)
{
  GTLSOutputStream *stream = G_TLS_OUTPUT_STREAM (object);

  g_assert (stream->session);
}

static void
g_tls_output_stream_finalize (GObject *object)
{
  GTLSOutputStream *stream = G_TLS_OUTPUT_STREAM (object);

  g_object_unref (stream->session);

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

static void
g_tls_output_stream_class_init (GOutputStreamClass *class)
{
  GObjectClass *obj_class = G_OBJECT_CLASS (class);

  class->write_fn = g_tls_output_stream_write;
  class->write_async = g_tls_output_stream_write_async;
  class->write_finish = g_tls_output_stream_write_finish;
  obj_class->set_property = g_tls_output_stream_set_property;
  obj_class->constructed = g_tls_output_stream_constructed;
  obj_class->finalize = g_tls_output_stream_finalize;

  g_object_class_install_property (obj_class, PROP_O_SESSION,
    g_param_spec_object ("session", "TLS session",
                         "the TLS session object for this stream",
                         G_TYPE_TLS_SESSION, G_PARAM_WRITABLE |
                         G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_NAME |
                         G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB));
}

static void
g_tls_input_stream_set_property (GObject *object, guint prop_id,
                                 const GValue *value, GParamSpec *pspec)
{
  GTLSInputStream *stream = G_TLS_INPUT_STREAM (object);

  switch (prop_id)
    {
     case PROP_C_SESSION:
      stream->session = g_value_dup_object (value);
      break;

     default:
      g_assert_not_reached ();
    }
}

static void
g_tls_input_stream_constructed (GObject *object)
{
  GTLSInputStream *stream = G_TLS_INPUT_STREAM (object);

  g_assert (stream->session);
}

static void
g_tls_input_stream_finalize (GObject *object)
{
  GTLSInputStream *stream = G_TLS_INPUT_STREAM (object);

  g_object_unref (stream->session);

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

static void
g_tls_input_stream_class_init (GInputStreamClass *class)
{
  GObjectClass *obj_class = G_OBJECT_CLASS (class);

  class->read_fn = g_tls_input_stream_read;
  class->read_async = g_tls_input_stream_read_async;
  class->read_finish = g_tls_input_stream_read_finish;
  obj_class->set_property = g_tls_input_stream_set_property;
  obj_class->constructed = g_tls_input_stream_constructed;
  obj_class->finalize = g_tls_input_stream_finalize;

  g_object_class_install_property (obj_class, PROP_I_SESSION,
    g_param_spec_object ("session", "TLS session",
                         "the TLS session object for this stream",
                         G_TYPE_TLS_SESSION, G_PARAM_WRITABLE |
                         G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_NAME |
                         G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB));
}

static void
g_tls_connection_init (GTLSConnection *connection)
{
}

static void
g_tls_session_read_ready (GObject      *object,
                          GAsyncResult *result,
                          gpointer      user_data)
{
  GTLSSession *session = G_TLS_SESSION (user_data);

  g_assert (session->read_op.state == G_TLS_OP_STATE_ACTIVE);

  session->read_op.result =
    g_input_stream_read_finish (G_INPUT_STREAM (object), result,
                                &session->read_op.error);
  session->read_op.state = G_TLS_OP_STATE_DONE;

  /* don't recurse if the async handler is already running */
  if (!session->async)
    g_tls_session_try_operation (session, G_TLS_OP_READ);
}

static void
g_tls_session_write_ready (GObject      *object,
                           GAsyncResult *result,
                           gpointer      user_data)
{
  GTLSSession *session = G_TLS_SESSION (user_data);

  g_assert (session->write_op.state == G_TLS_OP_STATE_ACTIVE);

  session->write_op.result =
    g_output_stream_write_finish (G_OUTPUT_STREAM (object), result,
                                  &session->write_op.error);
  session->write_op.state = G_TLS_OP_STATE_DONE;

  /* don't recurse if the async handler is already running */
  if (!session->async)
    g_tls_session_try_operation (session, G_TLS_OP_WRITE);
}

static gssize
g_tls_session_push_func (gpointer    user_data,
                         const void *buffer,
                         gsize       count)
{
  GTLSSession *session = G_TLS_SESSION (user_data);
  GOutputStream *stream;

  stream = g_io_stream_get_output_stream (session->stream);

  if (session->async)
    {
      GTLSJob *active_job;

      g_assert (session->handshake_job.job.active ||
                session->write_job.job.active);

      if (session->handshake_job.job.active)
        active_job = &session->handshake_job.job;
      else
        active_job = &session->write_job.job;

      g_assert (active_job->active);

      if (session->write_op.state == G_TLS_OP_STATE_IDLE)
        {
          session->write_op.state = G_TLS_OP_STATE_ACTIVE;
          session->write_op.buffer = g_memdup (buffer, count);
          session->write_op.requested = count;
          session->write_op.error = NULL;

          g_output_stream_write_async (stream,
                                       session->write_op.buffer,
                                       session->write_op.requested,
                                       active_job->io_priority,
                                       active_job->cancellable,
                                       g_tls_session_write_ready,
                                       session);

          if G_UNLIKELY (session->write_op.state != G_TLS_OP_STATE_ACTIVE)
            g_warning ("The underlying stream '%s' used by the GTLSSession "
                       "called the GAsyncResultCallback recursively.  This "
                       "is an error in the underlying implementation: in "
                       "some cases it may lead to unbounded recursion.  "
                       "Result callbacks should always be dispatched from "
                       "the mainloop.",
                       G_OBJECT_TYPE_NAME (stream));
        }

      g_assert (session->write_op.state != G_TLS_OP_STATE_IDLE);
      g_assert_cmpint (session->write_op.requested, ==, count);
      g_assert (memcmp (session->write_op.buffer, buffer, count) == 0);

      if (session->write_op.state == G_TLS_OP_STATE_DONE)
        {
          session->write_op.state = G_TLS_OP_STATE_IDLE;
          g_free (session->write_op.buffer);

          if (session->write_op.result < 0)
            {
              active_job->error = session->write_op.error;
              gnutls_transport_set_errno (session->session, EIO);

              return -1;
            }
          else
            {
              g_assert_cmpint (session->write_op.result, <=, count);

              return session->write_op.result;
            }
        }

      gnutls_transport_set_errno (session->session, EAGAIN);

      return -1;
    }
  else
    {
      gssize result;

      result = g_output_stream_write (stream, buffer, count,
                                      session->cancellable,
                                      &session->error);

      if (result < 0)
        gnutls_transport_set_errno (session->session, EIO);

      return result;
    }
}

static gssize
g_tls_session_pull_func (gpointer  user_data,
                         void     *buffer,
                         gsize     count)
{
  GTLSSession *session = G_TLS_SESSION (user_data);
  GInputStream *stream;

  stream = g_io_stream_get_input_stream (session->stream);

  if (session->async)
    {
      GTLSJob *active_job;

      g_assert (session->handshake_job.job.active ||
                session->read_job.job.active);

      if (session->handshake_job.job.active)
        active_job = &session->handshake_job.job;
      else
        active_job = &session->read_job.job;

      g_assert (active_job->active);

      if (session->read_op.state == G_TLS_OP_STATE_IDLE)
        {
          session->read_op.state = G_TLS_OP_STATE_ACTIVE;
          session->read_op.buffer = g_malloc (count);
          session->read_op.requested = count;
          session->read_op.error = NULL;

          g_input_stream_read_async (stream,
                                     session->read_op.buffer,
                                     session->read_op.requested,
                                     active_job->io_priority,
                                     active_job->cancellable,
                                     g_tls_session_read_ready,
                                     session);

          if G_UNLIKELY (session->read_op.state != G_TLS_OP_STATE_ACTIVE)
            g_warning ("The underlying stream '%s' used by the GTLSSession "
                       "called the GAsyncResultCallback recursively.  This "
                       "is an error in the underlying implementation: in "
                       "some cases it may lead to unbounded recursion.  "
                       "Result callbacks should always be dispatched from "
                       "the mainloop.",
                       G_OBJECT_TYPE_NAME (stream));
        }

      g_assert (session->read_op.state != G_TLS_OP_STATE_IDLE);
      g_assert_cmpint (session->read_op.requested, ==, count);

      if (session->read_op.state == G_TLS_OP_STATE_DONE)
        {
          session->read_op.state = G_TLS_OP_STATE_IDLE;

          if (session->read_op.result < 0)
            {
              g_free (session->read_op.buffer);
              active_job->error = session->read_op.error;
              gnutls_transport_set_errno (session->session, EIO);

              return -1;
            }
          else
            {
              g_assert_cmpint (session->read_op.result, <=, count);

              memcpy (buffer,
                      session->read_op.buffer,
                      session->read_op.result);
              g_free (session->read_op.buffer);

              return session->read_op.result;
            }
        }

      gnutls_transport_set_errno (session->session, EAGAIN);

      return -1;
    }
  else
    {
      gssize result;

      result = g_input_stream_read (stream, buffer, count,
                                    session->cancellable,
                                    &session->error);

      if (result < 0)
        gnutls_transport_set_errno (session->session, EIO);

      return result;
    }
}

static void
g_tls_session_init (GTLSSession *session)
{
  static gsize initialised;

  if G_UNLIKELY (g_once_init_enter (&initialised))
    {
      gnutls_global_init ();
      g_once_init_leave (&initialised, 1);
    }

  gnutls_init (&session->session, GNUTLS_CLIENT);
  gnutls_certificate_allocate_credentials (&session->gnutls_cert_cred);

  gnutls_credentials_set (session->session,
    GNUTLS_CRD_CERTIFICATE, session->gnutls_cert_cred);

  gnutls_set_default_priority (session->session);
  gnutls_transport_set_push_function (session->session,
                                      g_tls_session_push_func);
  gnutls_transport_set_pull_function (session->session,
                                      g_tls_session_pull_func);
  gnutls_transport_set_ptr (session->session, session);
}

static void
g_tls_session_set_property (GObject *object, guint prop_id,
                            const GValue *value, GParamSpec *pspec)
{
  GTLSSession *session = G_TLS_SESSION (object);

  switch (prop_id)
    {
     case PROP_S_STREAM:
      session->stream = g_value_dup_object (value);
      break;

     default:
      g_assert_not_reached ();
    }
}

static void
g_tls_session_constructed (GObject *object)
{
  GTLSSession *session = G_TLS_SESSION (object);

  g_assert (session->stream);
}

static void
g_tls_session_finalize (GObject *object)
{
  GTLSSession *session = G_TLS_SESSION (object);

  gnutls_deinit (session->session);
  gnutls_certificate_free_credentials (session->gnutls_cert_cred);
  g_object_unref (session->stream);

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

static void
g_tls_session_class_init (GObjectClass *class)
{
  class->set_property = g_tls_session_set_property;
  class->constructed = g_tls_session_constructed;
  class->finalize = g_tls_session_finalize;

  g_object_class_install_property (class, PROP_S_STREAM,
    g_param_spec_object ("base-stream", "base stream",
                         "the stream that TLS communicates over",
                         G_TYPE_IO_STREAM, G_PARAM_WRITABLE |
                         G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_NAME |
                         G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB));
}

static void
g_tls_connection_set_property (GObject *object, guint prop_id,
                               const GValue *value, GParamSpec *pspec)
{
  GTLSConnection *connection = G_TLS_CONNECTION (object);

  switch (prop_id)
    {
     case PROP_C_SESSION:
      connection->session = g_value_dup_object (value);
      break;

     default:
      g_assert_not_reached ();
    }
}


static GInputStream *
g_tls_connection_get_input_stream (GIOStream *io_stream)
{
  GTLSConnection *connection = G_TLS_CONNECTION (io_stream);

  if (connection->input == NULL)
    connection->input = g_object_new (G_TYPE_TLS_INPUT_STREAM,
                                      "session", connection->session,
                                      NULL);

  return G_INPUT_STREAM (connection->input);
}

static GOutputStream *
g_tls_connection_get_output_stream (GIOStream *io_stream)
{
  GTLSConnection *connection = G_TLS_CONNECTION (io_stream);

  if (connection->output == NULL)
    connection->output = g_object_new (G_TYPE_TLS_OUTPUT_STREAM,
                                       "session", connection->session,
                                       NULL);

  return G_OUTPUT_STREAM (connection->output);
}

static void
g_tls_connection_get_property (GObject *object, guint prop_id,
                               GValue *value, GParamSpec *pspec)
{
  GTLSConnection *connection = G_TLS_CONNECTION (object);

  switch (prop_id)
    {
     case PROP_C_INPUT:
      g_value_take_object (value,
        g_tls_connection_get_input_stream (G_IO_STREAM (connection)));
      break;

     case PROP_C_OUTPUT:
      g_value_take_object (value,
        g_tls_connection_get_output_stream (G_IO_STREAM (connection)));
      break;

     default:
      g_assert_not_reached ();
    }
}

static void
g_tls_connection_constructed (GObject *object)
{
  GTLSConnection *connection = G_TLS_CONNECTION (object);

  g_assert (connection->session);
}

static void
g_tls_connection_finalize (GObject *object)
{
  GTLSConnection *connection = G_TLS_CONNECTION (object);

  g_object_unref (connection->session);

  if (connection->input)
    g_object_unref (connection->input);

  if (connection->output)
    g_object_unref (connection->output);

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

static void
g_tls_connection_class_init (GTLSConnectionClass *klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
  GIOStreamClass *stream_class = G_IO_STREAM_CLASS (klass);

  stream_class->get_input_stream = g_tls_connection_get_input_stream;
  stream_class->get_output_stream = g_tls_connection_get_output_stream;

  gobject_class->get_property = g_tls_connection_get_property;
  gobject_class->set_property = g_tls_connection_set_property;
  gobject_class->constructed = g_tls_connection_constructed;
  gobject_class->finalize = g_tls_connection_finalize;

  g_object_class_install_property (gobject_class, PROP_C_SESSION,
    g_param_spec_object ("session", "TLS session",
                         "the TLS session object for this connection",
                         G_TYPE_TLS_SESSION, G_PARAM_WRITABLE |
                         G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_NAME |
                         G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB));

  g_object_class_override_property (gobject_class, PROP_C_INPUT, "input-stream");
  g_object_class_override_property (gobject_class, PROP_C_OUTPUT, "output-stream");
}

GTLSSession *
g_tls_session_new (GIOStream *stream)
{
  return g_object_new (G_TYPE_TLS_SESSION, "base-stream", stream, NULL);
}

