/*
 * 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.
 */

/**
 * @author Gabor X Toth
 *
 * @file
 * PSYC Slicer API
 */

#include <inttypes.h>

#include "platform.h"
#include "gnunet_util_lib.h"
#include "gnunet_psyc_util_lib.h"

#define LOG(kind,...) GNUNET_log_from (kind, "psyc-util-slicer",__VA_ARGS__)


/**
 * Handle for a try-and-slice instance.
 */
struct GNUNET_PSYC_Slicer
{
  /**
   * Method handlers: H(method_name) -> SlicerMethodCallbacks
   */
  struct GNUNET_CONTAINER_MultiHashMap *method_handlers;

  /**
   * Modifier handlers: H(modifier_name) -> SlicerModifierCallbacks
   */
  struct GNUNET_CONTAINER_MultiHashMap *modifier_handlers;

  /**
   * Receive handle for incoming messages.
   */
  struct GNUNET_PSYC_ReceiveHandle *recv;

  /**
   * Currently being processed message.
   */
  const struct GNUNET_PSYC_MessageHeader *msg;

  /**
   * Currently being processed message part.
   */
  const struct GNUNET_MessageHeader *pmsg;

  /**
   * ID of currently being received message.
   */
  uint64_t message_id;

  /**
   * Fragment offset of currently being received message.
   */
  uint64_t fragment_offset;

  /**
   * Flags of currently being received message.
   */
  uint32_t flags;

  /**
   * Method name of currently being received message.
   */
  char *method_name;

  /**
   * Name of currently processed modifier.
   */
  char *mod_name;

  /**
   * Value of currently processed modifier.
   */
  char *mod_value;

  /**
   * Public key of the nym the current message originates from.
   */
  struct GNUNET_CRYPTO_EcdsaPublicKey nym_pub_key;

  /**
   * Size of @a method_name (including terminating \0).
   */
  uint16_t method_name_size;

  /**
   * Size of @a modifier_name (including terminating \0).
   */
  uint16_t mod_name_size;

  /**
   * Size of modifier value fragment.
   */
  uint16_t mod_value_size;

  /**
   * Full size of modifier value.
   */
  uint16_t mod_full_value_size;

  /**
   * Remaining bytes from the value of the current modifier.
   */
  uint16_t mod_value_remaining;

  /**
   * Operator of currently processed modifier.
   */
  uint8_t mod_oper;
};


/**
 * Callbacks for a slicer method handler.
 */
struct SlicerMethodCallbacks
{
  GNUNET_PSYC_MessageCallback msg_cb;
  GNUNET_PSYC_MethodCallback method_cb;
  GNUNET_PSYC_ModifierCallback modifier_cb;
  GNUNET_PSYC_DataCallback data_cb;
  GNUNET_PSYC_EndOfMessageCallback eom_cb;
  void *cls;
};


struct SlicerMethodRemoveClosure
{
  struct GNUNET_PSYC_Slicer *slicer;
  struct SlicerMethodCallbacks rm_cbs;
};


/**
 * Callbacks for a slicer method handler.
 */
struct SlicerModifierCallbacks
{
  GNUNET_PSYC_ModifierCallback modifier_cb;
  void *cls;
};


struct SlicerModifierRemoveClosure
{
  struct GNUNET_PSYC_Slicer *slicer;
  struct SlicerModifierCallbacks rm_cbs;
};


/**
 * Call a method handler for an incoming message part.
 */
static int
slicer_method_handler_notify (void *cls, const struct GNUNET_HashCode *key,
                              void *value)
{
  struct GNUNET_PSYC_Slicer *slicer = cls;
  const struct GNUNET_MessageHeader *pmsg = slicer->pmsg;
  struct SlicerMethodCallbacks *cbs = value;

  uint16_t ptype = ntohs (pmsg->type);
  switch (ptype)
  {
  case GNUNET_MESSAGE_TYPE_PSYC_MESSAGE_METHOD:
  {
    if (NULL != cbs->msg_cb)
      cbs->msg_cb (cbs->cls, slicer->msg);
    if (NULL == cbs->method_cb)
      break;
    struct GNUNET_PSYC_MessageMethod *
      meth = (struct GNUNET_PSYC_MessageMethod *) pmsg;
    cbs->method_cb (cbs->cls, slicer->msg, meth, slicer->message_id,
                    slicer->method_name);
    break;
  }

  case GNUNET_MESSAGE_TYPE_PSYC_MESSAGE_MODIFIER:
  {
    if (NULL == cbs->modifier_cb)
      break;
    struct GNUNET_PSYC_MessageModifier *
      mod = (struct GNUNET_PSYC_MessageModifier *) pmsg;
    cbs->modifier_cb (cbs->cls, slicer->msg, &mod->header, slicer->message_id,
                      mod->oper, (const char *) &mod[1],
                      (const void *) &mod[1] + ntohs (mod->name_size),
                      ntohs (mod->header.size) - sizeof (*mod) - ntohs (mod->name_size),
                      ntohs (mod->value_size));
    break;
  }

  case GNUNET_MESSAGE_TYPE_PSYC_MESSAGE_MOD_CONT:
  {
    if (NULL == cbs->modifier_cb)
      break;
    cbs->modifier_cb (cbs->cls, slicer->msg, pmsg, slicer->message_id,
                      slicer->mod_oper, slicer->mod_name, &pmsg[1],
                      ntohs (pmsg->size) - sizeof (*pmsg),
                      slicer->mod_full_value_size);
    break;
  }

  case GNUNET_MESSAGE_TYPE_PSYC_MESSAGE_DATA:
  {
    if (NULL == cbs->data_cb)
      break;
    cbs->data_cb (cbs->cls, slicer->msg, pmsg, slicer->message_id,
                  &pmsg[1], ntohs (pmsg->size) - sizeof (*pmsg));
    break;
  }

  case GNUNET_MESSAGE_TYPE_PSYC_MESSAGE_END:
    if (NULL == cbs->eom_cb)
      break;
    cbs->eom_cb (cbs->cls, slicer->msg, pmsg, slicer->message_id, GNUNET_NO);
    break;

  case GNUNET_MESSAGE_TYPE_PSYC_MESSAGE_CANCEL:
    if (NULL == cbs->eom_cb)
      break;
    cbs->eom_cb (cbs->cls, slicer->msg, pmsg, slicer->message_id, GNUNET_YES);
    break;
  }
  return GNUNET_YES;
}


/**
 * Call a method handler for an incoming message part.
 */
static int
slicer_modifier_handler_notify (void *cls, const struct GNUNET_HashCode *key,
                                void *value)
{
  struct GNUNET_PSYC_Slicer *slicer = cls;
  struct SlicerModifierCallbacks *cbs = value;

  cbs->modifier_cb (cbs->cls, slicer->msg, slicer->pmsg, slicer->message_id,
                    slicer->mod_oper, slicer->mod_name, slicer->mod_value,
                    slicer->mod_value_size, slicer->mod_full_value_size);
  return GNUNET_YES;
}


/**
 * Process an incoming message and call matching handlers.
 *
 * @param slicer
 *        The slicer to use.
 * @param msg
 *        The message as it arrived from the network.
 */
void
GNUNET_PSYC_slicer_message (struct GNUNET_PSYC_Slicer *slicer,
                            const struct GNUNET_PSYC_MessageHeader *msg)
{
  GNUNET_PSYC_receive_message (slicer->recv, msg);
}


/**
 * Process an incoming message part and call matching handlers.
 *
 * @param cls
 *        Closure.
 * @param message_id
 *        ID of the message.
 * @param flags
 *        Flags for the message.
 *        @see enum GNUNET_PSYC_MessageFlags
 * @param msg
 *        The message part. as it arrived from the network.
 */
void
GNUNET_PSYC_slicer_message_part (struct GNUNET_PSYC_Slicer *slicer,
                                 const struct GNUNET_PSYC_MessageHeader *msg,
                                 const struct GNUNET_MessageHeader *pmsg)
{
  slicer->msg = msg;
  slicer->pmsg = pmsg;

  uint64_t message_id = GNUNET_ntohll (msg->message_id);

  uint16_t ptype = ntohs (pmsg->type);
  if (GNUNET_MESSAGE_TYPE_PSYC_MESSAGE_METHOD == ptype)
  {
    struct GNUNET_PSYC_MessageMethod *
      meth = (struct GNUNET_PSYC_MessageMethod *) pmsg;
    slicer->method_name_size = ntohs (meth->header.size) - sizeof (*meth);
    slicer->method_name = GNUNET_malloc (slicer->method_name_size);
    GNUNET_memcpy (slicer->method_name, &meth[1], slicer->method_name_size);
     slicer->message_id = message_id;
  }
  else
  {
    GNUNET_assert (message_id == slicer->message_id);
  }

  char *nym_str = GNUNET_CRYPTO_ecdsa_public_key_to_string (&msg->slave_pub_key);
  LOG (GNUNET_ERROR_TYPE_DEBUG,
       "Slicer received message of type %u and size %u, "
       "with ID %" PRIu64 " and method %s from %s\n",
       ptype, ntohs (pmsg->size), message_id, slicer->method_name, nym_str);
  GNUNET_free (nym_str);

  /* try-and-slice modifier */

  switch (ptype)
  {
  case GNUNET_MESSAGE_TYPE_PSYC_MESSAGE_MODIFIER:
  {
    struct GNUNET_PSYC_MessageModifier *
      mod = (struct GNUNET_PSYC_MessageModifier *) pmsg;
    slicer->mod_oper = mod->oper;
    slicer->mod_name_size = ntohs (mod->name_size);
    slicer->mod_name = GNUNET_malloc (slicer->mod_name_size);
    GNUNET_memcpy (slicer->mod_name, &mod[1], slicer->mod_name_size);
    slicer->mod_value = (char *) &mod[1] + slicer->mod_name_size;
    slicer->mod_full_value_size = ntohs (mod->value_size);
    slicer->mod_value_remaining = slicer->mod_full_value_size;
    slicer->mod_value_size
      = ntohs (mod->header.size) - sizeof (*mod) - slicer->mod_name_size;
    // fall through
  }
  case GNUNET_MESSAGE_TYPE_PSYC_MESSAGE_MOD_CONT:
    if (ptype == GNUNET_MESSAGE_TYPE_PSYC_MESSAGE_MOD_CONT)
    {
      slicer->mod_value = (char *) &pmsg[1];
      slicer->mod_value_size = ntohs (pmsg->size) - sizeof (*pmsg);
    }
    slicer->mod_value_remaining -= slicer->mod_value_size;
    char *name = GNUNET_malloc (slicer->mod_name_size);
    GNUNET_memcpy (name, slicer->mod_name, slicer->mod_name_size);
    do
    {
      struct GNUNET_HashCode key;
      uint16_t name_len = strlen (name);
      GNUNET_CRYPTO_hash (name, name_len, &key);
      GNUNET_CONTAINER_multihashmap_get_multiple (slicer->modifier_handlers, &key,
                                                  slicer_modifier_handler_notify,
                                                  slicer);
      char *p = strrchr (name, '_');
      if (NULL == p)
        break;
      *p = '\0';
    } while (1);
    GNUNET_free (name);
  }

  /* try-and-slice method */

  char *name = GNUNET_malloc (slicer->method_name_size);
  GNUNET_memcpy (name, slicer->method_name, slicer->method_name_size);
  do
  {
    struct GNUNET_HashCode key;
    uint16_t name_len = strlen (name);
    GNUNET_CRYPTO_hash (name, name_len, &key);
    GNUNET_CONTAINER_multihashmap_get_multiple (slicer->method_handlers, &key,
                                                slicer_method_handler_notify,
                                                slicer);
    char *p = strrchr (name, '_');
    if (NULL == p)
      break;
    *p = '\0';
  } while (1);
  GNUNET_free (name);

  if (GNUNET_MESSAGE_TYPE_PSYC_MESSAGE_END <= ptype)
    GNUNET_free (slicer->method_name);

  if (0 == slicer->mod_value_remaining && NULL != slicer->mod_name)
  {
    GNUNET_free (slicer->mod_name);
    slicer->mod_name = NULL;
    slicer->mod_name_size = 0;
    slicer->mod_value_size = 0;
    slicer->mod_full_value_size = 0;
    slicer->mod_oper = 0;
  }

  slicer->msg = NULL;
  slicer->pmsg = NULL;
}


/**
 * Create a try-and-slice instance.
 *
 * A slicer processes incoming messages and notifies callbacks about matching
 * methods or modifiers encountered.
 *
 * @return A new try-and-slice construct.
 */
struct GNUNET_PSYC_Slicer *
GNUNET_PSYC_slicer_create (void)
{
  struct GNUNET_PSYC_Slicer *slicer = GNUNET_malloc (sizeof (*slicer));
  slicer->method_handlers = GNUNET_CONTAINER_multihashmap_create (1, GNUNET_NO);
  slicer->modifier_handlers = GNUNET_CONTAINER_multihashmap_create (1, GNUNET_NO);
  slicer->recv = GNUNET_PSYC_receive_create (NULL,
                                             (GNUNET_PSYC_MessagePartCallback)
                                             GNUNET_PSYC_slicer_message_part,
                                             slicer);
  return slicer;
}


/**
 * Add a method to the try-and-slice instance.
 *
 * The callbacks are called for messages with a matching @a method_name prefix.
 *
 * @param slicer
 *        The try-and-slice instance to extend.
 * @param method_name
 *        Name of the given method, use empty string to match all.
 * @param method_cb
 *        Method handler invoked upon a matching message.
 * @param modifier_cb
 *        Modifier handler, invoked after @a method_cb
 *        for each modifier in the message.
 * @param data_cb
 *        Data handler, invoked after @a modifier_cb for each data fragment.
 * @param eom_cb
 *        Invoked upon reaching the end of a matching message.
 * @param cls
 *        Closure for the callbacks.
 */
void
GNUNET_PSYC_slicer_method_add (struct GNUNET_PSYC_Slicer *slicer,
                               const char *method_name,
                               GNUNET_PSYC_MessageCallback msg_cb,
                               GNUNET_PSYC_MethodCallback method_cb,
                               GNUNET_PSYC_ModifierCallback modifier_cb,
                               GNUNET_PSYC_DataCallback data_cb,
                               GNUNET_PSYC_EndOfMessageCallback eom_cb,
                               void *cls)
{
  struct GNUNET_HashCode key;
  GNUNET_CRYPTO_hash (method_name, strlen (method_name), &key);

  struct SlicerMethodCallbacks *cbs = GNUNET_malloc (sizeof (*cbs));
  cbs->msg_cb = msg_cb,
  cbs->method_cb = method_cb;
  cbs->modifier_cb = modifier_cb;
  cbs->data_cb = data_cb;
  cbs->eom_cb = eom_cb;
  cbs->cls = cls;

  GNUNET_CONTAINER_multihashmap_put (slicer->method_handlers, &key, cbs,
                                     GNUNET_CONTAINER_MULTIHASHMAPOPTION_MULTIPLE);
}


static int
slicer_method_remove (void *cls, const struct GNUNET_HashCode *key, void *value)
{
  struct SlicerMethodRemoveClosure *rm_cls = cls;
  struct GNUNET_PSYC_Slicer *slicer = rm_cls->slicer;
  struct SlicerMethodCallbacks *rm_cbs = &rm_cls->rm_cbs;
  struct SlicerMethodCallbacks *cbs = value;

  if ((NULL == rm_cbs->msg_cb || cbs->msg_cb == rm_cbs->msg_cb)
      && (NULL == rm_cbs->method_cb || cbs->method_cb == rm_cbs->method_cb)
      && (NULL == rm_cbs->modifier_cb || cbs->modifier_cb == rm_cbs->modifier_cb)
      && (NULL == rm_cbs->data_cb || cbs->data_cb == rm_cbs->data_cb)
      && (NULL == rm_cbs->eom_cb || cbs->eom_cb == rm_cbs->eom_cb))
  {
    GNUNET_CONTAINER_multihashmap_remove (slicer->method_handlers, key, cbs);
    GNUNET_free (cbs);
    return GNUNET_NO;
  }
  return GNUNET_YES;
}


/**
 * Remove a registered method from the try-and-slice instance.
 *
 * Removes one matching handler registered with the given
 * @a method_name and  callbacks.
 *
 * @param slicer
 *        The try-and-slice instance.
 * @param method_name
 *        Name of the method to remove.
 * @param method_cb
 *        Method handler.
 * @param modifier_cb
 *        Modifier handler.
 * @param data_cb
 *        Data handler.
 * @param eom_cb
 *        End of message handler.
 *
 * @return #GNUNET_OK if a method handler was removed,
 *         #GNUNET_NO if no handler matched the given method name and callbacks.
 */
int
GNUNET_PSYC_slicer_method_remove (struct GNUNET_PSYC_Slicer *slicer,
                                  const char *method_name,
                                  GNUNET_PSYC_MessageCallback msg_cb,
                                  GNUNET_PSYC_MethodCallback method_cb,
                                  GNUNET_PSYC_ModifierCallback modifier_cb,
                                  GNUNET_PSYC_DataCallback data_cb,
                                  GNUNET_PSYC_EndOfMessageCallback eom_cb)
{
  struct GNUNET_HashCode key;
  GNUNET_CRYPTO_hash (method_name, strlen (method_name), &key);

  struct SlicerMethodRemoveClosure rm_cls;
  rm_cls.slicer = slicer;
  struct SlicerMethodCallbacks *rm_cbs = &rm_cls.rm_cbs;
  rm_cbs->msg_cb = msg_cb;
  rm_cbs->method_cb = method_cb;
  rm_cbs->modifier_cb = modifier_cb;
  rm_cbs->data_cb = data_cb;
  rm_cbs->eom_cb = eom_cb;

  return
    (GNUNET_SYSERR
     == GNUNET_CONTAINER_multihashmap_get_multiple (slicer->method_handlers, &key,
                                                    slicer_method_remove,
                                                    &rm_cls))
    ? GNUNET_NO
    : GNUNET_OK;
}


/**
 * Watch a place for changed objects.
 *
 * @param slicer
 *        The try-and-slice instance.
 * @param object_filter
 *        Object prefix to match.
 * @param modifier_cb
 *        Function to call when encountering a state modifier.
 * @param cls
 *        Closure for callback.
 */
void
GNUNET_PSYC_slicer_modifier_add (struct GNUNET_PSYC_Slicer *slicer,
                                 const char *object_filter,
                                 GNUNET_PSYC_ModifierCallback modifier_cb,
                                 void *cls)
{
  struct SlicerModifierCallbacks *cbs = GNUNET_malloc (sizeof *cbs);
  cbs->modifier_cb = modifier_cb;
  cbs->cls = cls;

  struct GNUNET_HashCode key;
  GNUNET_CRYPTO_hash (object_filter, strlen (object_filter), &key);
  GNUNET_CONTAINER_multihashmap_put (slicer->modifier_handlers, &key, cbs,
                                     GNUNET_CONTAINER_MULTIHASHMAPOPTION_MULTIPLE);
}


static int
slicer_modifier_remove (void *cls, const struct GNUNET_HashCode *key, void *value)
{
  struct SlicerModifierRemoveClosure *rm_cls = cls;
  struct GNUNET_PSYC_Slicer *slicer = rm_cls->slicer;
  struct SlicerModifierCallbacks *rm_cbs = &rm_cls->rm_cbs;
  struct SlicerModifierCallbacks *cbs = value;

  if (cbs->modifier_cb == rm_cbs->modifier_cb)
  {
    GNUNET_CONTAINER_multihashmap_remove (slicer->modifier_handlers, key, cbs);
    GNUNET_free (cbs);
    return GNUNET_NO;
  }
  return GNUNET_YES;
}


/**
 * Remove a registered modifier from the try-and-slice instance.
 *
 * Removes one matching handler registered with the given
 * @a object_filter and @a modifier_cb.
 *
 * @param slicer
 *        The try-and-slice instance.
 * @param object_filter
 *        Object prefix to match.
 * @param modifier_cb
 *        Function to call when encountering a state modifier changes.
 */
int
GNUNET_PSYC_slicer_modifier_remove (struct GNUNET_PSYC_Slicer *slicer,
                                    const char *object_filter,
                                    GNUNET_PSYC_ModifierCallback modifier_cb)
{
  struct GNUNET_HashCode key;
  GNUNET_CRYPTO_hash (object_filter, strlen (object_filter), &key);

  struct SlicerModifierRemoveClosure rm_cls;
  rm_cls.slicer = slicer;
  struct SlicerModifierCallbacks *rm_cbs = &rm_cls.rm_cbs;
  rm_cbs->modifier_cb = modifier_cb;

  return
    (GNUNET_SYSERR
     == GNUNET_CONTAINER_multihashmap_get_multiple (slicer->modifier_handlers, &key,
                                                    slicer_modifier_remove,
                                                    &rm_cls))
    ? GNUNET_NO
    : GNUNET_OK;
 }


static int
slicer_method_free (void *cls, const struct GNUNET_HashCode *key, void *value)
{
  struct SlicerMethodCallbacks *cbs = value;
  GNUNET_free (cbs);
  return GNUNET_YES;
}


static int
slicer_modifier_free (void *cls, const struct GNUNET_HashCode *key, void *value)
{
  struct SlicerModifierCallbacks *cbs = value;
  GNUNET_free (cbs);
  return GNUNET_YES;
}


/**
 * Remove all registered method handlers.
 *
 * @param slicer
 *        Slicer to clear.
 */
void
GNUNET_PSYC_slicer_method_clear (struct GNUNET_PSYC_Slicer *slicer)
{
  GNUNET_CONTAINER_multihashmap_iterate (slicer->method_handlers,
                                         slicer_method_free, NULL);
  GNUNET_CONTAINER_multihashmap_clear (slicer->method_handlers);
}


/**
 * Remove all registered modifier handlers.
 *
 * @param slicer
 *        Slicer to clear.
 */
void
GNUNET_PSYC_slicer_modifier_clear (struct GNUNET_PSYC_Slicer *slicer)
{
  GNUNET_CONTAINER_multihashmap_iterate (slicer->modifier_handlers,
                                         slicer_modifier_free, NULL);
  GNUNET_CONTAINER_multihashmap_clear (slicer->modifier_handlers);
}


/**
 * Remove all registered method & modifier handlers.
 *
 * @param slicer
 *        Slicer to clear.
 */
void
GNUNET_PSYC_slicer_clear (struct GNUNET_PSYC_Slicer *slicer)
{
  GNUNET_PSYC_slicer_method_clear (slicer);
  GNUNET_PSYC_slicer_modifier_clear (slicer);
}


/**
 * Destroy a given try-and-slice instance.
 *
 * @param slicer
 *        Slicer to destroy
 */
void
GNUNET_PSYC_slicer_destroy (struct GNUNET_PSYC_Slicer *slicer)
{
  GNUNET_PSYC_slicer_clear (slicer);
  GNUNET_CONTAINER_multihashmap_destroy (slicer->method_handlers);
  GNUNET_CONTAINER_multihashmap_destroy (slicer->modifier_handlers);
  GNUNET_PSYC_receive_destroy (slicer->recv);
  GNUNET_free (slicer);
}
