/*
 * soup-discoverable-server.c - A variant of SoupServer with DNS-SD support.
 * Copyright (C) 2007 Openismus GmbH
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 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
 * 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
 *
 * Authors:
 *      Mathias Hasselmann <mathias@openismus.com>
 */
#include "soup-discoverable-server.h"

#include <avahi-client/client.h>
#include <avahi-common/alternative.h>
#include <avahi-common/error.h>
#include <avahi-client/publish.h>
#include <avahi-glib/glib-malloc.h>
#include <avahi-glib/glib-watch.h>

#include <libsoup/soup-address.h>
#include <libsoup/soup-socket.h>

#include <string.h>

enum
{
  PROP_NONE,
  PROP_NAME,
  PROP_DOMAIN,
  PROP_SERVICE
};

struct _SoupDiscoverableServerPrivate
{
  AvahiGLibPoll *poll;
  AvahiClient *client;
  AvahiEntryGroup *group;
  guint startup_handler;

  gchar *name;
  gchar *domain;
  gchar *service;
};

static gboolean startup_cb       (gpointer                data);
static void     publish_services (SoupDiscoverableServer *self);

G_DEFINE_TYPE (SoupDiscoverableServer, soup_discoverable_server, SOUP_TYPE_SERVER);

static void
handle_collision (SoupDiscoverableServer *self)
{
  gchar *alternate_name = avahi_alternative_service_name (self->priv->name);

  g_warning ("%s: Service name collision renaming service `%s' to `%s'", 
             G_STRLOC, self->priv->name, alternate_name);

  g_free (self->priv->name);
  self->priv->name = alternate_name;
  g_object_notify (G_OBJECT (self), "name");

  publish_services (self);
}

static void
publish_services (SoupDiscoverableServer *self)
{
  SoupAddress *address;
  SoupSocket *listener;
  AvahiProtocol protocol;

  gchar *service;
  gchar *host;
  gint port;

  gint result;

  listener = soup_server_get_listener (SOUP_SERVER (self));
  address = soup_socket_get_local_address (listener);
  protocol = avahi_af_to_proto (soup_address_get_sockaddr (address, NULL)->sa_family);
  port = soup_server_get_port (SOUP_SERVER (self));
  host = NULL;

  service = self->priv->service + strlen (self->priv->service);

  while (service > self->priv->service && '.' != *(--service));
  while (service > self->priv->service && '.' != *(--service));

  if (service > self->priv->service)
    service += 1;

  result = avahi_entry_group_add_service (self->priv->group,
                                          AVAHI_IF_UNSPEC, protocol, 0,
                                          self->priv->name, service,
                                          self->priv->domain, host, port, 
                                          "path=/config.xml", NULL);

  if (AVAHI_ERR_COLLISION == result)
    handle_collision (self);
  else if (result)
    g_warning ("%s: Failed to register %s service: %s (%d)\n",
               G_STRLOC, service, avahi_strerror(result), result);
  else if (service > self->priv->service)
    {
      result = avahi_entry_group_add_service_subtype (self->priv->group,
                                              AVAHI_IF_UNSPEC, protocol, 0,
                                              self->priv->name, service,
                                              self->priv->domain, self->priv->service);

      if (result)
        g_warning ("%s: Failed to register subtype %s: %s (%d)\n",
                   G_STRLOC, self->priv->service, avahi_strerror(result), result);
    }

  avahi_entry_group_commit (self->priv->group);
}

static void
avahi_entry_group_cb (AvahiEntryGroup      *group,
                      AvahiEntryGroupState  state,
                      gpointer              data)
{
  SoupDiscoverableServer *self = data;

  g_assert (NULL == self->priv->group ||
            group == self->priv->group);

  switch (state)
    {
      case AVAHI_ENTRY_GROUP_REGISTERING:
      case AVAHI_ENTRY_GROUP_ESTABLISHED:
        break;

      case AVAHI_ENTRY_GROUP_UNCOMMITED:
        self->priv->group = group;
        publish_services (self);
        break;

      case AVAHI_ENTRY_GROUP_COLLISION:
        self->priv->group = group;
        handle_collision (self);
        break;

      case AVAHI_ENTRY_GROUP_FAILURE:
        g_warning ("%s: Avahi group failure: %s (%d)\n", G_STRLOC,
                   avahi_strerror (avahi_client_errno (self->priv->client)),
                   avahi_client_errno (self->priv->client));

        if (!self->priv->startup_handler)
          self->priv->startup_handler = g_idle_add (startup_cb, self);

        break;
    }
}

static void
avahi_client_cb (AvahiClient      *client,
                 AvahiClientState  state,
                 gpointer          data)
{
  SoupDiscoverableServer *self = data;

  g_assert (NULL == self->priv->client ||
            client == self->priv->client);

  switch (state)
    {
      case AVAHI_CLIENT_S_RUNNING:
        if (!self->priv->group)
          self->priv->group = avahi_entry_group_new (client, avahi_entry_group_cb, self);

        break;

      case AVAHI_CLIENT_S_COLLISION:
      case AVAHI_CLIENT_S_REGISTERING:
        if (self->priv->group)
          avahi_entry_group_reset (self->priv->group);

        break;

      case AVAHI_CLIENT_FAILURE:
        g_warning ("%s: Avahi client failure: %s (%d)\n", G_STRLOC,
                   avahi_strerror (avahi_client_errno (client)),
                   avahi_client_errno (client));

        if (!self->priv->startup_handler)
          self->priv->startup_handler = g_idle_add (startup_cb, self);

        break;

      case AVAHI_CLIENT_CONNECTING:
        break;
    }
}

static gboolean
startup_cb (gpointer data)
{
  SoupDiscoverableServer *self = data;
  gboolean first_call = !self->priv->client;
  gint error;

  if (self->priv->group)
    {
      avahi_entry_group_reset (self->priv->group);
      self->priv->group = NULL;
    }

  if (self->priv->client)
    {
      avahi_client_free (self->priv->client);
      self->priv->client = NULL;
    }

  self->priv->client = avahi_client_new (avahi_glib_poll_get (self->priv->poll),
                                         AVAHI_CLIENT_NO_FAIL, avahi_client_cb,
                                         self, &error);

  if (!self->priv->client && first_call)
      g_warning ("%s: Failed to setup Avahi client: %s (%d)\n",
                 G_STRLOC, avahi_strerror (error), error);

  self->priv->startup_handler = 0;
  return FALSE;
}

static void
soup_discoverable_server_init (SoupDiscoverableServer *self)
{
  self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
                                            SOUP_TYPE_DISCOVERABLE_SERVER,
                                            SoupDiscoverableServerPrivate);

  avahi_set_allocator (avahi_glib_allocator ());

  self->priv->poll = avahi_glib_poll_new (NULL, G_PRIORITY_DEFAULT);
  self->priv->startup_handler = g_idle_add (startup_cb, self);
}

static void
soup_discoverable_server_set_property (GObject      *object,
                                       guint         prop_id,
                                       const GValue *value,
                                       GParamSpec   *pspec)
{
  SoupDiscoverableServer *self = SOUP_DISCOVERABLE_SERVER (object);

  switch (prop_id)
    {
      case PROP_NAME:
        g_free (self->priv->name);
        self->priv->name = g_value_dup_string (value);
        break;

      case PROP_DOMAIN:
        g_free (self->priv->domain);
        self->priv->domain = g_value_dup_string (value);
        break;

      case PROP_SERVICE:
        g_free (self->priv->service);
        self->priv->service = g_value_dup_string (value);
        break;

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

static void
soup_discoverable_server_get_property (GObject    *object,
                                       guint       prop_id,
                                       GValue     *value,
                                       GParamSpec *pspec)
{
  SoupDiscoverableServer *self = SOUP_DISCOVERABLE_SERVER (object);

  switch (prop_id)
    {
      case PROP_NAME:
        g_value_set_string (value, self->priv->name);
        break;

      case PROP_DOMAIN:
        g_value_set_string (value, self->priv->domain);
        break;

      case PROP_SERVICE:
        g_value_set_string (value, self->priv->service);
        break;

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

static void
soup_discoverable_server_dispose (GObject *object)
{
  SoupDiscoverableServer *self = SOUP_DISCOVERABLE_SERVER (object);

  if (self->priv->startup_handler)
    {
      g_source_remove (self->priv->startup_handler);
      self->priv->startup_handler = 0;
    }

  if (self->priv->group)
    {
      avahi_entry_group_free (self->priv->group);
      self->priv->group = NULL;
    }

  if (self->priv->client)
    {
      avahi_client_free (self->priv->client);
      self->priv->client = NULL;
    }

  if (self->priv->poll)
    {
      avahi_glib_poll_free (self->priv->poll);
      self->priv->poll = NULL;
    }

  g_free (self->priv->name);
  self->priv->name = NULL;

  g_free (self->priv->domain);
  self->priv->domain = NULL;

  g_free (self->priv->service);
  self->priv->service = NULL;

  G_OBJECT_CLASS (soup_discoverable_server_parent_class)->dispose (object);
}

static void
soup_discoverable_server_class_init (SoupDiscoverableServerClass *cls)
{
  GObjectClass *oclass = G_OBJECT_CLASS (cls);

  oclass->set_property = soup_discoverable_server_set_property;
  oclass->get_property = soup_discoverable_server_get_property;
  oclass->dispose = soup_discoverable_server_dispose;

  g_object_class_install_property (oclass, PROP_NAME,
                                   g_param_spec_string ("name", "Name",
                                                        "User friendly name for this service", NULL,
                                                        G_PARAM_READWRITE | G_PARAM_CONSTRUCT |
                                                        G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
                                                        G_PARAM_STATIC_BLURB));
  g_object_class_install_property (oclass, PROP_DOMAIN,
                                   g_param_spec_string ("domain", "Domain",
                                                        "Internet domain for publishing this service", NULL,
                                                        G_PARAM_READWRITE | G_PARAM_CONSTRUCT |
                                                        G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
                                                        G_PARAM_STATIC_BLURB));
  g_object_class_install_property (oclass, PROP_SERVICE,
                                   g_param_spec_string ("service", "Service",
                                                        "Wellknown DNS-SD name this service", NULL,
                                                        G_PARAM_READWRITE | G_PARAM_CONSTRUCT |
                                                        G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
                                                        G_PARAM_STATIC_BLURB));

  g_type_class_add_private (cls, sizeof (SoupDiscoverableServerPrivate));
}

SoupServer*
soup_discoverable_server_new (const gchar *name,
                              const gchar *service)
{
  g_return_val_if_fail (NULL != name, NULL);
  g_return_val_if_fail (NULL != service, NULL);

  return g_object_new (SOUP_TYPE_DISCOVERABLE_SERVER,
                       SOUP_SERVER_PORT, SOUP_ADDRESS_ANY_PORT,
                       "name", name, "service", service, NULL);
}

void
soup_discoverable_server_set_name (SoupDiscoverableServer *server,
                                   const gchar            *name)
{
  g_return_if_fail (SOUP_IS_DISCOVERABLE_SERVER (server));
  g_object_set (server, "name", name, NULL);
}

G_CONST_RETURN char*
soup_discoverable_server_get_name (SoupDiscoverableServer *server)
{
  g_return_val_if_fail (SOUP_IS_DISCOVERABLE_SERVER (server), NULL);
  return server->priv->name;
}

void
soup_discoverable_server_set_domain (SoupDiscoverableServer *server,
                                     const gchar            *domain)
{
  g_return_if_fail (SOUP_IS_DISCOVERABLE_SERVER (server));
  g_object_set (server, "domain", domain, NULL);
}

G_CONST_RETURN char*
soup_discoverable_server_get_domain (SoupDiscoverableServer *server)
{
  g_return_val_if_fail (SOUP_IS_DISCOVERABLE_SERVER (server), NULL);
  return server->priv->domain;
}

void
soup_discoverable_server_set_service_name (SoupDiscoverableServer *server,
                                           const gchar            *service)
{
  g_return_if_fail (SOUP_IS_DISCOVERABLE_SERVER (server));
  g_object_set (server, "service-name", service, NULL);
}

G_CONST_RETURN char*
soup_discoverable_server_get_service_name (SoupDiscoverableServer *server)
{
  g_return_val_if_fail (SOUP_IS_DISCOVERABLE_SERVER (server), NULL);
  return server->priv->service;
}
