/*
 * 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 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 multicast/test_multicast_2peers.c
 * @brief Tests for the Multicast API with two peers doing the ping
 *        pong test.
 * @author xrs
 */

#include <inttypes.h>

#include "platform.h"
#include "gnunet_crypto_lib.h"
#include "gnunet_common.h"
#include "gnunet_util_lib.h"
#include "gnunet_testbed_service.h"
#include "gnunet_multicast_service.h"

#define NUM_PEERS 2

static struct GNUNET_TESTBED_Operation *op0;
static struct GNUNET_TESTBED_Operation *op1;
static struct GNUNET_TESTBED_Operation *pi_op0;
static struct GNUNET_TESTBED_Operation *pi_op1;

static struct GNUNET_TESTBED_Peer **peers;
const struct GNUNET_PeerIdentity *peer_id[2];

static struct GNUNET_SCHEDULER_Task *timeout_tid;

static struct GNUNET_MULTICAST_Origin *origin;
static struct GNUNET_MULTICAST_Member *member;

struct GNUNET_CRYPTO_EddsaPrivateKey *group_key;
struct GNUNET_CRYPTO_EddsaPublicKey group_pub_key;

struct GNUNET_CRYPTO_EcdsaPrivateKey *member_key;
struct GNUNET_CRYPTO_EcdsaPublicKey member_pub_key;

/**
 * Global result for testcase.
 */
static int result;


/**
 * Function run on CTRL-C or shutdown (i.e. success/timeout/etc.).
 * Cleans up.
 */
static void
shutdown_task (void *cls)
{
  if (NULL != op0)
  {
    GNUNET_TESTBED_operation_done (op0);
    op0 = NULL;
  }
  if (NULL != op1)
  {
    GNUNET_TESTBED_operation_done (op1);
    op1 = NULL;
  }
  if (NULL != pi_op0)
  {
    GNUNET_TESTBED_operation_done (pi_op0);
    pi_op0 = NULL;
  }
  if (NULL != pi_op1)
  {
    GNUNET_TESTBED_operation_done (pi_op1);
    pi_op1 = NULL;
  }
  if (NULL != timeout_tid)
    {
      GNUNET_SCHEDULER_cancel (timeout_tid);
      timeout_tid = NULL;
    }
}


static void
timeout_task (void *cls)
{
  GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
	      "Timeout!\n");
  result = GNUNET_SYSERR;
  GNUNET_SCHEDULER_shutdown ();
}


static void
member_join_request (void *cls,
                     const struct GNUNET_CRYPTO_EcdsaPublicKey *member_pub_key,
                     const struct GNUNET_MessageHeader *join_msg,
                     struct GNUNET_MULTICAST_JoinHandle *jh)
{
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Member sent a join request.\n");

}


static int
notify (void *cls,
        size_t *data_size,
        void *data)
{

  char text[] = "ping";
  *data_size = strlen(text)+1;
  GNUNET_memcpy(data, text, *data_size);

  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Member sents message to origin: %s\n", text);

  return GNUNET_YES;
}


static void
member_join_decision (void *cls,
                      int is_admitted,
                      const struct GNUNET_PeerIdentity *peer,
                      uint16_t relay_count,
                      const struct GNUNET_PeerIdentity *relays,
                      const struct GNUNET_MessageHeader *join_msg)
{
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Member received a decision from origin: %s\n",
              (GNUNET_YES == is_admitted)
              ? "accepted"
              : "rejected");

  if (GNUNET_YES == is_admitted)
  {
    struct GNUNET_MULTICAST_MemberTransmitHandle *req;

    // FIXME: move to MQ-style API!
    req = GNUNET_MULTICAST_member_to_origin (member,
                                             0,
                                             &notify,
                                             NULL);
  }
}


static void
member_message (void *cls,
                const struct GNUNET_MULTICAST_MessageHeader *msg)
{
  if (0 != strncmp ("pong", (char *)&msg[1], 4))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "member did not receive pong\n");
    result = GNUNET_SYSERR;
    GNUNET_SCHEDULER_shutdown ();
  }

  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "member receives: %s\n", (char *)&msg[1]);

  // Testcase ends here.
  result = GNUNET_YES;
  GNUNET_SCHEDULER_shutdown ();
}


static void
origin_join_request (void *cls,
                 const struct GNUNET_CRYPTO_EcdsaPublicKey *member_pub_key,
                 const struct GNUNET_MessageHeader *join_msg,
                 struct GNUNET_MULTICAST_JoinHandle *jh)
{
  struct GNUNET_MessageHeader *join_resp;

  uint8_t data_size = ntohs (join_msg->size);

  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "origin got a join request...\n");
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "origin receives: '%s'\n", (char *)&join_msg[1]);

  const char data[] = "Come in!";
  data_size = strlen (data) + 1;
  join_resp = GNUNET_malloc (sizeof (join_resp) + data_size);
  join_resp->size = htons (sizeof (join_resp) + data_size);
  join_resp->type = htons (123);
  GNUNET_memcpy (&join_resp[1], data, data_size);

  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "origin sends: '%s'\n", data);

  GNUNET_MULTICAST_join_decision (jh,
                                  GNUNET_YES,
                                  0,
                                  NULL,
                                  join_resp);
  GNUNET_free (join_resp);
  result = GNUNET_OK;
}


int
origin_notify (void *cls,
               size_t *data_size,
               void *data)
{
  char text[] = "pong";
  *data_size = strlen(text)+1;
  memcpy(data, text, *data_size);

  GNUNET_log (GNUNET_ERROR_TYPE_INFO, "origin sends (to all): %s\n", text);

  return GNUNET_YES;
}


static void
origin_request (void *cls,
                const struct GNUNET_MULTICAST_RequestHeader *req)
{
  GNUNET_log (GNUNET_ERROR_TYPE_INFO, "origin receives: %s\n", (char *)&req[1]);

  if (0 != strncmp ("ping", (char *)&req[1], 4))
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "origin didn't reveice a correct request");

  GNUNET_MULTICAST_origin_to_all (origin,
                                  0,
                                  0,
                                  origin_notify,
                                  NULL);
}


static void
origin_message (void *cls,
                const struct GNUNET_MULTICAST_MessageHeader *msg)
{
  GNUNET_log (GNUNET_ERROR_TYPE_INFO, "origin message msg\n");
}


static void
service_connect1 (void *cls,
                  struct GNUNET_TESTBED_Operation *op,
                  void *ca_result,
                  const char *emsg)
{
  member = ca_result;

  if (NULL == member)
  {
    result = GNUNET_SYSERR;
    GNUNET_SCHEDULER_shutdown ();
  }
  else
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Connected to multicast service of member\n");
  }
}


static void
multicast_da1 (void *cls,
               void * op_result)
{
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Member parting from multicast group\n");

  GNUNET_MULTICAST_member_part (member, NULL, NULL);
}


static void *
multicast_ca1 (void *cls,
               const struct GNUNET_CONFIGURATION_Handle *cfg)
{
  struct GNUNET_MessageHeader *join_msg;
  void *ret;

  // Get members keys
  member_key = GNUNET_CRYPTO_ecdsa_key_create ();
  GNUNET_CRYPTO_ecdsa_key_get_public (member_key, &member_pub_key);

  char data[] = "Hi, can I enter?";
  uint8_t data_size = strlen (data) + 1;
  join_msg = GNUNET_malloc (sizeof (join_msg) + data_size);
  join_msg->size = htons (sizeof (join_msg) + data_size);
  join_msg->type = htons (123);
  GNUNET_memcpy (&join_msg[1], data, data_size);

  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Members tries to join multicast group\n");

  ret = GNUNET_MULTICAST_member_join (cfg,
                                       &group_pub_key,
                                       member_key,
                                       peer_id[0],
                                       0,
                                       NULL,
                                       join_msg, /* join message */
                                       member_join_request,
                                       member_join_decision,
                                       NULL, /* no test for member_replay_frag */
                                       NULL, /* no test for member_replay_msg */
                                       member_message,
                                       NULL);
  GNUNET_free (join_msg);
  return ret;
}


static void
peer_information_cb (void *cls,
                     struct GNUNET_TESTBED_Operation *op,
                     const struct GNUNET_TESTBED_PeerInformation *pinfo,
                     const char *emsg)
{
  int i = (int) (long) cls;

  if (NULL == pinfo)
  {
    result = GNUNET_SYSERR;
    GNUNET_SCHEDULER_shutdown ();
  }

  peer_id[i] = pinfo->result.id;

  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Got peer information of %s (%s)\n", (0==i)?"origin":"member" ,GNUNET_i2s(pinfo->result.id));

  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Create member peer\n");

  if (0 == i)
  {
    /* connect to multicast service of member */
    op1 = GNUNET_TESTBED_service_connect (NULL,                    /* Closure for operation */
                                          peers[1],                /* The peer whose service to connect to */
                                          "multicast",             /* The name of the service */
                                          service_connect1,   /* callback to call after a handle to service
                                                                 is opened */
                                          NULL,                    /* closure for the above callback */
                                          multicast_ca1,      /* callback to call with peer's configuration;
                                                                 this should open the needed service connection */
                                          multicast_da1,     /* callback to be called when closing the
                                                                opened service connection */
                                          NULL);                   /* closure for the above two callbacks */
  }
}


/**
 * Test logic of peer "0" being origin starts here.
 *
 * @param cls closure, for the example: NULL
 * @param op should be equal to "dht_op"
 * @param ca_result result of the connect operation, the
 *        connection to the DHT service
 * @param emsg error message, if testbed somehow failed to
 *        connect to the DHT.
 */
static void
service_connect0 (void *cls,
                  struct GNUNET_TESTBED_Operation *op,
                  void *ca_result,
                  const char *emsg)
{
  origin = ca_result;

  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Connected to multicast service of origin\n");

  // Get GNUnet identity of origin
  pi_op0 = GNUNET_TESTBED_peer_get_information (peers[0],
                                               GNUNET_TESTBED_PIT_IDENTITY,
                                               peer_information_cb,
                                               (void *) 0);
  // Get GNUnet identity of member
  pi_op1 = GNUNET_TESTBED_peer_get_information (peers[1],
                                               GNUNET_TESTBED_PIT_IDENTITY,
                                               peer_information_cb,
                                               (void *) 1);

  /* Connection to service successful. Here we'd usually do something with
   * the service. */
  result = GNUNET_OK;
  //GNUNET_SCHEDULER_shutdown (); /* Also kills the testbed */
}



/**
 * Function run when service multicast has started and is providing us
 * with a configuration file.
 */
static void *
multicast_ca0 (void *cls,
               const struct GNUNET_CONFIGURATION_Handle *cfg)
{
  group_key = GNUNET_CRYPTO_eddsa_key_create ();
  GNUNET_CRYPTO_eddsa_key_get_public (group_key, &group_pub_key);

  return GNUNET_MULTICAST_origin_start (cfg,
                                        group_key,
                                        0,
                                        origin_join_request,
                                        NULL, /* no test for origin_replay_frag */
                                        NULL, /* no test for origin_replay_msg */
                                        origin_request,
                                        origin_message,
                                        NULL);
}

static void
multicast_da0 (void *cls,
               void *op_result)
{
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Origin closes multicast group\n");

  GNUNET_MULTICAST_origin_stop (origin, NULL, NULL);
}


/**
 * Main function inovked from TESTBED once all of the
 * peers are up and running.  This one then connects
 * just to the multicast service of peer 0 and 1.
 * Peer 0 is going to be origin.
 * Peer 1 is going to be one member.
 * Origin will start a multicast group and the member will try to join it.
 * After that we execute some multicast test.
 *
 * @param cls closure
 * @param h the run handle
 * @param peers started peers for the test
 * @param num_peers size of the 'peers' array
 * @param links_succeeded number of links between peers that were created
 * @param links_failed number of links testbed was unable to establish
 */
static void
testbed_master (void *cls,
     struct GNUNET_TESTBED_RunHandle *h,
     unsigned int num_peers,
     struct GNUNET_TESTBED_Peer **p,
     unsigned int links_succeeded,
     unsigned int links_failed)
{
  /* Testbed is ready with peers running and connected in a pre-defined overlay
     topology (FIXME)  */
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Connected to testbed_master()\n");

  peers = p;

  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Create origin peer\n");
  op0 = GNUNET_TESTBED_service_connect (NULL,                    /* Closure for operation */
                                        peers[0],                /* The peer whose service to connect to */
                                        "multicast",             /* The name of the service */
                                        service_connect0,   /* callback to call after a handle to service
                                                               is opened */
                                        NULL,                    /* closure for the above callback */
                                        multicast_ca0,      /* callback to call with peer's configuration;
                                                               this should open the needed service connection */
                                        multicast_da0,     /* callback to be called when closing the
                                                              opened service connection */
                                        NULL);                   /* closure for the above two callbacks */

  GNUNET_SCHEDULER_add_shutdown (&shutdown_task, NULL); /* Schedule a new task on shutdown */

  /* Schedule the shutdown task with a delay of a few Seconds */
  timeout_tid = GNUNET_SCHEDULER_add_delayed (GNUNET_TIME_relative_multiply(GNUNET_TIME_UNIT_SECONDS, 50),
					      &timeout_task, NULL);
}


int
main (int argc, char *argv[])
{
  int ret;

  result = GNUNET_SYSERR;
  ret = GNUNET_TESTBED_test_run
      ("test-multicast-2peers",  /* test case name */
       "test_multicast.conf", /* template configuration */
       NUM_PEERS,       /* number of peers to start */
       0LL, /* Event mask - set to 0 for no event notifications */
       NULL, /* Controller event callback */
       NULL, /* Closure for controller event callback */
       testbed_master, /* continuation callback to be called when testbed setup is complete */
       NULL); /* Closure for the test_master callback */
  if ( (GNUNET_OK != ret) || (GNUNET_OK != result) )
    return 1;
  return 0;
}


/* end of test_multicast_2peers.c */
