/* GIO - GLib Input, Output and Streaming Library
 *
 * Copyright (C) 2008 Red Hat, Inc.
 *
 * 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 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 Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 *
 * Author: David Zeuthen <davidz@redhat.com>
 */

/*
 * TODO: - locking
 *       - cancellation
 *       - error handling
 */

#include <config.h>
#include <string.h>
#include <glib/gi18n-lib.h>

#include <avahi-client/client.h>
#include <avahi-client/lookup.h>
#include <avahi-common/error.h>
#include <avahi-common/timeval.h>
#include <avahi-glib/glib-watch.h>
#include <avahi-glib/glib-malloc.h>

#include <net/if.h>

#include "gvfsdnssdutils.h"
#include "gvfsdnssdresolver.h"

enum
{
  PROP_0,
  PROP_ENCODED_TRIPLE,
  PROP_REQUIRED_TXT_KEYS,
  PROP_SERVICE_NAME,
  PROP_SERVICE_TYPE,
  PROP_DOMAIN,
  PROP_TIMEOUT_MSEC,

  PROP_IS_RESOLVED,
  PROP_ADDRESS,
  PROP_INTERFACE,
  PROP_PORT,
  PROP_TXT_RECORDS,
};

enum {
    CHANGED,
    LAST_SIGNAL
};

static guint signals[LAST_SIGNAL] = { 0 };

struct _GVfsDnsSdResolver
{
  GObject parent_instance;

  char *encoded_triple;
  char *service_name;
  char *service_type;
  char *domain;
  char *required_txt_keys;
  char **required_txt_keys_broken_out;
  guint timeout_msec;

  gboolean is_resolved;
  char *address;
  gchar *interface;
  guint port;
  char **txt_records;

  AvahiServiceResolver *avahi_resolver;
  guint start_avahi_resolver_id;
};


struct _GVfsDnsSdResolverClass
{
  GObjectClass parent_class;

  /* signals */
  void (*changed) (GVfsDnsSdResolver *resolver);
};

G_DEFINE_TYPE (GVfsDnsSdResolver, g_vfs_dns_sd_resolver, G_TYPE_OBJECT);

static gboolean resolver_supports_mdns = FALSE;
static AvahiClient *global_client = NULL;
static gboolean avahi_initialized = FALSE;
static void free_global_avahi_client (void);
static AvahiClient *get_global_avahi_client (GError **error);

static void ensure_avahi_resolver (GVfsDnsSdResolver  *resolver);

static void service_resolver_cb (AvahiServiceResolver   *resolver,
                                 AvahiIfIndex            interface,
                                 AvahiProtocol           protocol,
                                 AvahiResolverEvent      event,
                                 const char             *name,
                                 const char             *type,
                                 const char             *domain,
                                 const char             *host_name,
                                 const AvahiAddress     *a,
                                 uint16_t                port,
                                 AvahiStringList        *txt,
                                 AvahiLookupResultFlags  flags,
                                 void                   *user_data);



static GList *resolvers = NULL;

static void
clear_avahi_data (GVfsDnsSdResolver *resolver);

static void
remove_client_from_resolver (GVfsDnsSdResolver *resolver)
{
  if (resolver->avahi_resolver != NULL)
    {
      avahi_service_resolver_free (resolver->avahi_resolver);
      resolver->avahi_resolver = NULL;
    }

  clear_avahi_data (resolver);
}

static void
add_client_to_resolver (GVfsDnsSdResolver *resolver)
{
  ensure_avahi_resolver (resolver);
}

/* Callback for state changes on the Client */
static void
avahi_client_callback (AvahiClient *client, AvahiClientState state, void *userdata)
{
  if (global_client == NULL)
    global_client = client;

  if (state == AVAHI_CLIENT_FAILURE)
    {
      if (avahi_client_errno (client) == AVAHI_ERR_DISCONNECTED)
        {
          free_global_avahi_client ();

          /* Attempt to reconnect */
          get_global_avahi_client (NULL);
        }
    }
  else if (state == AVAHI_CLIENT_S_RUNNING)
    {
      /* Start resolving again */
      g_list_foreach (resolvers, (GFunc) add_client_to_resolver, NULL);
    }
}

static void
free_global_avahi_client (void)
{
  /* Remove current resolvers */
  g_list_foreach (resolvers, (GFunc) remove_client_from_resolver, NULL);

  /* Destroy client */
  if (global_client)
    {
      avahi_client_free (global_client);
    }
  global_client = NULL;
  avahi_initialized = FALSE;
}

static AvahiClient *
get_global_avahi_client (GError **error)
{
  static AvahiGLibPoll *glib_poll = NULL;
  int avahi_error;

  if (!avahi_initialized)
    {
      avahi_initialized = TRUE;

      if (glib_poll == NULL)
        {
          avahi_set_allocator (avahi_glib_allocator ());
          glib_poll = avahi_glib_poll_new (NULL, G_PRIORITY_DEFAULT);
        }

      /* Create a new AvahiClient instance */
      global_client = avahi_client_new (avahi_glib_poll_get (glib_poll),
                                        AVAHI_CLIENT_NO_FAIL,
                                        avahi_client_callback,
                                        glib_poll,
                                        &avahi_error);

      if (global_client == NULL)
        {
          g_set_error (error,
                       G_IO_ERROR,
                       G_IO_ERROR_FAILED,
                       _("Error initializing Avahi: %s"),
                       avahi_strerror (avahi_error));
          goto out;
        }
    }

 out:

  return global_client;
}

static gboolean
start_avahi_resolver (gpointer user_data)
{
  GVfsDnsSdResolver *resolver = G_VFS_DNS_SD_RESOLVER (user_data);
  AvahiClient *avahi_client;

  avahi_client = get_global_avahi_client (NULL);
  if (avahi_client == NULL)
    goto out;

  resolver->avahi_resolver = avahi_service_resolver_new (avahi_client,
                                                         AVAHI_IF_UNSPEC,
                                                         AVAHI_PROTO_UNSPEC,
                                                         resolver->service_name,
                                                         resolver->service_type,
                                                         resolver->domain,
                                                         AVAHI_PROTO_UNSPEC,
                                                         0, /* AvahiLookupFlags */
                                                         service_resolver_cb,
                                                         resolver);

out:
  resolver->start_avahi_resolver_id = 0;
  g_object_unref (resolver);
  return FALSE;
}

static void
ensure_avahi_resolver (GVfsDnsSdResolver  *resolver)
{
  if (resolver->avahi_resolver != NULL || resolver->start_avahi_resolver_id != 0)
    return;

  resolver->start_avahi_resolver_id = g_idle_add (start_avahi_resolver, g_object_ref (resolver));
}

static void
g_vfs_dns_sd_resolver_get_property (GObject    *object,
                                    guint       prop_id,
                                    GValue     *value,
                                    GParamSpec *pspec)
{
  GVfsDnsSdResolver *resolver = G_VFS_DNS_SD_RESOLVER (object);

  switch (prop_id)
    {
    case PROP_ENCODED_TRIPLE:
      g_value_set_string (value, resolver->encoded_triple);
      break;

    case PROP_REQUIRED_TXT_KEYS:
      g_value_set_string (value, resolver->required_txt_keys);
      break;

    case PROP_SERVICE_NAME:
      g_value_set_string (value, resolver->service_name);
      break;

    case PROP_SERVICE_TYPE:
      g_value_set_string (value, resolver->service_type);
      break;

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

    case PROP_TIMEOUT_MSEC:
      g_value_set_uint (value, resolver->timeout_msec);
      break;

    case PROP_IS_RESOLVED:
      g_value_set_boolean (value, resolver->is_resolved);
      break;

    case PROP_ADDRESS:
      g_value_set_string (value, resolver->address);
      break;

    case PROP_INTERFACE:
      g_value_set_string (value, resolver->interface);
      break;

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

    case PROP_TXT_RECORDS:
      g_value_set_boxed (value, resolver->txt_records);
      break;

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

static void
g_vfs_dns_sd_resolver_set_property (GObject      *object,
                                    guint         prop_id,
                                    const GValue *value,
                                    GParamSpec   *pspec)
{
  GVfsDnsSdResolver *resolver = G_VFS_DNS_SD_RESOLVER (object);

  switch (prop_id)
    {
    case PROP_ENCODED_TRIPLE:
      resolver->encoded_triple = g_strdup (g_value_get_string (value));
      break;

    case PROP_REQUIRED_TXT_KEYS:
      resolver->required_txt_keys = g_strdup (g_value_get_string (value));
      if (resolver->required_txt_keys != NULL)
        {
          /* TODO: maybe support escaping ',' */
          resolver->required_txt_keys_broken_out = g_strsplit (resolver->required_txt_keys, ",", 0);
        }
      break;

    case PROP_SERVICE_NAME:
      resolver->service_name = g_strdup (g_value_get_string (value));
      break;

    case PROP_SERVICE_TYPE:
      resolver->service_type = g_strdup (g_value_get_string (value));
      break;

    case PROP_DOMAIN:
      resolver->domain = g_strdup (g_value_get_string (value));
      break;

    case PROP_TIMEOUT_MSEC:
      resolver->timeout_msec = g_value_get_uint (value);
      break;

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

static void
g_vfs_dns_sd_resolver_finalize (GObject *object)
{
  GVfsDnsSdResolver *resolver;

  resolver = G_VFS_DNS_SD_RESOLVER (object);

  g_free (resolver->encoded_triple);
  g_free (resolver->service_name);
  g_free (resolver->service_type);
  g_free (resolver->domain);
  g_free (resolver->required_txt_keys);
  g_strfreev (resolver->required_txt_keys_broken_out);

  g_free (resolver->address);
  g_free (resolver->interface);
  g_strfreev (resolver->txt_records);

  if (resolver->avahi_resolver != NULL)
    avahi_service_resolver_free (resolver->avahi_resolver);

  if (resolver->start_avahi_resolver_id != 0)
    g_source_remove (resolver->start_avahi_resolver_id);

  resolvers = g_list_remove (resolvers, resolver);

  /* free the global avahi client for the last resolver */
  if (resolvers == NULL)
    {
      free_global_avahi_client ();
    }

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

static void
g_vfs_dns_sd_resolver_constructed (GObject *object)
{
  GVfsDnsSdResolver *resolver;

  resolver = G_VFS_DNS_SD_RESOLVER (object);

  if (resolver->encoded_triple != NULL)
    {
      GError *error;

      if (resolver->service_name != NULL)
        {
          g_warning ("Ignoring service-name since encoded-triple is already set");
          g_free (resolver->service_name);
          resolver->service_name = NULL;
        }

      if (resolver->service_type != NULL)
        {
          g_warning ("Ignoring service-type since encoded-triple is already set");
          g_free (resolver->service_type);
          resolver->service_type = NULL;
        }

      if (resolver->domain != NULL)
        {
          g_warning ("Ignoring domain since encoded-triple is already set");
          g_free (resolver->domain);
          resolver->domain = NULL;
        }


      error = NULL;
      if (!g_vfs_decode_dns_sd_triple (resolver->encoded_triple,
                                       &(resolver->service_name),
                                       &(resolver->service_type),
                                       &(resolver->domain),
                                       &error))
        {
          /* GObject construction can't fail. So whine if the triple isn't valid. */
          g_warning ("Malformed construction data passed: %s", error->message);
          g_error_free (error);

          g_free (resolver->encoded_triple);
          g_free (resolver->service_name);
          g_free (resolver->service_type);
          g_free (resolver->domain);
          resolver->encoded_triple = NULL;
          resolver->service_name = NULL;
          resolver->service_type = NULL;
          resolver->domain = NULL;
          goto out;
        }
    }

  /* Always set encoded triple to what we encode; this is because we can decode
   * an encoded triple that isn't 100% properly URI encoded, e.g.
   *
   *  "davidz's public files on quad.fubar.dk._webdav._tcp.local"
   *
   * will be properly decoded. But we want to return a properly URI encoded triple
   *
   *  "davidz%27s%20public%20files%20on%20quad%2efubar%2edk._webdav._tcp.local"
   *
   * for e.g. setting the GMountSpec. This is useful because the use can
   * put the former into the pathbar in a file manager and then it will
   * be properly rewritten on mount.
   */
  g_free (resolver->encoded_triple);
  resolver->encoded_triple = g_vfs_encode_dns_sd_triple (resolver->service_name,
                                                         resolver->service_type,
                                                         resolver->domain);

  /* start resolving immediately */
  ensure_avahi_resolver (resolver);

  resolvers = g_list_prepend (resolvers, resolver);

 out:

  if (G_OBJECT_CLASS (g_vfs_dns_sd_resolver_parent_class)->constructed != NULL)
    G_OBJECT_CLASS (g_vfs_dns_sd_resolver_parent_class)->constructed (object);
}

static void
g_vfs_dns_sd_resolver_class_init (GVfsDnsSdResolverClass *klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);

  resolver_supports_mdns = (avahi_nss_support () > 0);

  gobject_class->get_property = g_vfs_dns_sd_resolver_get_property;
  gobject_class->set_property = g_vfs_dns_sd_resolver_set_property;
  gobject_class->finalize = g_vfs_dns_sd_resolver_finalize;
  gobject_class->constructed = g_vfs_dns_sd_resolver_constructed;

  /**
   * GVfsDnsSdResolver::changed:
   * @resolver: The resolver emitting the signal.
   *
   * Emitted when resolved data changes.
   */
  signals[CHANGED] = g_signal_new ("changed",
                                   G_VFS_TYPE_DNS_SD_RESOLVER,
                                   G_SIGNAL_RUN_LAST,
                                   G_STRUCT_OFFSET (GVfsDnsSdResolverClass, changed),
                                   NULL,
                                   NULL,
                                   g_cclosure_marshal_VOID__VOID,
                                   G_TYPE_NONE,
                                   0);


  /**
   * GVfsDnsSdResolver:encoded-triple:
   *
   * The encoded DNS-SD triple for the resolver.
   */
  g_object_class_install_property (gobject_class,
                                   PROP_ENCODED_TRIPLE,
                                   g_param_spec_string ("encoded-triple",
                                                        "Encoded triple",
                                                        "Encoded triple",
                                                        NULL,
                                                        G_PARAM_CONSTRUCT_ONLY |
                                                        G_PARAM_READWRITE |
                                                        G_PARAM_STATIC_NAME |
                                                        G_PARAM_STATIC_BLURB |
                                                        G_PARAM_STATIC_NICK));

  /**
   * GVfsDnsSdResolver:required-txt-keys:
   *
   * A comma separated list of keys that must appear in the TXT
   * records in order to consider the service being resolved.
   */
  g_object_class_install_property (gobject_class,
                                   PROP_REQUIRED_TXT_KEYS,
                                   g_param_spec_string ("required-txt-keys",
                                                        "Required TXT keys",
                                                        "Required TXT keys",
                                                        NULL,
                                                        G_PARAM_CONSTRUCT_ONLY |
                                                        G_PARAM_READWRITE |
                                                        G_PARAM_STATIC_NAME |
                                                        G_PARAM_STATIC_BLURB |
                                                        G_PARAM_STATIC_NICK));

  /**
   * GVfsDnsSdResolver:service-name:
   *
   * The name of the service for the resolver.
   */
  g_object_class_install_property (gobject_class,
                                   PROP_SERVICE_NAME,
                                   g_param_spec_string ("service-name",
                                                        "Service Name",
                                                        "Service Name",
                                                        NULL,
                                                        G_PARAM_CONSTRUCT_ONLY |
                                                        G_PARAM_READWRITE |
                                                        G_PARAM_STATIC_NAME |
                                                        G_PARAM_STATIC_BLURB |
                                                        G_PARAM_STATIC_NICK));

  /**
   * GVfsDnsSdResolver:service-type:
   *
   * The type of the service for the resolver.
   */
  g_object_class_install_property (gobject_class,
                                   PROP_SERVICE_TYPE,
                                   g_param_spec_string ("service-type",
                                                        "Service Type",
                                                        "Service Type",
                                                        NULL,
                                                        G_PARAM_CONSTRUCT_ONLY |
                                                        G_PARAM_READWRITE |
                                                        G_PARAM_STATIC_NAME |
                                                        G_PARAM_STATIC_BLURB |
                                                        G_PARAM_STATIC_NICK));

  /**
   * GVfsDnsSdResolver:domain:
   *
   * The domain for the resolver.
   */
  g_object_class_install_property (gobject_class,
                                   PROP_DOMAIN,
                                   g_param_spec_string ("domain",
                                                        "Domain",
                                                        "Domain",
                                                        NULL,
                                                        G_PARAM_CONSTRUCT_ONLY |
                                                        G_PARAM_READWRITE |
                                                        G_PARAM_STATIC_NAME |
                                                        G_PARAM_STATIC_BLURB |
                                                        G_PARAM_STATIC_NICK));

  /**
   * GVfsDnsSdResolver:timeout-msec:
   *
   * Timeout in milliseconds to use when resolving.
   */
  g_object_class_install_property (gobject_class,
                                   PROP_TIMEOUT_MSEC,
                                   g_param_spec_uint ("timeout-msec",
                                                      "Timeout in milliseconds",
                                                      "Timeout in milliseconds",
                                                      0,
                                                      G_MAXUINT,
                                                      5000,
                                                      G_PARAM_CONSTRUCT |
                                                      G_PARAM_READWRITE |
                                                      G_PARAM_STATIC_NAME |
                                                      G_PARAM_STATIC_BLURB |
                                                      G_PARAM_STATIC_NICK));

  /**
   * GVfsDnsSdResolver:is-resolved:
   *
   * Whether the service is resolved.
   */
  g_object_class_install_property (gobject_class,
                                   PROP_IS_RESOLVED,
                                   g_param_spec_boolean ("is-resolved",
                                                         "Is resolved",
                                                         "Is resolved",
                                                         FALSE,
                                                         G_PARAM_READABLE |
                                                         G_PARAM_STATIC_NAME |
                                                         G_PARAM_STATIC_BLURB |
                                                         G_PARAM_STATIC_NICK));

  /**
   * GVfsDnsSdResolver:address:
   *
   * The resolved address.
   */
  g_object_class_install_property (gobject_class,
                                   PROP_DOMAIN,
                                   g_param_spec_string ("address",
                                                        "Address",
                                                        "Address",
                                                        NULL,
                                                        G_PARAM_READABLE |
                                                        G_PARAM_STATIC_NAME |
                                                        G_PARAM_STATIC_BLURB |
                                                        G_PARAM_STATIC_NICK));

  /**
   * GVfsDnsSdResolver:interface:
   *
   * The resolved interface.
   */
  g_object_class_install_property (gobject_class,
                                   PROP_INTERFACE,
                                   g_param_spec_string ("interface",
                                                        "Interface",
                                                        "Interface",
                                                        NULL,
                                                        G_PARAM_READABLE |
                                                        G_PARAM_STATIC_NAME |
                                                        G_PARAM_STATIC_BLURB |
                                                        G_PARAM_STATIC_NICK));

  /**
   * GVfsDnsSdResolver:port:
   *
   * The resolved port.
   */
  g_object_class_install_property (gobject_class,
                                   PROP_PORT,
                                   g_param_spec_uint ("port",
                                                      "Port",
                                                      "Port",
                                                      0,
                                                      65536,
                                                      0,
                                                      G_PARAM_READABLE |
                                                      G_PARAM_STATIC_NAME |
                                                      G_PARAM_STATIC_BLURB |
                                                      G_PARAM_STATIC_NICK));

  /**
   * GVfsDnsSdResolver:txt-records:
   *
   * The resolved TXT records.
   */
  g_object_class_install_property (gobject_class,
                                   PROP_TXT_RECORDS,
                                   g_param_spec_boxed ("txt-records",
                                                       "TXT Records",
                                                       "TXT Records",
                                                       G_TYPE_STRV,
                                                       G_PARAM_READABLE |
                                                       G_PARAM_STATIC_NAME |
                                                       G_PARAM_STATIC_BLURB |
                                                       G_PARAM_STATIC_NICK));
}

static void
g_vfs_dns_sd_resolver_init (GVfsDnsSdResolver *resolver)
{
}

gboolean
g_vfs_dns_sd_resolver_is_resolved (GVfsDnsSdResolver *resolver)
{
  g_return_val_if_fail (G_VFS_IS_DNS_SD_RESOLVER (resolver), FALSE);
  return resolver->is_resolved;
}

const gchar *
g_vfs_dns_sd_resolver_get_encoded_triple (GVfsDnsSdResolver *resolver)
{
  g_return_val_if_fail (G_VFS_IS_DNS_SD_RESOLVER (resolver), NULL);
  return resolver->encoded_triple;
}

const gchar *
g_vfs_dns_sd_resolver_get_required_txt_keys (GVfsDnsSdResolver *resolver)
{
  g_return_val_if_fail (G_VFS_IS_DNS_SD_RESOLVER (resolver), NULL);
  return resolver->required_txt_keys;
}

const gchar *
g_vfs_dns_sd_resolver_get_service_name (GVfsDnsSdResolver *resolver)
{
  g_return_val_if_fail (G_VFS_IS_DNS_SD_RESOLVER (resolver), NULL);
  return resolver->service_name;
}

const gchar *
g_vfs_dns_sd_resolver_get_service_type (GVfsDnsSdResolver *resolver)
{
  g_return_val_if_fail (G_VFS_IS_DNS_SD_RESOLVER (resolver), NULL);
  return resolver->service_type;
}

const gchar *
g_vfs_dns_sd_resolver_get_domain (GVfsDnsSdResolver *resolver)
{
  g_return_val_if_fail (G_VFS_IS_DNS_SD_RESOLVER (resolver), NULL);
  return resolver->domain;
}

gchar *
g_vfs_dns_sd_resolver_get_address (GVfsDnsSdResolver *resolver)
{
  g_return_val_if_fail (G_VFS_IS_DNS_SD_RESOLVER (resolver), NULL);
  return g_strdup (resolver->address);
}

gchar *
g_vfs_dns_sd_resolver_get_interface (GVfsDnsSdResolver *resolver)
{
  g_return_val_if_fail (G_VFS_IS_DNS_SD_RESOLVER (resolver), NULL);
  return g_strdup (resolver->interface);
}

guint
g_vfs_dns_sd_resolver_get_port (GVfsDnsSdResolver *resolver)
{
  g_return_val_if_fail (G_VFS_IS_DNS_SD_RESOLVER (resolver), (guint) -1);
  return resolver->port;
}

gchar **
g_vfs_dns_sd_resolver_get_txt_records (GVfsDnsSdResolver *resolver)
{
  g_return_val_if_fail (G_VFS_IS_DNS_SD_RESOLVER (resolver), NULL);
  return g_strdupv (resolver->txt_records);
}

gchar *
g_vfs_dns_sd_resolver_lookup_txt_record (GVfsDnsSdResolver *resolver,
                                         const gchar       *key)
{
  gint n;
  gchar *result;
  gsize key_len;

  g_return_val_if_fail (G_VFS_IS_DNS_SD_RESOLVER (resolver), NULL);
  g_return_val_if_fail (key != NULL, NULL);

  result = NULL;


  if (resolver->txt_records == NULL)
    goto out;

  key_len = strlen (key);

  for (n = 0; resolver->txt_records[n] != NULL; n++)
    {
      const gchar *s = resolver->txt_records[n];
      const gchar *p;

      p = strchr (s, '=');
      if (p != NULL && (p - s) == key_len)
        {
          if (g_ascii_strncasecmp (s,
                                   key,
                                   p - s) == 0)
            {
              result = g_strdup (p + 1);
              goto out;
            }
        }
    }

 out:
  return result;
}

GVfsDnsSdResolver *
g_vfs_dns_sd_resolver_new_for_encoded_triple (const gchar *encoded_triple,
                                              const gchar *required_txt_keys)

{
  g_return_val_if_fail (encoded_triple != NULL, NULL);

  return G_VFS_DNS_SD_RESOLVER (g_object_new (G_VFS_TYPE_DNS_SD_RESOLVER,
                                              "encoded-triple", encoded_triple,
                                              "required-txt-keys", required_txt_keys,
                                              NULL));
}

GVfsDnsSdResolver *
g_vfs_dns_sd_resolver_new_for_service (const gchar *service_name,
                                       const gchar *service_type,
                                       const gchar *domain,
                                       const gchar *required_txt_keys)
{
  g_return_val_if_fail (service_name != NULL, NULL);
  g_return_val_if_fail (service_type != NULL, NULL);
  g_return_val_if_fail (domain != NULL, NULL);

  return G_VFS_DNS_SD_RESOLVER (g_object_new (G_VFS_TYPE_DNS_SD_RESOLVER,
                                              "service-name", service_name,
                                              "service-type", service_type,
                                              "domain", domain,
                                              "required-txt-keys", required_txt_keys,
                                              NULL));
}

static int
safe_strcmp (const char *a, const char *b)
{
  if (a == NULL)
    a = "";
  if (b == NULL)
    b = "";
  return strcmp (a, b);
}

static gboolean
strv_equal (char **a, char **b)
{
  static char *dummy[1] = {NULL};
  int n;
  gboolean ret;

  if (a == NULL)
    a = dummy;
  if (b == NULL)
    b = dummy;

  ret = FALSE;

  if (g_strv_length (a) != g_strv_length (b))
    goto out;

  for (n = 0; a[n] != NULL && b[n] != NULL; n++)
    {
      if (strcmp (a[n], b[n]) != 0)
        goto out;
    }

  ret = TRUE;

 out:
  return ret;

}

static gboolean
has_required_txt_keys (GVfsDnsSdResolver *resolver)
{
  gboolean ret;
  int n;
  char *value;

  ret = FALSE;

  if (resolver->required_txt_keys_broken_out != NULL)
    {
      for (n = 0; resolver->required_txt_keys_broken_out[n] != NULL; n++)
        {
          value = g_vfs_dns_sd_resolver_lookup_txt_record (resolver,
                                                           resolver->required_txt_keys_broken_out[n]);
          if (value == NULL)
            {
              /* key is missing */
              goto out;
            }
          g_free (value);
        }
    }

  ret = TRUE;

 out:
  return ret;
}

static void
set_avahi_data (GVfsDnsSdResolver    *resolver,
                const char           *host_name,
                AvahiProtocol         protocol,
                const AvahiAddress   *a,
                AvahiIfIndex          interface,
                uint16_t              port,
                AvahiStringList      *txt)
{
  char *address;
  gchar ifname[IF_NAMESIZE] = {0};
  gboolean changed;
  AvahiStringList *l;
  GPtrArray *p;
  char **txt_records;
  gboolean is_resolved;

  changed = FALSE;

  if (resolver_supports_mdns)
    {
      address = g_strdup (host_name);
    }
  else
    {
      char aa[128];

      avahi_address_snprint (aa, sizeof(aa), a);
      if (protocol == AVAHI_PROTO_INET6)
        {
          /* an ipv6 address, follow RFC 2732 */
          address = g_strdup_printf ("[%s]", aa);
        }
      else
        {
          address = g_strdup (aa);
        }
    }

  if (safe_strcmp (resolver->address, address) != 0)
    {
      g_free (resolver->address);
      resolver->address = g_strdup (address);
      g_object_notify (G_OBJECT (resolver), "address");
      changed = TRUE;
    }

  g_free (address);

  if_indextoname (interface, ifname);
  if (safe_strcmp (resolver->interface, ifname) != 0)
    {
      g_free (resolver->interface);
      resolver->interface = g_strdup (ifname);
      g_object_notify (G_OBJECT (resolver), "interface");
      changed = TRUE;
    }

  if (resolver->port != port)
    {
      resolver->port = port;
      g_object_notify (G_OBJECT (resolver), "port");
      changed = TRUE;
    }

  p = g_ptr_array_new ();
  for (l = txt; l != NULL; l = avahi_string_list_get_next (l))
    {
      g_ptr_array_add (p, g_strdup ((const char *) l->text));
    }
  g_ptr_array_add (p, NULL);
  txt_records = (char **) g_ptr_array_free (p, FALSE);

  if (strv_equal (resolver->txt_records, txt_records))
    {
      g_strfreev (txt_records);
    }
  else
    {
      g_strfreev (resolver->txt_records);
      resolver->txt_records = txt_records;
      g_object_notify (G_OBJECT (resolver), "txt-records");
      changed = TRUE;
    }

  is_resolved = has_required_txt_keys (resolver);

  if (is_resolved != resolver->is_resolved)
    {
      resolver->is_resolved = is_resolved;
      g_object_notify (G_OBJECT (resolver), "is-resolved");
      changed = TRUE;
    }

  if (changed)
    g_signal_emit (resolver, signals[CHANGED], 0);
}

static void
clear_avahi_data (GVfsDnsSdResolver *resolver)
{
  gboolean changed;

  changed = FALSE;

  if (resolver->is_resolved)
    {
      resolver->is_resolved = FALSE;
      g_object_notify (G_OBJECT (resolver), "is-resolved");
      changed = TRUE;
    }

  if (resolver->address != NULL)
    {
      g_free (resolver->address);
      resolver->address = NULL;
      g_object_notify (G_OBJECT (resolver), "address");
      changed = TRUE;
    }

  if (resolver->interface != NULL)
    {
      g_free (resolver->interface);
      resolver->interface = NULL;
      g_object_notify (G_OBJECT (resolver), "interface");
      changed = TRUE;
    }

  if (resolver->port != 0)
    {
      resolver->port = 0;
      g_object_notify (G_OBJECT (resolver), "port");
      changed = TRUE;
    }

  if (resolver->txt_records != NULL)
    {
      resolver->txt_records = NULL;
      g_object_notify (G_OBJECT (resolver), "txt-records");
      changed = TRUE;
    }

  if (changed)
    g_signal_emit (resolver, signals[CHANGED], 0);
}

static void
service_resolver_cb (AvahiServiceResolver   *avahi_resolver,
                     AvahiIfIndex            interface,
                     AvahiProtocol           protocol,
                     AvahiResolverEvent      event,
                     const char             *name,
                     const char             *type,
                     const char             *domain,
                     const char             *host_name,
                     const AvahiAddress     *a,
                     uint16_t                port,
                     AvahiStringList        *txt,
                     AvahiLookupResultFlags  flags,
                     void                   *user_data)
{
  GVfsDnsSdResolver *resolver = G_VFS_DNS_SD_RESOLVER (user_data);

  switch (event)
    {
    case AVAHI_RESOLVER_FOUND:
      set_avahi_data (resolver,
                      host_name,
                      protocol,
                      a,
                      interface,
                      port,
                      txt);
      break;

    case AVAHI_RESOLVER_FAILURE:
      clear_avahi_data (resolver);
      break;
    }
}


typedef struct
{
  GVfsDnsSdResolver *resolver;
  guint timeout_id;
} ResolveData;

static void service_resolver_changed (GVfsDnsSdResolver *resolver, GTask *task);

static void
resolve_data_free (ResolveData *data)
{
  if (data->timeout_id > 0)
    g_source_remove (data->timeout_id);
  g_signal_handlers_disconnect_by_func (data->resolver, service_resolver_changed, data);
  g_free (data);
}

static void
service_resolver_changed (GVfsDnsSdResolver *resolver,
                          GTask *task)
{
  ResolveData *data = g_task_get_task_data (task);

  if (resolver->is_resolved)
    {
      g_task_return_boolean (task, TRUE);
      g_object_unref (task);
    }
  else
    {
      if (data->resolver->address != NULL)
        {
          /* keep going until timeout if we're missing TXT records */
        }
      else
        {
          g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED,
          /* Translators:
           * - the first %s refers to the service type
           * - the second %s refers to the service name
           * - the third %s refers to the domain
           */
                                   _("Error resolving “%s” service “%s” on domain “%s”"),
                                   data->resolver->service_type,
                                   data->resolver->service_name,
                                   data->resolver->domain);
          g_object_unref (task);
        }
    }
}

static gboolean
service_resolver_timed_out (GTask *task)
{
  ResolveData *data = g_task_get_task_data (task);

  if (data->resolver->address != NULL)
    {
      /* special case if one of the required TXT records are missing */
      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED,
      /* Translators:
       * - the first %s refers to the service type
       * - the second %s refers to the service name
       * - the third %s refers to the domain
       * - the fourth %s refers to the required TXT keys
       */
                               _("Error resolving “%s” service “%s” on domain “%s”. "
                                 "One or more TXT records are missing. Keys required: “%s”."),
                               data->resolver->service_type,
                               data->resolver->service_name,
                               data->resolver->domain,
                               data->resolver->required_txt_keys);
    }
  else
    {
      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_TIMED_OUT,
      /* Translators:
       * - the first %s refers to the service type
       * - the second %s refers to the service name
       * - the third %s refers to the domain
       */
                               _("Timed out resolving “%s” service “%s” on domain “%s”"),
                               data->resolver->service_type,
                               data->resolver->service_name,
                               data->resolver->domain);
    }

  data->timeout_id = 0;
  g_object_unref (task);

  return FALSE;
}

gboolean
g_vfs_dns_sd_resolver_resolve_finish (GVfsDnsSdResolver  *resolver,
                                      GAsyncResult       *res,
                                      GError            **error)
{
  g_return_val_if_fail (g_task_is_valid (res, resolver), FALSE);
  g_return_val_if_fail (g_async_result_is_tagged (res, g_vfs_dns_sd_resolver_resolve), FALSE);

  return g_task_propagate_boolean (G_TASK (res), error);
}

void
g_vfs_dns_sd_resolver_resolve (GVfsDnsSdResolver  *resolver,
                               GCancellable       *cancellable,
                               GAsyncReadyCallback callback,
                               gpointer            user_data)
{
  ResolveData *data;
  GTask *task;

  g_return_if_fail (G_VFS_IS_DNS_SD_RESOLVER (resolver));

  task = g_task_new (resolver, cancellable, callback, user_data);
  g_task_set_source_tag (task, g_vfs_dns_sd_resolver_resolve);

  if (resolver->service_type == NULL)
    {
      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED,
                               _("Error initializing Avahi resolver"));
      g_object_unref (task);
      return;
    }

  if (resolver->is_resolved)
    {
      g_task_return_boolean (task, TRUE);
      g_object_unref (task);
      return;
    }

  ensure_avahi_resolver (resolver);

  data = g_new0 (ResolveData, 1);
  data->resolver = resolver;
  data->timeout_id = g_timeout_add (resolver->timeout_msec,
                                    (GSourceFunc) service_resolver_timed_out,
                                    task);

  g_task_set_task_data (task, data, (GDestroyNotify) resolve_data_free);

  g_signal_connect (resolver,
                    "changed",
                    (GCallback) service_resolver_changed,
                    task);
}


typedef struct
{
  GMutex mutex;
  GCond cond;
  gboolean done;
  GError *error;
  gboolean ret;
} ResolveDataSync;

static void
resolve_sync_cb (GVfsDnsSdResolver *resolver,
                 GAsyncResult      *res,
                 ResolveDataSync   *data)
{
  data->ret = g_vfs_dns_sd_resolver_resolve_finish (resolver,
                                                    res,
                                                    &(data->error));
  g_mutex_lock (&data->mutex);
  data->done = TRUE;
  g_cond_signal (&data->cond);
  g_mutex_unlock (&data->mutex);
}

/* Do not call from the global main loop thread. */
gboolean
g_vfs_dns_sd_resolver_resolve_sync (GVfsDnsSdResolver  *resolver,
                                    GCancellable       *cancellable,
                                    GError            **error)
{
  ResolveDataSync *data;
  gboolean ret;

  g_return_val_if_fail (G_VFS_IS_DNS_SD_RESOLVER (resolver), FALSE);

  data = g_new0 (ResolveDataSync, 1);
  g_cond_init (&data->cond);
  g_mutex_init (&data->mutex);

  g_mutex_lock (&data->mutex);
  g_vfs_dns_sd_resolver_resolve (resolver,
                                 cancellable,
                                 (GAsyncReadyCallback) resolve_sync_cb,
                                 data);

  while (!data->done)
    g_cond_wait (&data->cond, &data->mutex);
  g_mutex_unlock (&data->mutex);

  ret = data->ret;
  if (data->error != NULL)
    g_propagate_error (error, data->error);

  g_mutex_clear (&data->mutex);
  g_cond_clear (&data->cond);
  g_free (data);

  return ret;
}
