/*
     This file is part of GNUnet.
     Copyright (C) 2009--2013, 2016 GNUnet e.V.

     GNUnet is free software; you can redistribute it and/or modify
     it under the terms of the GNU General Public License as published
     by the Free Software Foundation; either version 3, or (at your
     option) any later version.

     GNUnet is distributed in the hope that it will be useful, but
     WITHOUT ANY WARRANTY; without even the implied warranty of
     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     General Public License for more details.

     You should have received a copy of the GNU General Public License
     along with GNUnet; see the file COPYING.  If not, write to the
     Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
     Boston, MA 02110-1301, USA.
*/

/**
 * @file dv/dv_api.c
 * @brief library to access the DV service
 * @author Christian Grothoff
 * @author Nathan Evans
 */
#include "platform.h"
#include "gnunet_util_lib.h"
#include "gnunet_dv_service.h"
#include "gnunet_protocols.h"
#include "dv.h"
#include "gnunet_transport_plugin.h"

#define LOG(kind,...) GNUNET_log_from (kind, "dv-api",__VA_ARGS__)


/**
 * Information we track for each peer.
 */
struct ConnectedPeer
{

  /**
   * Identity of the peer.
   */
  struct GNUNET_PeerIdentity pid;

};


/**
 * Handle to the DV service.
 */
struct GNUNET_DV_ServiceHandle
{

  /**
   * Connection to DV service.
   */
  struct GNUNET_MQ_Handle *mq;

  /**
   * Our configuration.
   */
  const struct GNUNET_CONFIGURATION_Handle *cfg;

  /**
   * Closure for the callbacks.
   */
  void *cls;

  /**
   * Function to call on connect events.
   */
  GNUNET_DV_ConnectCallback connect_cb;

  /**
   * Function to call on distance change events.
   */
  GNUNET_DV_DistanceChangedCallback distance_cb;

  /**
   * Function to call on disconnect events.
   */
  GNUNET_DV_DisconnectCallback disconnect_cb;

  /**
   * Function to call on receiving messages events.
   */
  GNUNET_DV_MessageReceivedCallback message_cb;

  /**
   * Information tracked per connected peer.  Maps peer
   * identities to `struct ConnectedPeer` entries.
   */
  struct GNUNET_CONTAINER_MultiPeerMap *peers;

};


/**
 * Disconnect and then reconnect to the DV service.
 *
 * @param sh service handle
 */
static void
reconnect (struct GNUNET_DV_ServiceHandle *sh);


/**
 * We got disconnected from the service and thus all of the
 * connections need to be torn down.
 *
 * @param cls the `struct GNUNET_DV_ServiceHandle`
 * @param key a peer identity
 * @param value a `struct ConnectedPeer` to clean up
 * @return #GNUNET_OK (continue to iterate)
 */
static int
cleanup_send_cb (void *cls,
		 const struct GNUNET_PeerIdentity *key,
		 void *value)
{
  struct GNUNET_DV_ServiceHandle *sh = cls;
  struct ConnectedPeer *peer = value;

  GNUNET_assert (GNUNET_YES ==
		 GNUNET_CONTAINER_multipeermap_remove (sh->peers,
						       key,
						       peer));
  sh->disconnect_cb (sh->cls,
                     key);
  GNUNET_free (peer);
  return GNUNET_OK;
}


/**
 * Handles a message sent from the DV service to us.
 * Parse it out and give it to the plugin.
 *
 * @param cls the handle to the DV API
 * @param cm the message that was received
 */
static void
handle_connect (void *cls,
                const struct GNUNET_DV_ConnectMessage *cm)
{
  struct GNUNET_DV_ServiceHandle *sh = cls;
  struct ConnectedPeer *peer;

  peer = GNUNET_CONTAINER_multipeermap_get (sh->peers,
                                            &cm->peer);
  if (NULL != peer)
  {
    GNUNET_break (0);
    reconnect (sh);
    return;
  }
  peer = GNUNET_new (struct ConnectedPeer);
  peer->pid = cm->peer;
  GNUNET_assert (GNUNET_OK ==
                 GNUNET_CONTAINER_multipeermap_put (sh->peers,
                                                    &peer->pid,
                                                    peer,
                                                    GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY));
  sh->connect_cb (sh->cls,
                  &cm->peer,
                  ntohl (cm->distance),
                  (enum GNUNET_ATS_Network_Type) ntohl (cm->network));
}


/**
 * Handles a message sent from the DV service to us.
 * Parse it out and give it to the plugin.
 *
 * @param cls the handle to the DV API
 * @param dm the message that was received
 */
static void
handle_disconnect (void *cls,
                   const struct GNUNET_DV_DisconnectMessage *dm)
{
  struct GNUNET_DV_ServiceHandle *sh = cls;
  struct ConnectedPeer *peer;

  peer = GNUNET_CONTAINER_multipeermap_get (sh->peers,
                                            &dm->peer);
  if (NULL == peer)
  {
    GNUNET_break (0);
    reconnect (sh);
    return;
  }
  cleanup_send_cb (sh,
                   &dm->peer,
                   peer);
}


/**
 * Handles a message sent from the DV service to us.
 * Parse it out and give it to the plugin.
 *
 * @param cls the handle to the DV API
 * @param msg the message that was received
 */
static void
handle_distance_update (void *cls,
                        const struct GNUNET_DV_DistanceUpdateMessage *dum)
{
  struct GNUNET_DV_ServiceHandle *sh = cls;
  struct ConnectedPeer *peer;

  peer = GNUNET_CONTAINER_multipeermap_get (sh->peers,
                                            &dum->peer);
  if (NULL == peer)
  {
    GNUNET_break (0);
    reconnect (sh);
    return;
  }
  sh->distance_cb (sh->cls,
                   &dum->peer,
                   ntohl (dum->distance),
                   (enum GNUNET_ATS_Network_Type) ntohl (dum->network));
}


/**
 * Handles a message sent from the DV service to us.
 * Parse it out and give it to the plugin.
 *
 * @param cls the handle to the DV API
 * @param rm the message that was received
 */
static int
check_received (void *cls,
                const struct GNUNET_DV_ReceivedMessage *rm)
{
  struct GNUNET_DV_ServiceHandle *sh = cls;
  const struct GNUNET_MessageHeader *payload;

  if (NULL ==
      GNUNET_CONTAINER_multipeermap_get (sh->peers,
                                         &rm->sender))
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  if (ntohs (rm->header.size) - sizeof (struct GNUNET_DV_ReceivedMessage) <
      sizeof (*payload))
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  payload = (const struct GNUNET_MessageHeader *) &rm[1];
  if (ntohs (rm->header.size) !=
      sizeof (struct GNUNET_DV_ReceivedMessage) + ntohs (payload->size))
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  return GNUNET_OK;
}


/**
 * Handles a message sent from the DV service to us.
 * Parse it out and give it to the plugin.
 *
 * @param cls the handle to the DV API
 * @param rm the message that was received
 */
static void
handle_received (void *cls,
                 const struct GNUNET_DV_ReceivedMessage *rm)
{
  struct GNUNET_DV_ServiceHandle *sh = cls;
  const struct GNUNET_MessageHeader *payload;

  payload = (const struct GNUNET_MessageHeader *) &rm[1];
  sh->message_cb (sh->cls,
                  &rm->sender,
                  ntohl (rm->distance),
                  payload);
}


/**
 * Generic error handler, called with the appropriate error code and
 * the same closure specified at the creation of the message queue.
 * Not every message queue implementation supports an error handler.
 *
 * @param cls closure with the `struct GNUNET_DV_ServiceHandle *`
 * @param error error code
 */
static void
mq_error_handler (void *cls,
                  enum GNUNET_MQ_Error error)
{
  struct GNUNET_DV_ServiceHandle *sh = cls;

  reconnect (sh);
}


/**
 * Disconnect and then reconnect to the DV service.
 *
 * @param sh service handle
 */
static void
reconnect (struct GNUNET_DV_ServiceHandle *sh)
{
  struct GNUNET_MQ_MessageHandler handlers[] = {
    GNUNET_MQ_hd_fixed_size (connect,
                             GNUNET_MESSAGE_TYPE_DV_CONNECT,
                             struct GNUNET_DV_ConnectMessage,
                             sh),
    GNUNET_MQ_hd_fixed_size (disconnect,
                             GNUNET_MESSAGE_TYPE_DV_DISCONNECT,
                             struct GNUNET_DV_DisconnectMessage,
                             sh),
    GNUNET_MQ_hd_fixed_size (distance_update,
                             GNUNET_MESSAGE_TYPE_DV_DISTANCE_CHANGED,
                             struct GNUNET_DV_DistanceUpdateMessage,
                             sh),
    GNUNET_MQ_hd_var_size (received,
                           GNUNET_MESSAGE_TYPE_DV_RECV,
                           struct GNUNET_DV_ReceivedMessage,
                           sh),
    GNUNET_MQ_handler_end ()
  };
  struct GNUNET_MessageHeader *sm;
  struct GNUNET_MQ_Envelope *env;

  if (NULL != sh->mq)
  {
    GNUNET_MQ_destroy (sh->mq);
    sh->mq = NULL;
  }
  GNUNET_CONTAINER_multipeermap_iterate (sh->peers,
					 &cleanup_send_cb,
					 sh);
  LOG (GNUNET_ERROR_TYPE_DEBUG,
       "Connecting to DV service\n");
  sh->mq = GNUNET_CLIENT_connect (sh->cfg,
                                  "dv",
                                  handlers,
                                  &mq_error_handler,
                                  sh);
  if (NULL == sh->mq)
  {
    GNUNET_break (0);
    return;
  }
  env = GNUNET_MQ_msg (sm,
                       GNUNET_MESSAGE_TYPE_DV_START);
  GNUNET_MQ_send (sh->mq,
                  env);
}


/**
 * Connect to the DV service.
 *
 * @param cfg configuration
 * @param cls closure for callbacks
 * @param connect_cb function to call on connects
 * @param distance_cb function to call if distances change
 * @param disconnect_cb function to call on disconnects
 * @param message_cb function to call if we receive messages
 * @return handle to access the service
 */
struct GNUNET_DV_ServiceHandle *
GNUNET_DV_service_connect (const struct GNUNET_CONFIGURATION_Handle *cfg,
			   void *cls,
			   GNUNET_DV_ConnectCallback connect_cb,
			   GNUNET_DV_DistanceChangedCallback distance_cb,
			   GNUNET_DV_DisconnectCallback disconnect_cb,
			   GNUNET_DV_MessageReceivedCallback message_cb)
{
  struct GNUNET_DV_ServiceHandle *sh;

  sh = GNUNET_new (struct GNUNET_DV_ServiceHandle);
  sh->cfg = cfg;
  sh->cls = cls;
  sh->connect_cb = connect_cb;
  sh->distance_cb = distance_cb;
  sh->disconnect_cb = disconnect_cb;
  sh->message_cb = message_cb;
  sh->peers = GNUNET_CONTAINER_multipeermap_create (128,
                                                    GNUNET_YES);
  reconnect (sh);
  return sh;
}


/**
 * Disconnect from DV service.
 *
 * @param sh service handle
 */
void
GNUNET_DV_service_disconnect (struct GNUNET_DV_ServiceHandle *sh)
{
  if (NULL == sh)
    return;
  if (NULL != sh->mq)
  {
    GNUNET_MQ_destroy (sh->mq);
    sh->mq = NULL;
  }
  GNUNET_CONTAINER_multipeermap_iterate (sh->peers,
					 &cleanup_send_cb,
					 sh);
  GNUNET_CONTAINER_multipeermap_destroy (sh->peers);
  GNUNET_free (sh);
}


/**
 * Send a message via DV service.
 *
 * @param sh service handle
 * @param target intended recpient
 * @param msg message payload
 */
void
GNUNET_DV_send (struct GNUNET_DV_ServiceHandle *sh,
		const struct GNUNET_PeerIdentity *target,
		const struct GNUNET_MessageHeader *msg)
{
  struct GNUNET_DV_SendMessage *sm;
  struct ConnectedPeer *peer;
  struct GNUNET_MQ_Envelope *env;

  if (ntohs (msg->size) + sizeof (*sm) >= GNUNET_MAX_MESSAGE_SIZE)
  {
    GNUNET_break (0);
    return;
  }
  LOG (GNUNET_ERROR_TYPE_DEBUG,
       "Asked to send %u bytes of type %u to %s\n",
       (unsigned int) ntohs (msg->size),
       (unsigned int) ntohs (msg->type),
       GNUNET_i2s (target));
  peer = GNUNET_CONTAINER_multipeermap_get (sh->peers,
                                            target);
  if (NULL == peer)
  {
    GNUNET_break (0);
    return;
  }
  GNUNET_assert (NULL != sh->mq);
  env = GNUNET_MQ_msg_nested_mh (sm,
                                 GNUNET_MESSAGE_TYPE_DV_SEND,
                                 msg);
  sm->target = *target;
  GNUNET_MQ_send (sh->mq,
                  env);
}


/* end of dv_api.c */
