/*
     This file is part of GNUnet.
     Copyright (C) 2001-2014 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 peerinfo/peerinfo_api.c
 * @brief API to access peerinfo service
 * @author Christian Grothoff
 */
#include "platform.h"
#include "gnunet_util_lib.h"
#include "gnunet_protocols.h"
#include "peerinfo.h"

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


/**
 * Context for an iteration request.
 */
struct GNUNET_PEERINFO_IteratorContext
{
  /**
   * Kept in a DLL.
   */
  struct GNUNET_PEERINFO_IteratorContext *next;

  /**
   * Kept in a DLL.
   */
  struct GNUNET_PEERINFO_IteratorContext *prev;

  /**
   * Handle to the PEERINFO service.
   */
  struct GNUNET_PEERINFO_Handle *h;

  /**
   * Function to call with the results.
   */
  GNUNET_PEERINFO_Processor callback;

  /**
   * Closure for @e callback.
   */
  void *callback_cls;

  /**
   * Peer we are interested in (only valid if iteration was restricted to one peer).
   */
  struct GNUNET_PeerIdentity peer;

  /**
   * Is @e peer set?
   */
  int have_peer;

  /**
   * Only include friends in reply?
   */
  int include_friend_only;
};


/**
 * Handle to the peerinfo service.
 */
struct GNUNET_PEERINFO_Handle
{
  /**
   * Our configuration.
   */
  const struct GNUNET_CONFIGURATION_Handle *cfg;

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

  /**
   * Head of iterator DLL.
   */
  struct GNUNET_PEERINFO_IteratorContext *ic_head;

  /**
   * Tail of iterator DLL.
   */
  struct GNUNET_PEERINFO_IteratorContext *ic_tail;

  /**
   * ID for a reconnect task.
   */
  struct GNUNET_SCHEDULER_Task *r_task;
};


/**
 * Close the existing connection to PEERINFO and reconnect.
 *
 * @param h handle to the service
 */
static void
reconnect (struct GNUNET_PEERINFO_Handle *h);


/**
 * Connect to the peerinfo service.
 *
 * @param cfg configuration to use
 * @return NULL on error (configuration related, actual connection
 *         establishment may happen asynchronously).
 */
struct GNUNET_PEERINFO_Handle *
GNUNET_PEERINFO_connect (const struct GNUNET_CONFIGURATION_Handle *cfg)
{
  struct GNUNET_PEERINFO_Handle *h;

  h = GNUNET_new (struct GNUNET_PEERINFO_Handle);
  h->cfg = cfg;
  reconnect (h);
  if (NULL == h->mq)
  {
    GNUNET_free (h);
    return NULL;
  }
  return h;
}


/**
 * Disconnect from the peerinfo service.  Note that all iterators must
 * have completed or have been cancelled by the time this function is
 * called (otherwise, calling this function is a serious error).
 * Furthermore, if #GNUNET_PEERINFO_add_peer() operations are still
 * pending, they will be cancelled silently on disconnect.
 *
 * @param h handle to disconnect
 */
void
GNUNET_PEERINFO_disconnect (struct GNUNET_PEERINFO_Handle *h)
{
  struct GNUNET_PEERINFO_IteratorContext *ic;

  while (NULL != (ic = h->ic_head))
  {
    GNUNET_CONTAINER_DLL_remove (h->ic_head,
                                 h->ic_tail,
                                 ic);
    GNUNET_free (ic);
  }
  if (NULL != h->mq)
  {
    GNUNET_MQ_destroy (h->mq);
    h->mq = NULL;
  }
  if (NULL != h->r_task)
  {
    GNUNET_SCHEDULER_cancel (h->r_task);
    h->r_task = NULL;
  }
  GNUNET_free (h);
}


/**
 * Task scheduled to re-try connecting to the peerinfo service.
 *
 * @param cls the `struct GNUNET_PEERINFO_Handle *`
 */
static void
reconnect_task (void *cls)
{
  struct GNUNET_PEERINFO_Handle *h = cls;

  h->r_task = NULL;
  reconnect (h);
}


/**
 * We encountered an error, reconnect to the PEERINFO service.
 *
 * @param h handle to reconnect
 */
static void
do_reconnect (struct GNUNET_PEERINFO_Handle *h)
{
  struct GNUNET_PEERINFO_IteratorContext *ic = h->ic_head;

  GNUNET_MQ_destroy (h->mq);
  h->mq = NULL;
  if (NULL != ic)
  {
    GNUNET_CONTAINER_DLL_remove (h->ic_head,
                                 h->ic_tail,
                                 ic);
    if (NULL != ic->callback)
      ic->callback (ic->callback_cls,
                    NULL,
                    NULL,
                    _ ("Failed to receive response from `PEERINFO' service."));
    GNUNET_free (ic);
  }
  h->r_task = GNUNET_SCHEDULER_add_now (&reconnect_task,
                                        h);
}


/**
 * We got a disconnect after asking regex to do the announcement.
 * Retry.
 *
 * @param cls the `struct GNUNET_PEERINFO_Handle` to retry
 * @param error error code
 */
static void
mq_error_handler (void *cls,
                  enum GNUNET_MQ_Error error)
{
  struct GNUNET_PEERINFO_Handle *h = cls;

  do_reconnect (h);
}


/**
 * Function called when we receive an info message. Check it is
 * well-formed.
 *
 * @param cls closure
 * @param im message received
 * @return #GNUNET_OK if the message is OK
 */
static int
check_info (void *cls,
            const struct InfoMessage *im)
{
  struct GNUNET_PEERINFO_Handle *h = cls;
  struct GNUNET_PEERINFO_IteratorContext *ic = h->ic_head;
  uint16_t ms = ntohs (im->header.size) - sizeof(*im);

  if (0 != ntohl (im->reserved))
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  if (NULL == ic)
  {
    /* didn't expect a response, bad */
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  if ((GNUNET_YES == ic->have_peer) &&
      (0 != GNUNET_memcmp (&ic->peer,
                           &im->peer)))
  {
    /* bogus message (from a different iteration call?); out of sequence! */
    LOG (GNUNET_ERROR_TYPE_ERROR,
         "Received HELLO for peer `%s', expected peer `%s'\n",
         GNUNET_i2s (&im->peer),
         GNUNET_i2s (&ic->peer));
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  if (ms > sizeof(struct GNUNET_MessageHeader))
  {
    const struct GNUNET_HELLO_Message *hello;
    struct GNUNET_PeerIdentity id;

    hello = (const struct GNUNET_HELLO_Message *) &im[1];
    if (ms != GNUNET_HELLO_size (hello))
    {
      /* malformed message */
      GNUNET_break (0);
      return GNUNET_SYSERR;
    }
    if (GNUNET_OK !=
        GNUNET_HELLO_get_id (hello,
                             &id))
    {
      /* malformed message */
      GNUNET_break (0);
      return GNUNET_SYSERR;
    }
    if (0 != GNUNET_memcmp (&im->peer,
                            &id))
    {
      /* malformed message */
      GNUNET_break (0);
      return GNUNET_SYSERR;
    }
  }
  else if (0 != ms)
  {
    /* malformed message */
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  return GNUNET_OK;
}


/**
 * Handle info message.
 *
 * @param cls closure
 * @param im message received
 */
static void
handle_info (void *cls,
             const struct InfoMessage *im)
{
  struct GNUNET_PEERINFO_Handle *h = cls;
  struct GNUNET_PEERINFO_IteratorContext *ic = h->ic_head;
  const struct GNUNET_HELLO_Message *hello = NULL;
  uint16_t ms;

  ms = ntohs (im->header.size);
  if (ms > sizeof(struct InfoMessage))
    hello = (const struct GNUNET_HELLO_Message *) &im[1];
  if (NULL != ic->callback)
    ic->callback (ic->callback_cls,
                  &im->peer,
                  hello,
                  NULL);
}


/**
 * Send the next IC request at the head of the queue.
 *
 * @param h handle
 */
static void
send_ic_request (struct GNUNET_PEERINFO_Handle *h)
{
  struct GNUNET_PEERINFO_IteratorContext *ic = h->ic_head;
  struct GNUNET_MQ_Envelope *env;
  struct ListAllPeersMessage *lapm;
  struct ListPeerMessage *lpm;

  if (NULL == ic)
  {
    GNUNET_break (0);
    return;
  }
  if (NULL == h->mq)
  {
    GNUNET_break (0);
    return;
  }
  if (GNUNET_NO == ic->have_peer)
  {
    LOG (GNUNET_ERROR_TYPE_DEBUG,
         "Requesting list of peers from PEERINFO service\n");
    env = GNUNET_MQ_msg (lapm,
                         GNUNET_MESSAGE_TYPE_PEERINFO_GET_ALL);
    lapm->include_friend_only = htonl (ic->include_friend_only);
  }
  else
  {
    LOG (GNUNET_ERROR_TYPE_DEBUG,
         "Requesting information on peer `%s' from PEERINFO service\n",
         GNUNET_i2s (&ic->peer));
    env = GNUNET_MQ_msg (lpm,
                         GNUNET_MESSAGE_TYPE_PEERINFO_GET);
    lpm->include_friend_only = htonl (ic->include_friend_only);
    lpm->peer = ic->peer;
  }
  GNUNET_MQ_send (h->mq,
                  env);
}


/**
 * Type of a function to call when we receive a message from the
 * service.  Call the iterator with the result and (if applicable)
 * continue to receive more messages or trigger processing the next
 * event (if applicable).
 *
 * @param cls closure
 * @param msg message received, NULL on timeout or fatal error
 */
static void
handle_end_iteration (void *cls,
                      const struct GNUNET_MessageHeader *msg)
{
  struct GNUNET_PEERINFO_Handle *h = cls;
  struct GNUNET_PEERINFO_IteratorContext *ic = h->ic_head;

  if (NULL == ic)
  {
    /* didn't expect a response, reconnect */
    GNUNET_break (0);
    reconnect (h);
    return;
  }
  LOG (GNUNET_ERROR_TYPE_DEBUG,
       "Received end of list of peers from PEERINFO service\n");
  GNUNET_CONTAINER_DLL_remove (h->ic_head,
                               h->ic_tail,
                               ic);
  if (NULL != h->ic_head)
    send_ic_request (h);
  if (NULL != ic->callback)
    ic->callback (ic->callback_cls,
                  NULL,
                  NULL,
                  NULL);
  GNUNET_free (ic);
}


/**
 * Close the existing connection to PEERINFO and reconnect.
 *
 * @param h handle to the service
 */
static void
reconnect (struct GNUNET_PEERINFO_Handle *h)
{
  struct GNUNET_MQ_MessageHandler handlers[] = {
    GNUNET_MQ_hd_var_size (info,
                           GNUNET_MESSAGE_TYPE_PEERINFO_INFO,
                           struct InfoMessage,
                           h),
    GNUNET_MQ_hd_fixed_size (end_iteration,
                             GNUNET_MESSAGE_TYPE_PEERINFO_INFO_END,
                             struct GNUNET_MessageHeader,
                             h),
    GNUNET_MQ_handler_end ()
  };

  if (NULL != h->r_task)
  {
    GNUNET_SCHEDULER_cancel (h->r_task);
    h->r_task = NULL;
  }
  if (NULL != h->mq)
  {
    GNUNET_MQ_destroy (h->mq);
    h->mq = NULL;
  }
  h->mq = GNUNET_CLIENT_connect (h->cfg,
                                 "peerinfo",
                                 handlers,
                                 &mq_error_handler,
                                 h);
  if (NULL != h->ic_head)
    send_ic_request (h);
}


/**
 * Call a method for each known matching host.  The callback method
 * will be invoked once for each matching host and then finally once
 * with a NULL pointer.  After that final invocation, the iterator
 * context must no longer be used.
 *
 * Instead of calling this function with `peer == NULL` it is often
 * better to use #GNUNET_PEERINFO_notify().
 *
 * @param h handle to the peerinfo service
 * @param include_friend_only include HELLO messages for friends only
 * @param peer restrict iteration to this peer only (can be NULL)
 * @param callback the method to call for each peer
 * @param callback_cls closure for @a callback
 * @return iterator context
 */
struct GNUNET_PEERINFO_IteratorContext *
GNUNET_PEERINFO_iterate (struct GNUNET_PEERINFO_Handle *h,
                         int include_friend_only,
                         const struct GNUNET_PeerIdentity *peer,
                         GNUNET_PEERINFO_Processor callback,
                         void *callback_cls)
{
  struct GNUNET_PEERINFO_IteratorContext *ic;

  ic = GNUNET_new (struct GNUNET_PEERINFO_IteratorContext);
  ic->h = h;
  ic->include_friend_only = include_friend_only;
  ic->callback = callback;
  ic->callback_cls = callback_cls;
  if (NULL != peer)
  {
    ic->have_peer = GNUNET_YES;
    ic->peer = *peer;
  }
  GNUNET_CONTAINER_DLL_insert_tail (h->ic_head,
                                    h->ic_tail,
                                    ic);
  if (h->ic_head == ic)
    send_ic_request (h);
  return ic;
}


/**
 * Cancel an iteration over peer information.
 *
 * @param ic context of the iterator to cancel
 */
void
GNUNET_PEERINFO_iterate_cancel (struct GNUNET_PEERINFO_IteratorContext *ic)
{
  struct GNUNET_PEERINFO_Handle *h = ic->h;

  ic->callback = NULL;
  if (ic == h->ic_head)
    return;
  GNUNET_CONTAINER_DLL_remove (h->ic_head,
                               h->ic_tail,
                               ic);
  GNUNET_free (ic);
}


/**
 * Add a host to the persistent list.  This method operates in
 * semi-reliable mode: if the transmission is not completed by
 * the time #GNUNET_PEERINFO_disconnect() is called, it will be
 * aborted.  Furthermore, if a second HELLO is added for the
 * same peer before the first one was transmitted, PEERINFO may
 * merge the two HELLOs prior to transmission to the service.
 *
 * @param h handle to the peerinfo service
 * @param hello the verified (!) HELLO message
 * @param cont continuation to call when done, NULL is allowed
 * @param cont_cls closure for @a cont
 * @return handle to cancel add operation; all pending
 *         'add' operations will be cancelled automatically
 *        on disconnect, so it is not necessary to keep this
 *        handle (unless @a cont is NULL and at some point
 *        calling @a cont must be prevented)
 */
struct GNUNET_MQ_Envelope *
GNUNET_PEERINFO_add_peer (struct GNUNET_PEERINFO_Handle *h,
                          const struct GNUNET_HELLO_Message *hello,
                          GNUNET_SCHEDULER_TaskCallback cont,
                          void *cont_cls)
{
  struct GNUNET_MQ_Envelope *env;
  struct GNUNET_PeerIdentity peer;

  if (NULL == h->mq)
    return NULL;
  GNUNET_assert (GNUNET_OK ==
                 GNUNET_HELLO_get_id (hello,
                                      &peer));
  LOG (GNUNET_ERROR_TYPE_DEBUG,
       "Adding peer `%s' to PEERINFO database\n",
       GNUNET_i2s (&peer));
  env = GNUNET_MQ_msg_copy ((const struct GNUNET_MessageHeader *) hello);
  if (NULL != cont)
    GNUNET_MQ_notify_sent (env,
                           cont,
                           cont_cls);
  GNUNET_MQ_send (h->mq,
                  env);
  return env;
}


/* end of peerinfo_api.c */
