/*
     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 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 transport/transport_api_manipulation.c
 * @brief library to access the low-level P2P IO service
 * @author Christian Grothoff
 */
#include "platform.h"
#include "gnunet_util_lib.h"
#include "gnunet_constants.h"
#include "gnunet_arm_service.h"
#include "gnunet_hello_lib.h"
#include "gnunet_protocols.h"
#include "gnunet_transport_service.h"
#include "transport.h"

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


/**
 * Handle for the transport service (includes all of the
 * state for the transport service).
 */
struct GNUNET_TRANSPORT_ManipulationHandle
{
  /**
   * My client connection to the transport service.
   */
  struct GNUNET_MQ_Handle *mq;

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

  /**
   * ID of the task trying to reconnect to the service.
   */
  struct GNUNET_SCHEDULER_Task *reconnect_task;

  /**
   * Delay until we try to reconnect.
   */
  struct GNUNET_TIME_Relative reconnect_delay;

  /**
   * Reconnect in progress
   */
  int reconnecting;
};


/**
 * Function that will schedule the job that will try
 * to connect us again to the client.
 *
 * @param h transport service to reconnect
 */
static void
disconnect_and_schedule_reconnect (struct
                                   GNUNET_TRANSPORT_ManipulationHandle *h);


/**
 * 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_TRANSPORT_ManipulationHandle *`
 * @param error error code
 */
static void
mq_error_handler (void *cls,
                  enum GNUNET_MQ_Error error)
{
  struct GNUNET_TRANSPORT_ManipulationHandle *h = cls;

  LOG (GNUNET_ERROR_TYPE_DEBUG,
       "Error receiving from transport service, disconnecting temporarily.\n");
  h->reconnecting = GNUNET_YES;
  disconnect_and_schedule_reconnect (h);
}


/**
 * Try again to connect to transport service.
 *
 * @param cls the handle to the transport service
 */
static void
reconnect (void *cls)
{
  struct GNUNET_TRANSPORT_ManipulationHandle *h = cls;
  struct GNUNET_MQ_MessageHandler handlers[] = {
    GNUNET_MQ_handler_end ()
  };
  struct GNUNET_MQ_Envelope *env;
  struct StartMessage *s;

  h->reconnect_task = NULL;
  LOG (GNUNET_ERROR_TYPE_DEBUG,
       "Connecting to transport service.\n");
  GNUNET_assert (NULL == h->mq);
  h->reconnecting = GNUNET_NO;
  h->mq = GNUNET_CLIENT_connect (h->cfg,
                                 "transport",
                                 handlers,
                                 &mq_error_handler,
                                 h);
  if (NULL == h->mq)
    return;
  env = GNUNET_MQ_msg (s,
                       GNUNET_MESSAGE_TYPE_TRANSPORT_START);
  GNUNET_MQ_send (h->mq,
                  env);
}


/**
 * Function that will schedule the job that will try
 * to connect us again to the client.
 *
 * @param h transport service to reconnect
 */
static void
disconnect_and_schedule_reconnect (struct
                                   GNUNET_TRANSPORT_ManipulationHandle *h)
{
  GNUNET_assert (NULL == h->reconnect_task);
  if (NULL != h->mq)
  {
    GNUNET_MQ_destroy (h->mq);
    h->mq = NULL;
  }
  h->reconnect_task =
    GNUNET_SCHEDULER_add_delayed (h->reconnect_delay,
                                  &reconnect,
                                  h);
  h->reconnect_delay = GNUNET_TIME_STD_BACKOFF (h->reconnect_delay);
}


/**
 * Set transport metrics for a peer and a direction.
 *
 * @param handle transport handle
 * @param peer the peer to set the metric for
 * @param prop the performance metrics to set
 * @param delay_in inbound delay to introduce
 * @param delay_out outbound delay to introduce
 *
 * Note: Delay restrictions in receiving direction will be enforced
 * with one message delay.
 */
void
GNUNET_TRANSPORT_manipulation_set (struct
                                   GNUNET_TRANSPORT_ManipulationHandle *handle,
                                   const struct GNUNET_PeerIdentity *peer,
                                   const struct GNUNET_ATS_Properties *prop,
                                   struct GNUNET_TIME_Relative delay_in,
                                   struct GNUNET_TIME_Relative delay_out)
{
  struct GNUNET_MQ_Envelope *env;
  struct TrafficMetricMessage *msg;

  if (NULL == handle->mq)
    return;
  env = GNUNET_MQ_msg (msg,
                       GNUNET_MESSAGE_TYPE_TRANSPORT_TRAFFIC_METRIC);
  msg->reserved = htonl (0);
  msg->peer = *peer;
  GNUNET_ATS_properties_hton (&msg->properties,
                              prop);
  msg->delay_in = GNUNET_TIME_relative_hton (delay_in);
  msg->delay_out = GNUNET_TIME_relative_hton (delay_out);
  GNUNET_MQ_send (handle->mq,
                  env);
}


/**
 * Connect to the transport service.  Note that the connection may
 * complete (or fail) asynchronously.
 *
 * @param cfg configuration to use
 * @return NULL on error
 */
struct GNUNET_TRANSPORT_ManipulationHandle *
GNUNET_TRANSPORT_manipulation_connect (const struct
                                       GNUNET_CONFIGURATION_Handle *cfg)
{
  struct GNUNET_TRANSPORT_ManipulationHandle *h;

  h = GNUNET_new (struct GNUNET_TRANSPORT_ManipulationHandle);
  h->cfg = cfg;
  LOG (GNUNET_ERROR_TYPE_DEBUG,
       "Connecting to transport service.\n");
  reconnect (h);
  if (NULL == h->mq)
  {
    GNUNET_free (h);
    return NULL;
  }
  return h;
}


/**
 * Disconnect from the transport service.
 *
 * @param handle handle to the service as returned from #GNUNET_TRANSPORT_manipulation_connect()
 */
void
GNUNET_TRANSPORT_manipulation_disconnect (struct
                                          GNUNET_TRANSPORT_ManipulationHandle *
                                          handle)
{
  if (NULL == handle->reconnect_task)
    disconnect_and_schedule_reconnect (handle);
  /* and now we stop trying to connect again... */
  if (NULL != handle->reconnect_task)
  {
    GNUNET_SCHEDULER_cancel (handle->reconnect_task);
    handle->reconnect_task = NULL;
  }
  GNUNET_free (handle);
}


/* end of transport_api_manipulation.c */
