/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

#include <errno.h>
#include <netdb.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>

#include <thrift/c_glib/thrift.h>
#include <thrift/c_glib/transport/thrift_socket.h>
#include <thrift/c_glib/transport/thrift_transport.h>
#include <thrift/c_glib/transport/thrift_server_transport.h>
#include <thrift/c_glib/transport/thrift_server_socket.h>

/* object properties */
enum _ThriftServerSocketProperties
{
  PROP_0,
  PROP_THRIFT_SERVER_SOCKET_PORT,
  PROP_THRIFT_SERVER_SOCKET_PATH,
  PROP_THRIFT_SERVER_SOCKET_BACKLOG
};

/* define the GError domain string */
#define THRIFT_SERVER_SOCKET_ERROR_DOMAIN "thrift-server-socket-error-quark"

G_DEFINE_TYPE(ThriftServerSocket, thrift_server_socket, THRIFT_TYPE_SERVER_TRANSPORT)

gboolean
thrift_server_socket_listen (ThriftServerTransport *transport, GError **error)
{
  int enabled = 1; /* for setsockopt() */
  ThriftServerSocket *tsocket = THRIFT_SERVER_SOCKET (transport);

  const int socket_domain = tsocket->path ? PF_UNIX : AF_INET;

  /* create a socket */
  if ((tsocket->sd = socket (socket_domain, SOCK_STREAM, 0)) == -1)
  {
    g_set_error (error, THRIFT_SERVER_SOCKET_ERROR,
                 THRIFT_SERVER_SOCKET_ERROR_SOCKET,
                 "failed to create socket - %s", strerror (errno));
    return FALSE;
  }

  if (setsockopt(tsocket->sd, SOL_SOCKET, SO_REUSEADDR, &enabled,
                 sizeof(enabled)) == -1)
  {
    g_set_error (error, THRIFT_SERVER_SOCKET_ERROR,
                 THRIFT_SERVER_SOCKET_ERROR_SETSOCKOPT,
                 "unable to set SO_REUSEADDR - %s", strerror(errno));
    return FALSE;
  }

  /* bind to the socket */
  if (tsocket->path)
  {
    /* create a socket structure */
    struct sockaddr_un pin;
    memset (&pin, 0, sizeof(pin));
    pin.sun_family = AF_UNIX;
    memcpy(pin.sun_path, tsocket->path, strlen(tsocket->path) + 1);

    if (bind(tsocket->sd, (struct sockaddr *) &pin, sizeof(pin)) == -1)
    {
      g_set_error (error, THRIFT_SERVER_SOCKET_ERROR,
                   THRIFT_SERVER_SOCKET_ERROR_BIND,
                   "failed to bind to path %s: - %s",
                   tsocket->path, strerror(errno));
      return FALSE;
    }
  }
  else
  {
    /* create a address structure */
    struct sockaddr_in pin;
    memset (&pin, 0, sizeof(pin));
    pin.sin_family = AF_INET;
    pin.sin_addr.s_addr = INADDR_ANY;
    pin.sin_port = htons(tsocket->port);

    if (bind(tsocket->sd, (struct sockaddr *) &pin, sizeof(pin)) == -1)
    {
      g_set_error (error, THRIFT_SERVER_SOCKET_ERROR,
                   THRIFT_SERVER_SOCKET_ERROR_BIND,
                   "failed to bind to port %d - %s",
                   tsocket->port, strerror(errno));
      return FALSE;
    }
  }

  if (listen(tsocket->sd, tsocket->backlog) == -1)
  {
    if (tsocket->path)
    {
      g_set_error (error, THRIFT_SERVER_SOCKET_ERROR,
                   THRIFT_SERVER_SOCKET_ERROR_BIND,
                   "failed to bind to path %s: - %s",
                   tsocket->path, strerror(errno));
      return FALSE;
    }
    else
    {
      g_set_error (error, THRIFT_SERVER_SOCKET_ERROR,
                   THRIFT_SERVER_SOCKET_ERROR_LISTEN,
                   "failed to listen to port %d - %s",
                   tsocket->port, strerror(errno));
      return FALSE;
    }
  }

  return TRUE;
}

ThriftTransport *
thrift_server_socket_accept (ThriftServerTransport *transport, GError **error)
{
  int sd = THRIFT_INVALID_SOCKET;
  guint addrlen = 0;
  struct sockaddr_in address;
  ThriftSocket *socket = NULL;

  ThriftServerSocket *tsocket = THRIFT_SERVER_SOCKET (transport);

  if ((sd = accept(tsocket->sd, (struct sockaddr *) &address, &addrlen)) == -1)
  {
    g_set_error (error, THRIFT_SERVER_SOCKET_ERROR,
                 THRIFT_SERVER_SOCKET_ERROR_ACCEPT,
                 "failed to accept connection - %s",
                 strerror(errno));
    return FALSE;
  }

  socket = g_object_new (THRIFT_TYPE_SOCKET, NULL);
  socket->sd = sd;

  return THRIFT_TRANSPORT(socket);
}

gboolean
thrift_server_socket_close (ThriftServerTransport *transport, GError **error)
{
  ThriftServerSocket *tsocket = THRIFT_SERVER_SOCKET (transport);

  if (close (tsocket->sd) == -1)
  {
    g_set_error (error, THRIFT_SERVER_SOCKET_ERROR,
                 THRIFT_SERVER_SOCKET_ERROR_CLOSE,
                 "unable to close socket - %s", strerror(errno));
    return FALSE;
  }
  tsocket->sd = THRIFT_INVALID_SOCKET;

  return TRUE;
}

/* define the GError domain for this implementation */
GQuark
thrift_server_socket_error_quark (void)
{
  return g_quark_from_static_string(THRIFT_SERVER_SOCKET_ERROR_DOMAIN);
}

/* initializes the instance */
static void
thrift_server_socket_init (ThriftServerSocket *socket)
{
  socket->sd = THRIFT_INVALID_SOCKET;
}

/* destructor */
static void
thrift_server_socket_finalize (GObject *object)
{
  ThriftServerSocket *socket = THRIFT_SERVER_SOCKET (object);

  if (socket->sd != THRIFT_INVALID_SOCKET)
  {
    close (socket->sd);
  }
  socket->sd = THRIFT_INVALID_SOCKET;
}

/* property accessor */
void
thrift_server_socket_get_property (GObject *object, guint property_id,
                                   GValue *value, GParamSpec *pspec)
{
  ThriftServerSocket *socket = THRIFT_SERVER_SOCKET (object);

  switch (property_id)
  {
    case PROP_THRIFT_SERVER_SOCKET_PORT:
      g_value_set_uint (value, socket->port);
      break;
    case PROP_THRIFT_SERVER_SOCKET_PATH:
      g_value_set_string (value, socket->path);
      break;
    case PROP_THRIFT_SERVER_SOCKET_BACKLOG:
      g_value_set_uint (value, socket->backlog);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
  }
}

/* property mutator */
void
thrift_server_socket_set_property (GObject *object, guint property_id,
                                   const GValue *value, GParamSpec *pspec)
{
  ThriftServerSocket *socket = THRIFT_SERVER_SOCKET (object);

  switch (property_id)
  {
    case PROP_THRIFT_SERVER_SOCKET_PORT:
      socket->port = g_value_get_uint (value);
      break;
    case PROP_THRIFT_SERVER_SOCKET_PATH:
      if (socket->path) {
        g_free(socket->path);
      }
      socket->path = g_strdup (g_value_get_string (value));
      break;
    case PROP_THRIFT_SERVER_SOCKET_BACKLOG:
      socket->backlog = g_value_get_uint (value);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
  }
}

/* initializes the class */
static void
thrift_server_socket_class_init (ThriftServerSocketClass *cls)
{
  ThriftServerTransportClass *tstc = THRIFT_SERVER_TRANSPORT_CLASS (cls);
  GObjectClass *gobject_class = G_OBJECT_CLASS (cls);
  GParamSpec *param_spec = NULL;

  /* setup accessors and mutators */
  gobject_class->get_property = thrift_server_socket_get_property;
  gobject_class->set_property = thrift_server_socket_set_property;

  param_spec = g_param_spec_uint ("port",
                                  "port (construct)",
                                  "Set the port to listen to",
                                  0, /* min */
                                  65535, /* max */
                                  9090, /* default by convention */
                                  G_PARAM_CONSTRUCT_ONLY |
                                  G_PARAM_READWRITE);
  g_object_class_install_property (gobject_class,
                                   PROP_THRIFT_SERVER_SOCKET_PORT,
                                   param_spec);

  param_spec = g_param_spec_string ("path",
                                    "path (construct)",
                                    "Set the path to listen to",
                                    NULL, /* default value */
                                    G_PARAM_CONSTRUCT_ONLY |
                                    G_PARAM_READWRITE);
  g_object_class_install_property (gobject_class,
                                   PROP_THRIFT_SERVER_SOCKET_PATH,
                                   param_spec);

  param_spec = g_param_spec_uint ("backlog",
                                  "backlog (construct)",
                                  "Set the accept backlog",
                                  0, /* max */
                                  65534, /* max */
                                  1024, /* default */
                                  G_PARAM_CONSTRUCT_ONLY |
                                  G_PARAM_READWRITE);
  g_object_class_install_property (gobject_class,
                                   PROP_THRIFT_SERVER_SOCKET_BACKLOG,
                                   param_spec);

  gobject_class->finalize = thrift_server_socket_finalize;

  tstc->listen = thrift_server_socket_listen;
  tstc->accept = thrift_server_socket_accept;
  tstc->close = thrift_server_socket_close;
}

