/*
   This file is part of GNUnet.
   Copyright (C) 2011-2015 GNUnet e.V.

   GNUnet is free software: you can redistribute it and/or modify it
   under the terms of the GNU Affero General Public License as published
   by the Free Software Foundation, either version 3 of the License,
   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
   Affero General Public License for more details.

   You should have received a copy of the GNU Affero General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.

     SPDX-License-Identifier: AGPL3.0-or-later
 */

/**
 * @file ats/gnunet-service-ats_addresses.c
 * @brief ats service address management
 * @author Matthias Wachs
 * @author Christian Grothoff
 */
#include "platform.h"
#include "gnunet-service-ats_addresses.h"
#include "gnunet-service-ats_performance.h"
#include "gnunet-service-ats_normalization.h"
#include "gnunet-service-ats_plugins.h"


/**
 * A multihashmap to store all addresses
 */
struct GNUNET_CONTAINER_MultiPeerMap *GSA_addresses;


/**
 * Update statistic on number of addresses.
 */
static void
update_addresses_stat ()
{
  GNUNET_STATISTICS_set (GSA_stats,
                         "# addresses",
                         GNUNET_CONTAINER_multipeermap_size (GSA_addresses),
                         GNUNET_NO);
}


/**
 * Free the given address
 *
 * @param addr address to destroy
 */
static void
free_address (struct ATS_Address *addr)
{
  GNUNET_assert (GNUNET_YES ==
                 GNUNET_CONTAINER_multipeermap_remove (GSA_addresses,
                                                       &addr->peer,
                                                       addr));
  update_addresses_stat ();
  GAS_plugin_delete_address (addr);
  GAS_performance_notify_all_clients (&addr->peer,
                                      addr->plugin,
                                      addr->addr,
                                      addr->addr_len,
                                      GNUNET_NO,
                                      NULL,
                                      addr->local_address_info,
                                      GNUNET_BANDWIDTH_ZERO,
                                      GNUNET_BANDWIDTH_ZERO);
  GNUNET_free (addr->plugin);
  GNUNET_free (addr);
}


/**
 * Initialize @a norm.  Sets all historic values to undefined.
 *
 * @param norm normalization data to initialize
 */
static void
init_norm (struct GAS_NormalizationInfo *norm)
{
  unsigned int c;

  for (c = 0; c < GAS_normalization_queue_length; c++)
    norm->atsi_abs[c] = UINT64_MAX;
}


/**
 * Create a ATS_address with the given information
 *
 * @param peer peer
 * @param plugin_name plugin
 * @param plugin_addr address
 * @param plugin_addr_len address length
 * @param local_address_info additional local info for the address
 * @param session_id session identifier, can never be 0
 * @return the ATS_Address
 */
static struct ATS_Address *
create_address (const struct GNUNET_PeerIdentity *peer,
                const char *plugin_name,
                const void *plugin_addr,
                size_t plugin_addr_len,
                uint32_t local_address_info,
                uint32_t session_id)
{
  struct ATS_Address *aa;

  aa = GNUNET_malloc (sizeof(struct ATS_Address) + plugin_addr_len);
  aa->peer = *peer;
  aa->addr_len = plugin_addr_len;
  aa->addr = &aa[1];
  GNUNET_memcpy (&aa[1],
                 plugin_addr,
                 plugin_addr_len);
  aa->plugin = GNUNET_strdup (plugin_name);
  aa->session_id = session_id;
  aa->local_address_info = local_address_info;
  init_norm (&aa->norm_delay);
  init_norm (&aa->norm_distance);
  init_norm (&aa->norm_utilization_in);
  init_norm (&aa->norm_utilization_out);
  return aa;
}


/**
 * Closure for #find_address_cb()
 */
struct FindAddressContext
{
  /**
   * Session Id to look for.
   */
  uint32_t session_id;

  /**
   * Where to store matching address result.
   */
  struct ATS_Address *exact_address;
};


/**
 * Find session matching given session ID.
 *
 * @param cls a `struct FindAddressContext`
 * @param key peer id
 * @param value the address to compare with
 * @return #GNUNET_YES to continue, #GNUNET_NO if address is found
 */
static int
find_address_cb (void *cls,
                 const struct GNUNET_PeerIdentity *key,
                 void *value)
{
  struct FindAddressContext *fac = cls;
  struct ATS_Address *aa = value;

  if (aa->session_id == fac->session_id)
  {
    fac->exact_address = aa;
    return GNUNET_NO;
  }
  return GNUNET_YES;
}


/**
 * Find the exact address
 *
 * @param peer peer
 * @param session_id session id, can never be 0
 * @return an ATS_address or NULL
 */
static struct ATS_Address *
find_exact_address (const struct GNUNET_PeerIdentity *peer,
                    uint32_t session_id)
{
  struct FindAddressContext fac;

  fac.exact_address = NULL;
  fac.session_id = session_id;
  GNUNET_CONTAINER_multipeermap_get_multiple (GSA_addresses,
                                              peer,
                                              &find_address_cb, &fac);
  return fac.exact_address;
}


/**
 * Add a new address for a peer.
 *
 * @param peer peer
 * @param plugin_name transport plugin name
 * @param plugin_addr plugin address
 * @param plugin_addr_len length of the plugin address in @a plugin_addr
 * @param local_address_info the local address for the address
 * @param session_id session id, can be 0
 * @param prop performance information for this address
 */
void
GAS_addresses_add (const struct GNUNET_PeerIdentity *peer,
                   const char *plugin_name,
                   const void *plugin_addr,
                   size_t plugin_addr_len,
                   uint32_t local_address_info,
                   uint32_t session_id,
                   const struct GNUNET_ATS_Properties *prop)
{
  struct ATS_Address *new_address;

  if (NULL != find_exact_address (peer,
                                  session_id))
  {
    GNUNET_break (0);
    return;
  }
  GNUNET_break (GNUNET_NT_UNSPECIFIED != prop->scope);
  new_address = create_address (peer,
                                plugin_name,
                                plugin_addr,
                                plugin_addr_len,
                                local_address_info,
                                session_id);
  /* Add a new address */
  new_address->properties = *prop;
  new_address->t_added = GNUNET_TIME_absolute_get ();
  new_address->t_last_activity = GNUNET_TIME_absolute_get ();
  GNUNET_assert (GNUNET_OK ==
                 GNUNET_CONTAINER_multipeermap_put (GSA_addresses,
                                                    peer,
                                                    new_address,
                                                    GNUNET_CONTAINER_MULTIHASHMAPOPTION_MULTIPLE));
  update_addresses_stat ();
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Adding new address for peer `%s' slot %u\n",
              GNUNET_i2s (peer),
              session_id);
  /* Tell solver about new address */
  GAS_plugin_solver_lock ();
  GAS_plugin_new_address (new_address);
  GAS_normalization_update_property (new_address);  // FIXME: needed?
  GAS_plugin_solver_unlock ();
  /* Notify performance clients about new address */
  GAS_performance_notify_all_clients (&new_address->peer,
                                      new_address->plugin,
                                      new_address->addr,
                                      new_address->addr_len,
                                      new_address->active,
                                      &new_address->properties,
                                      new_address->local_address_info,
                                      GNUNET_BANDWIDTH_value_init (
                                        new_address->assigned_bw_out),
                                      GNUNET_BANDWIDTH_value_init (
                                        new_address->assigned_bw_in));
}


/**
 * Update an address with new performance information for a peer.
 *
 * @param peer peer
 * @param session_id session id, never 0
 * @param prop performance information for this address
 */
void
GAS_addresses_update (const struct GNUNET_PeerIdentity *peer,
                      uint32_t session_id,
                      const struct GNUNET_ATS_Properties *prop)
{
  struct ATS_Address *aa;

  /* Get existing address */
  aa = find_exact_address (peer,
                           session_id);
  if (NULL == aa)
  {
    GNUNET_break (0);
    return;
  }
  if (NULL == aa->solver_information)
  {
    GNUNET_break (0);
    return;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Received ADDRESS_UPDATE for peer `%s' slot %u\n",
              GNUNET_i2s (peer),
              (unsigned int) session_id);
  GNUNET_break (GNUNET_NT_UNSPECIFIED != prop->scope);
  /* Update address */
  aa->t_last_activity = GNUNET_TIME_absolute_get ();
  aa->properties = *prop;
  /* Notify performance clients about updated address */
  GAS_performance_notify_all_clients (&aa->peer,
                                      aa->plugin,
                                      aa->addr,
                                      aa->addr_len,
                                      aa->active,
                                      prop,
                                      aa->local_address_info,
                                      GNUNET_BANDWIDTH_value_init (
                                        aa->assigned_bw_out),
                                      GNUNET_BANDWIDTH_value_init (
                                        aa->assigned_bw_in));

  GAS_normalization_update_property (aa);
}


/**
 * Remove an address for a peer.
 *
 * @param peer peer
 * @param session_id session id, can never be 0
 */
void
GAS_addresses_destroy (const struct GNUNET_PeerIdentity *peer,
                       uint32_t session_id)
{
  struct ATS_Address *ea;

  /* Get existing address */
  ea = find_exact_address (peer,
                           session_id);
  if (NULL == ea)
  {
    GNUNET_break (0);
    return;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Received ADDRESS_DESTROYED for peer `%s' session %u\n",
              GNUNET_i2s (peer),
              session_id);
  free_address (ea);
}


/**
 * Initialize address subsystem. The addresses subsystem manages the addresses
 * known and current performance information. It has a solver component
 * responsible for the resource allocation. It tells the solver about changes
 * and receives updates when the solver changes the resource allocation.
 */
void
GAS_addresses_init ()
{
  GSA_addresses
    = GNUNET_CONTAINER_multipeermap_create (128,
                                            GNUNET_NO);
  update_addresses_stat ();
}


/**
 * Destroy all addresses iterator
 *
 * @param cls NULL
 * @param key peer identity (unused)
 * @param value the 'struct ATS_Address' to free
 * @return #GNUNET_OK (continue to iterate)
 */
static int
destroy_all_address_it (void *cls,
                        const struct GNUNET_PeerIdentity *key,
                        void *value)
{
  struct ATS_Address *aa = value;

  free_address (aa);
  return GNUNET_OK;
}


/**
 * Remove all addresses
 */
void
GAS_addresses_destroy_all ()
{
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Destroying all addresses\n");
  if (0 ==
      GNUNET_CONTAINER_multipeermap_size (GSA_addresses))
    return;
  GAS_plugin_solver_lock ();
  GNUNET_CONTAINER_multipeermap_iterate (GSA_addresses,
                                         &destroy_all_address_it,
                                         NULL);
  GAS_plugin_solver_unlock ();
}


/**
 * Shutdown address subsystem.
 */
void
GAS_addresses_done ()
{
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Shutting down addresses\n");
  GAS_plugin_solver_lock ();
  GAS_addresses_destroy_all ();
  GAS_plugin_solver_unlock ();
  GNUNET_CONTAINER_multipeermap_destroy (GSA_addresses);
  GSA_addresses = NULL;
}


/**
 * Closure for #peerinfo_it().
 */
struct PeerInfoIteratorContext
{
  /**
   * Function to call for each address.
   */
  GNUNET_ATS_PeerInfo_Iterator it;

  /**
   * Closure for @e it.
   */
  void *it_cls;
};


/**
 * Iterator to iterate over a peer's addresses
 *
 * @param cls a `struct PeerInfoIteratorContext`
 * @param key the peer id
 * @param value the `struct ATS_address`
 * @return #GNUNET_OK to continue
 */
static int
peerinfo_it (void *cls,
             const struct GNUNET_PeerIdentity *key,
             void *value)
{
  struct PeerInfoIteratorContext *pi_ctx = cls;
  struct ATS_Address *addr = value;

  pi_ctx->it (pi_ctx->it_cls,
              &addr->peer,
              addr->plugin,
              addr->addr,
              addr->addr_len,
              addr->active,
              &addr->properties,
              addr->local_address_info,
              GNUNET_BANDWIDTH_value_init (addr->assigned_bw_out),
              GNUNET_BANDWIDTH_value_init (addr->assigned_bw_in));
  return GNUNET_OK;
}


/**
 * Return information all peers currently known to ATS
 *
 * @param peer the respective peer, NULL for 'all' peers
 * @param pi_it the iterator to call for every peer
 * @param pi_it_cls the closure for @a pi_it
 */
void
GAS_addresses_get_peer_info (const struct GNUNET_PeerIdentity *peer,
                             GNUNET_ATS_PeerInfo_Iterator pi_it,
                             void *pi_it_cls)
{
  struct PeerInfoIteratorContext pi_ctx;

  if (NULL == pi_it)
  {
    /* does not make sense without callback */
    GNUNET_break (0);
    return;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Returning information for %s from a total of %u known addresses\n",
              (NULL == peer)
              ? "all peers"
              : GNUNET_i2s (peer),
              (unsigned int) GNUNET_CONTAINER_multipeermap_size (
                GSA_addresses));
  pi_ctx.it = pi_it;
  pi_ctx.it_cls = pi_it_cls;
  if (NULL == peer)
    GNUNET_CONTAINER_multipeermap_iterate (GSA_addresses,
                                           &peerinfo_it,
                                           &pi_ctx);
  else
    GNUNET_CONTAINER_multipeermap_get_multiple (GSA_addresses,
                                                peer,
                                                &peerinfo_it, &pi_ctx);
  pi_it (pi_it_cls,
         NULL, NULL, NULL, 0,
         GNUNET_NO,
         NULL,
         GNUNET_HELLO_ADDRESS_INFO_NONE,
         GNUNET_BANDWIDTH_ZERO,
         GNUNET_BANDWIDTH_ZERO);
}


/**
 * Information we need for the callbacks to return a list of addresses
 * back to the client.
 */
struct AddressIteration
{
  /**
   * Actual handle to the client.
   */
  struct GNUNET_SERVICE_Client *client;

  /**
   * Are we sending all addresses, or only those that are active?
   */
  int all;

  /**
   * Which ID should be included in the response?
   */
  uint32_t id;
};


/**
 * Send a #GNUNET_MESSAGE_TYPE_ATS_ADDRESSLIST_RESPONSE with the
 * given address details to the client identified in @a ai.
 *
 * @param ai our address information context (identifies the client)
 * @param id the peer id this address is for
 * @param plugin_name name of the plugin that supports this address
 * @param plugin_addr address
 * @param plugin_addr_len length of @a plugin_addr
 * @param active #GNUNET_YES if this address is actively used
 * @param prop performance information
 * @param local_address_info flags for the address
 * @param bandwidth_out current outbound bandwidth assigned to address
 * @param bandwidth_in current inbound bandwidth assigned to address
 */
static void
transmit_req_addr (struct AddressIteration *ai,
                   const struct GNUNET_PeerIdentity *id,
                   const char *plugin_name,
                   const void *plugin_addr,
                   size_t plugin_addr_len,
                   int active,
                   const struct GNUNET_ATS_Properties *prop,
                   enum GNUNET_HELLO_AddressInfo local_address_info,
                   struct GNUNET_BANDWIDTH_Value32NBO bandwidth_out,
                   struct GNUNET_BANDWIDTH_Value32NBO bandwidth_in)

{
  struct GNUNET_MQ_Envelope *env;
  struct PeerInformationMessage *msg;
  char *addrp;
  size_t plugin_name_length;
  size_t msize;

  if (NULL != plugin_name)
    plugin_name_length = strlen (plugin_name) + 1;
  else
    plugin_name_length = 0;
  msize = plugin_addr_len + plugin_name_length;

  GNUNET_assert (sizeof(struct PeerInformationMessage) + msize
                 < GNUNET_MAX_MESSAGE_SIZE);
  env = GNUNET_MQ_msg_extra (msg,
                             msize,
                             GNUNET_MESSAGE_TYPE_ATS_ADDRESSLIST_RESPONSE);
  msg->id = htonl (ai->id);
  if (NULL != id)
    msg->peer = *id;
  msg->address_length = htons (plugin_addr_len);
  msg->address_active = ntohl (active);
  msg->plugin_name_length = htons (plugin_name_length);
  msg->bandwidth_out = bandwidth_out;
  msg->bandwidth_in = bandwidth_in;
  if (NULL != prop)
    GNUNET_ATS_properties_hton (&msg->properties,
                                prop);
  msg->address_local_info = htonl ((uint32_t) local_address_info);
  addrp = (char *) &msg[1];
  GNUNET_memcpy (addrp,
                 plugin_addr,
                 plugin_addr_len);
  if (NULL != plugin_name)
    strcpy (&addrp[plugin_addr_len],
            plugin_name);
  GNUNET_MQ_send (GNUNET_SERVICE_client_get_mq (ai->client),
                  env);
}


/**
 * Iterator for #GAS_addresses_get_peer_info(), called with peer-specific
 * information to be passed back to the client.
 *
 * @param cls closure with our `struct AddressIteration *`
 * @param id the peer id
 * @param plugin_name plugin name
 * @param plugin_addr address
 * @param plugin_addr_len length of @a plugin_addr
 * @param active is address actively used
 * @param prop performance information
 * @param local_address_info additional local info for the address
 * @param bandwidth_out current outbound bandwidth assigned to address
 * @param bandwidth_in current inbound bandwidth assigned to address
 */
static void
req_addr_peerinfo_it (void *cls,
                      const struct GNUNET_PeerIdentity *id,
                      const char *plugin_name,
                      const void *plugin_addr,
                      size_t plugin_addr_len,
                      int active,
                      const struct GNUNET_ATS_Properties *prop,
                      enum GNUNET_HELLO_AddressInfo local_address_info,
                      struct GNUNET_BANDWIDTH_Value32NBO bandwidth_out,
                      struct GNUNET_BANDWIDTH_Value32NBO bandwidth_in)
{
  struct AddressIteration *ai = cls;

  if ((NULL == id) &&
      (NULL == plugin_name) &&
      (NULL == plugin_addr))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
                "Address iteration done for one peer\n");
    return;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Callback for %s peer `%s' plugin `%s' BW out %u, BW in %u\n",
              (active == GNUNET_YES) ? "ACTIVE" : "INACTIVE",
              GNUNET_i2s (id),
              plugin_name,
              (unsigned int) ntohl (bandwidth_out.value__),
              (unsigned int) ntohl (bandwidth_in.value__));
  /* Transmit result (either if address is active, or if
     client wanted all addresses) */
  if ((GNUNET_YES != ai->all) &&
      (GNUNET_YES != active))
    return;
  transmit_req_addr (ai,
                     id,
                     plugin_name,
                     plugin_addr, plugin_addr_len,
                     active,
                     prop,
                     local_address_info,
                     bandwidth_out,
                     bandwidth_in);
}


/**
 * Handle 'address list request' messages from clients.
 *
 * @param cls client that sent the request
 * @param alrm the request message
 */
void
GAS_handle_request_address_list (struct GNUNET_SERVICE_Client *client,
                                 const struct AddressListRequestMessage *alrm)
{
  struct AddressIteration ai;
  struct GNUNET_PeerIdentity allzeros;

  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Received ADDRESSLIST_REQUEST message\n");
  ai.all = ntohl (alrm->all);
  ai.id = ntohl (alrm->id);
  ai.client = client;

  memset (&allzeros,
          '\0',
          sizeof(struct GNUNET_PeerIdentity));
  if (GNUNET_YES == GNUNET_is_zero (&alrm->peer))
  {
    /* Return addresses for all peers */
    GAS_addresses_get_peer_info (NULL,
                                 &req_addr_peerinfo_it,
                                 &ai);
  }
  else
  {
    /* Return addresses for a specific peer */
    GAS_addresses_get_peer_info (&alrm->peer,
                                 &req_addr_peerinfo_it,
                                 &ai);
  }
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Finished handling `%s' message\n",
              "ADDRESSLIST_REQUEST");
  transmit_req_addr (&ai,
                     NULL, NULL, NULL,
                     0, GNUNET_NO,
                     NULL,
                     GNUNET_HELLO_ADDRESS_INFO_NONE,
                     GNUNET_BANDWIDTH_ZERO,
                     GNUNET_BANDWIDTH_ZERO);
}


/* end of gnunet-service-ats_addresses.c */
