/*
      This file is part of GNUnet
      Copyright (C) 2013 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 set/gnunet-set-profiler.c
 * @brief profiling tool for set
 * @author Florian Dold
 */
#include "platform.h"
#include "gnunet_util_lib.h"
#include "gnunet_statistics_service.h"
#include "gnunet_set_service.h"
#include "gnunet_testbed_service.h"


static int ret;

static unsigned int num_a = 5;
static unsigned int num_b = 5;
static unsigned int num_c = 20;

static char *op_str = "union";

const static struct GNUNET_CONFIGURATION_Handle *config;

struct SetInfo
{
  char *id;
  struct GNUNET_SET_Handle *set;
  struct GNUNET_SET_OperationHandle *oh;
  struct GNUNET_CONTAINER_MultiHashMap *sent;
  struct GNUNET_CONTAINER_MultiHashMap *received;
  int done;
} info1, info2;

static struct GNUNET_CONTAINER_MultiHashMap *common_sent;

static struct GNUNET_HashCode app_id;

static struct GNUNET_PeerIdentity local_peer;

static struct GNUNET_SET_ListenHandle *set_listener;

static int byzantine;
static unsigned int force_delta;
static unsigned int force_full;
static unsigned int element_size = 32;

/**
 * Handle to the statistics service.
 */
static struct GNUNET_STATISTICS_Handle *statistics;

/**
 * The profiler will write statistics
 * for all peers to the file with this name.
 */
static char *statistics_filename;

/**
 * The profiler will write statistics
 * for all peers to this file.
 */
static FILE *statistics_file;


static int
map_remove_iterator (void *cls,
                     const struct GNUNET_HashCode *key,
                     void *value)
{
  struct GNUNET_CONTAINER_MultiHashMap *m = cls;
  int ret;

  GNUNET_assert (NULL != key);

  ret = GNUNET_CONTAINER_multihashmap_remove_all (m, key);
  if (GNUNET_OK != ret)
    printf ("spurious element\n");
  return GNUNET_YES;
}


/**
 * Callback function to process statistic values.
 *
 * @param cls closure
 * @param subsystem name of subsystem that created the statistic
 * @param name the name of the datum
 * @param value the current value
 * @param is_persistent #GNUNET_YES if the value is persistent, #GNUNET_NO if not
 * @return #GNUNET_OK to continue, #GNUNET_SYSERR to abort iteration
 */
static int
statistics_result (void *cls,
                   const char *subsystem,
                   const char *name,
                   uint64_t value,
                   int is_persistent)
{
  if (NULL != statistics_file)
  {
    fprintf (statistics_file, "%s\t%s\t%lu\n", subsystem, name, (unsigned
                                                                 long) value);
  }
  return GNUNET_OK;
}


static void
statistics_done (void *cls,
                 int success)
{
  GNUNET_assert (GNUNET_YES == success);
  if (NULL != statistics_file)
    fclose (statistics_file);
  GNUNET_SCHEDULER_shutdown ();
}


static void
check_all_done (void)
{
  if ((info1.done == GNUNET_NO) || (info2.done == GNUNET_NO))
    return;

  GNUNET_CONTAINER_multihashmap_iterate (info1.received, map_remove_iterator,
                                         info2.sent);
  GNUNET_CONTAINER_multihashmap_iterate (info2.received, map_remove_iterator,
                                         info1.sent);

  printf ("set a: %d missing elements\n", GNUNET_CONTAINER_multihashmap_size (
            info1.sent));
  printf ("set b: %d missing elements\n", GNUNET_CONTAINER_multihashmap_size (
            info2.sent));

  if (NULL == statistics_filename)
  {
    GNUNET_SCHEDULER_shutdown ();
    return;
  }

  statistics_file = fopen (statistics_filename, "w");
  GNUNET_STATISTICS_get (statistics, NULL, NULL,
                         &statistics_done,
                         &statistics_result, NULL);
}


static void
set_result_cb (void *cls,
               const struct GNUNET_SET_Element *element,
               uint64_t current_size,
               enum GNUNET_SET_Status status)
{
  struct SetInfo *info = cls;
  struct GNUNET_HashCode hash;

  GNUNET_assert (GNUNET_NO == info->done);
  switch (status)
  {
  case GNUNET_SET_STATUS_DONE:
  case GNUNET_SET_STATUS_HALF_DONE:
    info->done = GNUNET_YES;
    GNUNET_log (GNUNET_ERROR_TYPE_INFO, "set %s done\n", info->id);
    check_all_done ();
    info->oh = NULL;
    return;

  case GNUNET_SET_STATUS_FAILURE:
    info->oh = NULL;
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "failure\n");
    GNUNET_SCHEDULER_shutdown ();
    return;

  case GNUNET_SET_STATUS_ADD_LOCAL:
    GNUNET_log (GNUNET_ERROR_TYPE_INFO, "set %s: local element\n", info->id);
    break;

  case GNUNET_SET_STATUS_ADD_REMOTE:
    GNUNET_CRYPTO_hash (element->data, element->size, &hash);
    GNUNET_log (GNUNET_ERROR_TYPE_INFO, "set %s: remote element %s\n", info->id,
                GNUNET_h2s (&hash));
    // XXX: record and check
    return;

  default:
    GNUNET_assert (0);
  }

  if (element->size != element_size)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "wrong element size: %u, expected %u\n",
                element->size,
                (unsigned int) sizeof(struct GNUNET_HashCode));
    GNUNET_assert (0);
  }

  GNUNET_log (GNUNET_ERROR_TYPE_INFO, "set %s: got element (%s)\n",
              info->id, GNUNET_h2s (element->data));
  GNUNET_assert (NULL != element->data);
  struct GNUNET_HashCode data_hash;
  GNUNET_CRYPTO_hash (element->data, element_size, &data_hash);
  GNUNET_CONTAINER_multihashmap_put (info->received,
                                     &data_hash, NULL,
                                     GNUNET_CONTAINER_MULTIHASHMAPOPTION_REPLACE);
}


static void
set_listen_cb (void *cls,
               const struct GNUNET_PeerIdentity *other_peer,
               const struct GNUNET_MessageHeader *context_msg,
               struct GNUNET_SET_Request *request)
{
  /* max. 2 options plus terminator */
  struct GNUNET_SET_Option opts[3] = { { 0 } };
  unsigned int n_opts = 0;

  if (NULL == request)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "listener failed\n");
    return;
  }
  GNUNET_assert (NULL == info2.oh);
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "set listen cb called\n");
  if (byzantine)
  {
    opts[n_opts++] = (struct GNUNET_SET_Option) { .type =
                                                    GNUNET_SET_OPTION_BYZANTINE };
  }
  GNUNET_assert (! (force_full && force_delta));
  if (force_full)
  {
    opts[n_opts++] = (struct GNUNET_SET_Option) { .type =
                                                    GNUNET_SET_OPTION_FORCE_FULL };
  }
  if (force_delta)
  {
    opts[n_opts++] = (struct GNUNET_SET_Option) { .type =
                                                    GNUNET_SET_OPTION_FORCE_DELTA };
  }

  opts[n_opts].type = 0;
  info2.oh = GNUNET_SET_accept (request, GNUNET_SET_RESULT_SYMMETRIC,
                                opts,
                                set_result_cb, &info2);
  GNUNET_SET_commit (info2.oh, info2.set);
}


static int
set_insert_iterator (void *cls,
                     const struct GNUNET_HashCode *key,
                     void *value)
{
  struct GNUNET_SET_Handle *set = cls;
  struct GNUNET_SET_Element el;

  el.element_type = 0;
  el.data = value;
  el.size = element_size;
  GNUNET_SET_add_element (set, &el, NULL, NULL);
  return GNUNET_YES;
}


static void
handle_shutdown (void *cls)
{
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Shutting down set profiler\n");
  if (NULL != set_listener)
  {
    GNUNET_SET_listen_cancel (set_listener);
    set_listener = NULL;
  }
  if (NULL != info1.oh)
  {
    GNUNET_SET_operation_cancel (info1.oh);
    info1.oh = NULL;
  }
  if (NULL != info2.oh)
  {
    GNUNET_SET_operation_cancel (info2.oh);
    info2.oh = NULL;
  }
  if (NULL != info1.set)
  {
    GNUNET_SET_destroy (info1.set);
    info1.set = NULL;
  }
  if (NULL != info2.set)
  {
    GNUNET_SET_destroy (info2.set);
    info2.set = NULL;
  }
  GNUNET_STATISTICS_destroy (statistics, GNUNET_NO);
}


static void
run (void *cls,
     const struct GNUNET_CONFIGURATION_Handle *cfg,
     struct GNUNET_TESTING_Peer *peer)
{
  unsigned int i;
  struct GNUNET_HashCode hash;
  /* max. 2 options plus terminator */
  struct GNUNET_SET_Option opts[3] = { { 0 } };
  unsigned int n_opts = 0;

  config = cfg;

  GNUNET_assert (element_size > 0);

  if (GNUNET_OK != GNUNET_CRYPTO_get_peer_identity (cfg, &local_peer))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "could not retrieve host identity\n");
    ret = 0;
    return;
  }

  statistics = GNUNET_STATISTICS_create ("set-profiler", cfg);

  GNUNET_SCHEDULER_add_shutdown (&handle_shutdown, NULL);

  info1.id = "a";
  info2.id = "b";

  info1.sent = GNUNET_CONTAINER_multihashmap_create (num_a + 1, GNUNET_NO);
  info2.sent = GNUNET_CONTAINER_multihashmap_create (num_b + 1, GNUNET_NO);
  common_sent = GNUNET_CONTAINER_multihashmap_create (num_c + 1, GNUNET_NO);

  info1.received = GNUNET_CONTAINER_multihashmap_create (num_a + 1, GNUNET_NO);
  info2.received = GNUNET_CONTAINER_multihashmap_create (num_b + 1, GNUNET_NO);

  for (i = 0; i < num_a; i++)
  {
    char *data = GNUNET_malloc (element_size);
    GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, data, element_size);
    GNUNET_CRYPTO_hash (data, element_size, &hash);
    GNUNET_CONTAINER_multihashmap_put (info1.sent, &hash, data,
                                       GNUNET_CONTAINER_MULTIHASHMAPOPTION_REPLACE);
  }

  for (i = 0; i < num_b; i++)
  {
    char *data = GNUNET_malloc (element_size);
    GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, data, element_size);
    GNUNET_CRYPTO_hash (data, element_size, &hash);
    GNUNET_CONTAINER_multihashmap_put (info2.sent, &hash, data,
                                       GNUNET_CONTAINER_MULTIHASHMAPOPTION_REPLACE);
  }

  for (i = 0; i < num_c; i++)
  {
    char *data = GNUNET_malloc (element_size);
    GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, data, element_size);
    GNUNET_CRYPTO_hash (data, element_size, &hash);
    GNUNET_CONTAINER_multihashmap_put (common_sent, &hash, data,
                                       GNUNET_CONTAINER_MULTIHASHMAPOPTION_REPLACE);
  }

  GNUNET_CRYPTO_hash_create_random (GNUNET_CRYPTO_QUALITY_STRONG, &app_id);

  /* FIXME: also implement intersection etc. */
  info1.set = GNUNET_SET_create (config, GNUNET_SET_OPERATION_UNION);
  info2.set = GNUNET_SET_create (config, GNUNET_SET_OPERATION_UNION);

  GNUNET_CONTAINER_multihashmap_iterate (info1.sent, set_insert_iterator,
                                         info1.set);
  GNUNET_CONTAINER_multihashmap_iterate (info2.sent, set_insert_iterator,
                                         info2.set);
  GNUNET_CONTAINER_multihashmap_iterate (common_sent, set_insert_iterator,
                                         info1.set);
  GNUNET_CONTAINER_multihashmap_iterate (common_sent, set_insert_iterator,
                                         info2.set);

  set_listener = GNUNET_SET_listen (config, GNUNET_SET_OPERATION_UNION,
                                    &app_id, set_listen_cb, NULL);


  if (byzantine)
  {
    opts[n_opts++] = (struct GNUNET_SET_Option) { .type =
                                                    GNUNET_SET_OPTION_BYZANTINE };
  }
  GNUNET_assert (! (force_full && force_delta));
  if (force_full)
  {
    opts[n_opts++] = (struct GNUNET_SET_Option) { .type =
                                                    GNUNET_SET_OPTION_FORCE_FULL };
  }
  if (force_delta)
  {
    opts[n_opts++] = (struct GNUNET_SET_Option) { .type =
                                                    GNUNET_SET_OPTION_FORCE_DELTA };
  }

  opts[n_opts].type = 0;

  info1.oh = GNUNET_SET_prepare (&local_peer, &app_id, NULL,
                                 GNUNET_SET_RESULT_SYMMETRIC,
                                 opts,
                                 set_result_cb, &info1);
  GNUNET_SET_commit (info1.oh, info1.set);
  GNUNET_SET_destroy (info1.set);
  info1.set = NULL;
}


static void
pre_run (void *cls, char *const *args, const char *cfgfile,
         const struct GNUNET_CONFIGURATION_Handle *cfg)
{
  if (0 != GNUNET_TESTING_peer_run ("set-profiler",
                                    cfgfile,
                                    &run, NULL))
    ret = 2;
}


int
main (int argc, char **argv)
{
  struct GNUNET_GETOPT_CommandLineOption options[] = {
    GNUNET_GETOPT_option_uint ('A',
                               "num-first",
                               NULL,
                               gettext_noop ("number of values"),
                               &num_a),

    GNUNET_GETOPT_option_uint ('B',
                               "num-second",
                               NULL,
                               gettext_noop ("number of values"),
                               &num_b),

    GNUNET_GETOPT_option_flag ('b',
                               "byzantine",
                               gettext_noop ("use byzantine mode"),
                               &byzantine),

    GNUNET_GETOPT_option_uint ('f',
                               "force-full",
                               NULL,
                               gettext_noop ("force sending full set"),
                               &force_full),

    GNUNET_GETOPT_option_uint ('d',
                               "force-delta",
                               NULL,
                               gettext_noop ("number delta operation"),
                               &force_delta),

    GNUNET_GETOPT_option_uint ('C',
                               "num-common",
                               NULL,
                               gettext_noop ("number of values"),
                               &num_c),

    GNUNET_GETOPT_option_string ('x',
                                 "operation",
                                 NULL,
                                 gettext_noop ("operation to execute"),
                                 &op_str),

    GNUNET_GETOPT_option_uint ('w',
                               "element-size",
                               NULL,
                               gettext_noop ("element size"),
                               &element_size),

    GNUNET_GETOPT_option_filename ('s',
                                   "statistics",
                                   "FILENAME",
                                   gettext_noop ("write statistics to file"),
                                   &statistics_filename),

    GNUNET_GETOPT_OPTION_END
  };

  GNUNET_PROGRAM_run2 (argc, argv, "gnunet-set-profiler",
                       "help",
                       options, &pre_run, NULL, GNUNET_YES);
  return ret;
}
