/*
   This file is part of GNUnet.
   Copyright (C) 2021--2022 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
 */
/*
 * @author Tobias Frisch
 * @file gnunet_chat_lib.c
 */

#include "gnunet_chat_lib.h"

#include <limits.h>

#include "gnunet_chat_contact.h"
#include "gnunet_chat_context.h"
#include "gnunet_chat_file.h"
#include "gnunet_chat_group.h"
#include "gnunet_chat_handle.h"
#include "gnunet_chat_invitation.h"
#include "gnunet_chat_lobby.h"
#include "gnunet_chat_message.h"

#include "gnunet_chat_util.h"

#include "gnunet_chat_lib_intern.c"

#define GNUNET_CHAT_VERSION_ASSERT() {\
  GNUNET_assert(\
      (GNUNET_MESSENGER_VERSION == ((GNUNET_CHAT_VERSION >> 16L) & 0xFFFFFFFFL))\
  );\
}

struct GNUNET_CHAT_Handle*
GNUNET_CHAT_start (const struct GNUNET_CONFIGURATION_Handle *cfg,
		   GNUNET_CHAT_ContextMessageCallback msg_cb, void *msg_cls)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!cfg)
    return NULL;

  return handle_create_from_config(
      cfg,
      msg_cb,
      msg_cls
  );
}


void
GNUNET_CHAT_stop (struct GNUNET_CHAT_Handle *handle)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!handle) || (handle->destruction))
    return;

  handle->destruction = GNUNET_SCHEDULER_add_at_with_priority(
      GNUNET_TIME_absolute_get(),
      GNUNET_SCHEDULER_PRIORITY_IDLE,
      task_handle_destruction,
      handle
  );
}


int
GNUNET_CHAT_account_create (struct GNUNET_CHAT_Handle *handle,
			    const char* name)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!handle) || (handle->destruction) || (!name))
    return GNUNET_SYSERR;

  char *low = util_get_lower(name);

  int result = handle_create_account(handle, low);

  GNUNET_free(low);
  return result;
}


int
GNUNET_CHAT_account_delete(struct GNUNET_CHAT_Handle *handle,
			   const char* name)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!handle) || (handle->destruction) || (!name))
    return GNUNET_SYSERR;

  return handle_delete_account(handle, name);
}


int
GNUNET_CHAT_iterate_accounts (const struct GNUNET_CHAT_Handle *handle,
			      GNUNET_CHAT_AccountCallback callback,
			      void *cls)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!handle) || (handle->destruction))
    return GNUNET_SYSERR;

  int result = 0;

  struct GNUNET_CHAT_InternalAccounts *accounts = handle->accounts_head;
  while (accounts)
  {
    if ((!(accounts->account)) || (accounts->op))
      goto skip_account;

    result++;

    if ((callback) && (GNUNET_YES != callback(cls, handle, accounts->account)))
      break;

  skip_account:
    accounts = accounts->next;
  }

  return result;
}


void
GNUNET_CHAT_connect (struct GNUNET_CHAT_Handle *handle,
		     const struct GNUNET_CHAT_Account *account)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!handle) || (handle->destruction))
    return;

  if (handle->current == account)
    return;

  if (handle->current)
    handle_disconnect(handle);

  if (!account)
    return;

  handle_connect(handle, account);
}


void
GNUNET_CHAT_disconnect (struct GNUNET_CHAT_Handle *handle)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!handle) || (handle->destruction) || (!(handle->current)))
    return;

  handle_disconnect(handle);
}


const struct GNUNET_CHAT_Account*
GNUNET_CHAT_get_connected (const struct GNUNET_CHAT_Handle *handle)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!handle) || (handle->destruction))
    return NULL;

  return handle->current;
}


int
GNUNET_CHAT_update (struct GNUNET_CHAT_Handle *handle)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!handle) || (handle->destruction))
    return GNUNET_SYSERR;

  return GNUNET_MESSENGER_update(handle->messenger);
}


int
GNUNET_CHAT_set_name (struct GNUNET_CHAT_Handle *handle,
		      const char *name)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!handle) || (handle->destruction))
    return GNUNET_SYSERR;

  if (!name)
    return GNUNET_NO;

  char *low = util_get_lower(name);

  int result = GNUNET_MESSENGER_set_name(handle->messenger, name);

  GNUNET_free(low);
  return result;
}


const char*
GNUNET_CHAT_get_name (const struct GNUNET_CHAT_Handle *handle)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!handle) || (handle->destruction))
    return NULL;

  return GNUNET_MESSENGER_get_name(handle->messenger);
}


const char*
GNUNET_CHAT_get_key (const struct GNUNET_CHAT_Handle *handle)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!handle) || (handle->destruction))
    return NULL;

  return handle->public_key;
}


struct GNUNET_CHAT_Uri*
GNUNET_CHAT_uri_parse (const char *uri,
		       char **emsg)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!uri)
    return NULL;

  const size_t prefix_len = strlen(GNUNET_CHAT_URI_PREFIX);

  if (0 != strncmp(GNUNET_CHAT_URI_PREFIX, uri, prefix_len))
  {
    if (emsg)
      *emsg = GNUNET_strdup (_ ("CHAT URI malformed (invalid prefix)"));

    return NULL;
  }

  struct GNUNET_IDENTITY_PublicKey zone;

  const char *data = uri + prefix_len;
  char *end = strchr(data, '.');

  if (!end)
  {
    if (emsg)
      *emsg = GNUNET_strdup (_ ("CHAT URI malformed (zone key missing)"));

    return NULL;
  }

  char *zone_data = GNUNET_strndup(data, (size_t) (end - data));

  if (GNUNET_OK != GNUNET_IDENTITY_public_key_from_string(zone_data, &zone))
  {
    GNUNET_free(zone_data);

    if (emsg)
      *emsg = GNUNET_strdup (_ ("CHAT URI malformed (zone key invalid)"));

    return NULL;
  }

  GNUNET_free(zone_data);

  return uri_create(&zone, end + 1);
}


char*
GNUNET_CHAT_uri_to_string (const struct GNUNET_CHAT_Uri *uri)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!uri)
    return NULL;

  char *key_string = GNUNET_IDENTITY_public_key_to_string(&(uri->zone));

  char *string;
  GNUNET_asprintf (
      &string,
      "gnunet://chat/%s.%s",
      key_string,
      uri->label
  );

  GNUNET_free(key_string);
  return string;
}


void
GNUNET_CHAT_uri_destroy (struct GNUNET_CHAT_Uri *uri)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!uri)
    return;

  uri_destroy(uri);
}


struct GNUNET_CHAT_Lobby*
GNUNET_CHAT_lobby_open (struct GNUNET_CHAT_Handle *handle,
			struct GNUNET_TIME_Relative delay,
			GNUNET_CHAT_LobbyCallback callback,
			void *cls)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!handle) || (handle->destruction))
    return NULL;

  struct GNUNET_CHAT_InternalLobbies *lobbies = GNUNET_new(
      struct GNUNET_CHAT_InternalLobbies
  );

  lobbies->lobby = lobby_create(handle);

  GNUNET_CONTAINER_DLL_insert(
      handle->lobbies_head,
      handle->lobbies_tail,
      lobbies
  );

  lobby_open(lobbies->lobby, delay, callback, cls);

  return lobbies->lobby;
}


void
GNUNET_CHAT_lobby_close (struct GNUNET_CHAT_Lobby *lobby)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!lobby)
    return;

  struct GNUNET_CHAT_InternalLobbies *lobbies = lobby->handle->lobbies_head;

  while (lobbies)
  {
    if (lobbies->lobby == lobby)
    {
      GNUNET_CONTAINER_DLL_remove(
	  lobby->handle->lobbies_head,
	  lobby->handle->lobbies_tail,
	  lobbies
      );

      GNUNET_free(lobbies);
      break;
    }

    lobbies = lobbies->next;
  }

  lobby_destroy(lobby);
}


void
GNUNET_CHAT_lobby_join (struct GNUNET_CHAT_Handle *handle,
			const struct GNUNET_CHAT_Uri *uri)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!handle) || (handle->destruction) || (!uri) || (!(handle->gns)))
    return;

  struct GNUNET_CHAT_UriLookups *lookups = GNUNET_new(
      struct GNUNET_CHAT_UriLookups
  );

  lookups->handle = handle;
  lookups->uri = uri_create(&(uri->zone), uri->label);

  lookups->request = GNUNET_GNS_lookup(
      handle->gns,
      lookups->uri->label,
      &(uri->zone),
      GNUNET_GNSRECORD_TYPE_MESSENGER_ROOM_ENTRY,
      GNUNET_GNS_LO_DEFAULT,
      cb_lobby_lookup,
      lookups
  );

  GNUNET_CONTAINER_DLL_insert(
      handle->lookups_head,
      handle->lookups_tail,
      lookups
  );
}


void
GNUNET_CHAT_set_user_pointer (struct GNUNET_CHAT_Handle *handle,
			      void *user_pointer)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!handle) || (handle->destruction))
    return;

  handle->user_pointer = user_pointer;
}


void*
GNUNET_CHAT_get_user_pointer (const struct GNUNET_CHAT_Handle *handle)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!handle) || (handle->destruction))
    return NULL;

  return handle->user_pointer;
}


int
GNUNET_CHAT_iterate_contacts (struct GNUNET_CHAT_Handle *handle,
			      GNUNET_CHAT_ContactCallback callback,
			      void *cls)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!handle) || (handle->destruction) || (!(handle->contacts)))
    return GNUNET_SYSERR;

  struct GNUNET_CHAT_HandleIterateContacts it;
  it.handle = handle;
  it.cb = callback;
  it.cls = cls;

  return GNUNET_CONTAINER_multishortmap_iterate(
      handle->contacts, it_handle_iterate_contacts, &it
  );
}


const char*
GNUNET_CHAT_account_get_name (const struct GNUNET_CHAT_Account *account)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!account)
    return NULL;

  return account->name;
}


void
GNUNET_CHAT_account_set_user_pointer (struct GNUNET_CHAT_Account *account,
				      void *user_pointer)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!account)
    return;

  account->user_pointer = user_pointer;
}


void*
GNUNET_CHAT_account_get_user_pointer (const struct GNUNET_CHAT_Account *account)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!account)
    return NULL;

  return account->user_pointer;
}


struct GNUNET_CHAT_Group *
GNUNET_CHAT_group_create (struct GNUNET_CHAT_Handle *handle,
			  const char* topic)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!handle) || (handle->destruction) ||
      (!(handle->groups)) || (!(handle->contexts)))
    return NULL;

  struct GNUNET_HashCode key;

  if (topic)
    GNUNET_CRYPTO_hash(topic, strlen(topic), &key);
  else
    GNUNET_CRYPTO_random_block(GNUNET_CRYPTO_QUALITY_WEAK, &key, sizeof(key));

  if (GNUNET_YES == GNUNET_CONTAINER_multihashmap_contains(
      handle->contexts, &key))
    return NULL;

  struct GNUNET_MESSENGER_Room *room = GNUNET_MESSENGER_open_room(
      handle->messenger, &key
  );

  if (!room)
    return NULL;

  struct GNUNET_CHAT_Context *context = context_create_from_room(handle, room);

  handle_send_room_name(handle, room);

  context->type = GNUNET_CHAT_CONTEXT_TYPE_GROUP;

  util_set_name_field(topic, &(context->topic));

  if (GNUNET_OK != GNUNET_CONTAINER_multihashmap_put(
      handle->contexts, &key, context,
      GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_FAST))
    goto destroy_context;

  struct GNUNET_CHAT_Group *group = group_create_from_context(handle, context);

  if (context->topic)
    group_publish(group);

  if (GNUNET_OK == GNUNET_CONTAINER_multihashmap_put(
      handle->groups, &key, group,
      GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_FAST))
  {
    context_write_records(context);
    return group;
  }

  group_destroy(group);

  GNUNET_CONTAINER_multihashmap_remove(handle->contexts, &key, context);

destroy_context:
  context_destroy(context);
  return NULL;
}


int
GNUNET_CHAT_iterate_groups (struct GNUNET_CHAT_Handle *handle,
			    GNUNET_CHAT_GroupCallback callback,
			    void *cls)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!handle) || (handle->destruction) || (!(handle->groups)))
    return GNUNET_SYSERR;

  struct GNUNET_CHAT_HandleIterateGroups it;
  it.handle = handle;
  it.cb = callback;
  it.cls = cls;

  return GNUNET_CONTAINER_multihashmap_iterate(
      handle->groups, it_handle_iterate_groups, &it
  );
}


int
GNUNET_CHAT_contact_delete (struct GNUNET_CHAT_Contact *contact)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!contact)
    return GNUNET_SYSERR;

  struct GNUNET_ShortHashCode shorthash;
  util_shorthash_from_member(contact->member, &shorthash);

  GNUNET_CONTAINER_multishortmap_remove(
      contact->handle->contacts, &shorthash, contact
  );

  const struct GNUNET_HashCode *key = GNUNET_MESSENGER_room_get_key(
      contact->context->room
  );

  GNUNET_CONTAINER_multihashmap_remove(
      contact->handle->contexts, key, contact->context
  );

  GNUNET_MESSENGER_close_room(contact->context->room);

  contact->context->deleted = GNUNET_YES;
  context_write_records(contact->context);

  context_destroy(contact->context);
  contact_destroy(contact);
  return GNUNET_OK;
}


void
GNUNET_CHAT_contact_set_name (struct GNUNET_CHAT_Contact *contact,
			      const char *name)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!contact) || (!(contact->context)))
    return;

  context_update_nick(contact->context, name);

  if (contact->context->room)
    context_write_records(contact->context);
}


const char*
GNUNET_CHAT_contact_get_name (const struct GNUNET_CHAT_Contact *contact)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!contact)
    return NULL;

  if ((contact->context) && (contact->context->nick[0]))
    return contact->context->nick;

  return GNUNET_MESSENGER_contact_get_name(contact->member);
}


const char*
GNUNET_CHAT_contact_get_key (const struct GNUNET_CHAT_Contact *contact)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!contact)
    return NULL;

  return contact->public_key;
}


struct GNUNET_CHAT_Context*
GNUNET_CHAT_contact_get_context (struct GNUNET_CHAT_Contact *contact)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!contact)
    return NULL;

  if (contact->context)
    return contact->context;

  struct GNUNET_CHAT_Context *context = contact_find_context(contact);

  if ((context) && (GNUNET_CHAT_CONTEXT_TYPE_CONTACT == context->type))
    return context;

  context = context_create_from_contact(contact->handle, contact->member);

  if (context)
    contact->context = context;

  return context;
}


void
GNUNET_CHAT_contact_set_user_pointer (struct GNUNET_CHAT_Contact *contact,
				      void *user_pointer)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!contact)
    return;

  contact->user_pointer = user_pointer;
}


void*
GNUNET_CHAT_contact_get_user_pointer (const struct GNUNET_CHAT_Contact *contact)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!contact)
    return NULL;

  return contact->user_pointer;
}


int
GNUNET_CHAT_contact_is_owned (const struct GNUNET_CHAT_Contact *contact)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!contact)
    return GNUNET_SYSERR;

  return contact->is_owned;
}


int
GNUNET_CHAT_group_leave (struct GNUNET_CHAT_Group *group)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!group)
    return GNUNET_SYSERR;

  const struct GNUNET_HashCode *key = GNUNET_MESSENGER_room_get_key(
      group->context->room
  );

  GNUNET_CONTAINER_multihashmap_remove(
      group->handle->groups, key, group
  );

  GNUNET_CONTAINER_multihashmap_remove(
      group->handle->contexts, key, group->context
  );

  GNUNET_MESSENGER_close_room(group->context->room);

  group->context->deleted = GNUNET_YES;
  context_write_records(group->context);

  context_destroy(group->context);
  group_destroy(group);
  return GNUNET_OK;
}


void
GNUNET_CHAT_group_set_name (struct GNUNET_CHAT_Group *group,
			    const char *name)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!group) || (!(group->context)))
    return;

  context_update_nick(group->context, name);

  if (group->context->room)
    context_write_records(group->context);
}


const char*
GNUNET_CHAT_group_get_name (const struct GNUNET_CHAT_Group *group)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!group) || (!(group->context)))
    return NULL;

  if (group->context->nick[0])
    return group->context->nick;

  return group->context->topic;
}


void
GNUNET_CHAT_group_set_user_pointer (struct GNUNET_CHAT_Group *group,
				    void *user_pointer)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!group)
    return;

  group->user_pointer = user_pointer;
}


void*
GNUNET_CHAT_group_get_user_pointer (const struct GNUNET_CHAT_Group *group)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!group)
    return NULL;

  return group->user_pointer;
}


void
GNUNET_CHAT_group_invite_contact (const struct GNUNET_CHAT_Group *group,
				  struct GNUNET_CHAT_Contact *contact)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!group) || (!contact))
    return;

  struct GNUNET_CHAT_Context *context = contact_find_context(contact);

  if (!context)
    return;

  const struct GNUNET_HashCode *key = GNUNET_MESSENGER_room_get_key(
      group->context->room
  );

  handle_send_room_name(group->handle, GNUNET_MESSENGER_open_room(
      group->handle->messenger, key
  ));

  struct GNUNET_MESSENGER_Message msg;
  msg.header.kind = GNUNET_MESSENGER_KIND_INVITE;
  GNUNET_CRYPTO_get_peer_identity(group->handle->cfg, &(msg.body.invite.door));
  GNUNET_memcpy(&(msg.body.invite.key), key, sizeof(msg.body.invite.key));

  GNUNET_MESSENGER_send_message(context->room, &msg, contact->member);
}


int
GNUNET_CHAT_group_iterate_contacts (const struct GNUNET_CHAT_Group *group,
				    GNUNET_CHAT_GroupContactCallback callback,
				    void *cls)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!group)
    return GNUNET_SYSERR;

  struct GNUNET_CHAT_GroupIterateContacts it;
  it.group = group;
  it.cb = callback;
  it.cls = cls;

  return GNUNET_MESSENGER_iterate_members(
      group->context->room, it_group_iterate_contacts, &it
  );
}


void
GNUNET_CHAT_member_set_user_pointer (struct GNUNET_CHAT_Group *group,
				     const struct GNUNET_CHAT_Contact *member,
				     void *user_pointer)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!group) || (!(group->context)) || (!member))
    return;

  struct GNUNET_ShortHashCode hash;
  util_shorthash_from_member(member->member, &hash);

  GNUNET_CONTAINER_multishortmap_put(
      group->context->member_pointers,
      &hash,
      user_pointer,
      GNUNET_CONTAINER_MULTIHASHMAPOPTION_REPLACE
  );
}


void*
GNUNET_CHAT_member_get_user_pointer (const struct GNUNET_CHAT_Group *group,
				     const struct GNUNET_CHAT_Contact *member)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!group) || (!(group->context)) || (!member))
    return NULL;

  struct GNUNET_ShortHashCode hash;
  util_shorthash_from_member(member->member, &hash);

  return GNUNET_CONTAINER_multishortmap_get(
      group->context->member_pointers,
      &hash
  );
}


struct GNUNET_CHAT_Context*
GNUNET_CHAT_group_get_context (struct GNUNET_CHAT_Group *group)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!group)
    return NULL;

  return group->context;
}


int
GNUNET_CHAT_context_get_status (const struct GNUNET_CHAT_Context *context)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!context) || (!(context->room)))
    return GNUNET_SYSERR;

  if ((GNUNET_CHAT_CONTEXT_TYPE_UNKNOWN == context->type) ||
      (1 >= GNUNET_MESSENGER_iterate_members(context->room, NULL, NULL)))
    return GNUNET_NO;

  return GNUNET_OK;
}


void
GNUNET_CHAT_context_request (struct GNUNET_CHAT_Context *context)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!context) || (context->room))
    return;

  struct GNUNET_CHAT_Handle *handle = context->handle;

  if ((!handle) || (!(context->contact)))
    return;

  struct GNUNET_CHAT_Contact *contact = contact_create_from_member(
      handle, context->contact
  );

  if (!contact)
    return;

  context->type = GNUNET_CHAT_CONTEXT_TYPE_CONTACT;

  struct GNUNET_CHAT_Context *other = contact_find_context(contact);

  if ((!other) || (!(other->room)))
    return;

  struct GNUNET_HashCode key;
  GNUNET_CRYPTO_random_block(GNUNET_CRYPTO_QUALITY_WEAK, &key, sizeof(key));

  if (GNUNET_YES == GNUNET_CONTAINER_multihashmap_contains(
      handle->contexts, &key))
    return;

  struct GNUNET_MESSENGER_Room *room = GNUNET_MESSENGER_open_room(
      handle->messenger, &key
  );

  if (!room)
    return;

  context_update_room(context, room);
  handle_send_room_name(handle, room);

  struct GNUNET_MESSENGER_Message msg;
  msg.header.kind = GNUNET_MESSENGER_KIND_INVITE;
  GNUNET_CRYPTO_get_peer_identity(handle->cfg, &(msg.body.invite.door));
  GNUNET_memcpy(&(msg.body.invite.key), &key, sizeof(msg.body.invite.key));

  GNUNET_MESSENGER_send_message(other->room, &msg, context->contact);
}


struct GNUNET_CHAT_Contact*
GNUNET_CHAT_context_get_contact (struct GNUNET_CHAT_Context *context)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!context) || (GNUNET_CHAT_CONTEXT_TYPE_CONTACT != context->type))
    return NULL;

  if (context->contact)
    return handle_get_contact_from_messenger(context->handle, context->contact);

  struct GNUNET_MESSENGER_Room *room = context->room;
  struct GNUNET_CHAT_RoomFindContact find;

  find.ignore_key = GNUNET_MESSENGER_get_key(context->handle->messenger);
  find.contact = NULL;

  int member_count = GNUNET_MESSENGER_iterate_members(
      room,
      it_room_find_contact,
      &find
  );

  if ((!find.contact) || (member_count > 2))
    return NULL;

  return handle_get_contact_from_messenger(context->handle, find.contact);
}


struct GNUNET_CHAT_Group*
GNUNET_CHAT_context_get_group (struct GNUNET_CHAT_Context *context)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!context) || (GNUNET_CHAT_CONTEXT_TYPE_GROUP != context->type))
    return NULL;

  if (!(context->room))
    return NULL;

  return handle_get_group_from_messenger(context->handle, context->room);
}


void
GNUNET_CHAT_context_set_user_pointer (struct GNUNET_CHAT_Context *context,
				      void *user_pointer)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!context)
    return;

  context->user_pointer = user_pointer;
}


void*
GNUNET_CHAT_context_get_user_pointer (const struct GNUNET_CHAT_Context *context)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!context)
    return NULL;

  return context->user_pointer;
}


int
GNUNET_CHAT_context_send_text (struct GNUNET_CHAT_Context *context,
			       const char *text)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!context) || (!text) || (!(context->room)))
    return GNUNET_SYSERR;

  struct GNUNET_MESSENGER_Message msg;
  msg.header.kind = GNUNET_MESSENGER_KIND_TEXT;
  msg.body.text.text = GNUNET_strdup(text);

  GNUNET_MESSENGER_send_message(context->room, &msg, NULL);

  GNUNET_free(msg.body.text.text);
  return GNUNET_OK;
}


int
GNUNET_CHAT_context_send_read_receipt (struct GNUNET_CHAT_Context *context,
				       const struct GNUNET_CHAT_Message *message)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!context) || (!(context->room)))
    return GNUNET_SYSERR;

  char zero = '\0';
  struct GNUNET_MESSENGER_Message msg;
  msg.header.kind = GNUNET_MESSENGER_KIND_TEXT;
  msg.body.text.text = &zero;

  const struct GNUNET_MESSENGER_Contact *receiver = NULL;

  if (!message)
    goto skip_filter;

  if (GNUNET_CHAT_FLAG_NONE != message->flag)
    return GNUNET_SYSERR;

  if (message->flags & GNUNET_MESSENGER_FLAG_SENT)
    return GNUNET_OK;

  if (message->flags & GNUNET_MESSENGER_FLAG_PRIVATE)
  {
    receiver = GNUNET_MESSENGER_get_sender(context->room, &(message->hash));

    if (!receiver)
      return GNUNET_SYSERR;
  }

  if ((!(message->msg)) ||
      (GNUNET_MESSENGER_KIND_TEXT != message->msg->header.kind))
    goto skip_filter;

  if ((!(message->msg->body.text.text)) ||
      (!(message->msg->body.text.text[0])))
    return GNUNET_SYSERR;

skip_filter:
  GNUNET_MESSENGER_send_message(context->room, &msg, receiver);
  return GNUNET_OK;
}


struct GNUNET_CHAT_File*
GNUNET_CHAT_context_send_file (struct GNUNET_CHAT_Context *context,
			       const char *path,
			       GNUNET_CHAT_FileUploadCallback callback,
			       void *cls)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!context) || (!path) || (!(context->room)))
    return NULL;

  struct GNUNET_HashCode hash;
  if (GNUNET_OK != util_hash_file(path, &hash))
    return NULL;

  const char *directory = handle_get_directory(context->handle);

  if (!directory)
    return NULL;

  struct GNUNET_CHAT_File *file = GNUNET_CONTAINER_multihashmap_get(
      context->handle->files,
      &hash
  );

  char *filename;
  util_get_filename (
      directory, "files", &hash, &filename
  );

  if (file)
    goto file_binding;

  if ((GNUNET_YES == GNUNET_DISK_file_test(filename)) ||
      (GNUNET_OK != GNUNET_DISK_directory_create_for_file(filename)) ||
      (GNUNET_OK != GNUNET_DISK_file_copy(path, filename)))
  {
    GNUNET_free(filename);
    return NULL;
  }

  struct GNUNET_CRYPTO_SymmetricSessionKey key;
  GNUNET_CRYPTO_symmetric_create_session_key(&key);

  if (GNUNET_OK != util_encrypt_file(filename, &hash, &key))
  {
    GNUNET_free(filename);
    return NULL;
  }

  char* p = GNUNET_strdup(path);

  file = file_create_from_disk(
      context->handle,
      basename(p),
      &hash,
      &key
  );

  GNUNET_free(p);

  if (GNUNET_OK != GNUNET_CONTAINER_multihashmap_put(
      context->handle->files, &hash, file,
      GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_FAST))
  {
    file_destroy(file);
    GNUNET_free(filename);
    return NULL;
  }

  struct GNUNET_FS_BlockOptions bo;

  bo.anonymity_level = 1;
  bo.content_priority = 100;
  bo.replication_level = 1;

  bo.expiration_time = GNUNET_TIME_absolute_add(
      GNUNET_TIME_absolute_get(), GNUNET_TIME_relative_get_hour_()
  );

  struct GNUNET_FS_FileInformation* fi = GNUNET_FS_file_information_create_from_file(
      context->handle->fs,
      file,
      filename,
      NULL,
      file->meta,
      GNUNET_YES,
      &bo
  );

  file->publish = GNUNET_FS_publish_start(
      context->handle->fs, fi,
      NULL, NULL, NULL,
      GNUNET_FS_PUBLISH_OPTION_NONE
  );

  if (file->publish)
    file->status |= GNUNET_CHAT_FILE_STATUS_PUBLISH;

  GNUNET_free(filename);

file_binding:
  file_bind_upload(file, context, callback, cls);
  return file;
}


int
GNUNET_CHAT_context_share_file (struct GNUNET_CHAT_Context *context,
				const struct GNUNET_CHAT_File *file)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!context) || (!file) || (strlen(file->name) > NAME_MAX) ||
      (!(context->room)))
    return GNUNET_SYSERR;

  struct GNUNET_MESSENGER_Message msg;
  msg.header.kind = GNUNET_MESSENGER_KIND_FILE;
  GNUNET_memcpy(&(msg.body.file.key), &(file->key), sizeof(file->key));
  GNUNET_memcpy(&(msg.body.file.hash), &(file->hash), sizeof(file->hash));
  GNUNET_strlcpy(msg.body.file.name, file->name, NAME_MAX);
  msg.body.file.uri = GNUNET_FS_uri_to_string(file->uri);

  GNUNET_MESSENGER_send_message(context->room, &msg, NULL);

  GNUNET_free(msg.body.file.uri);
  return GNUNET_OK;
}


int
GNUNET_CHAT_context_iterate_messages (struct GNUNET_CHAT_Context *context,
				      GNUNET_CHAT_ContextMessageCallback callback,
				      void *cls)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!context)
    return GNUNET_SYSERR;

  struct GNUNET_CHAT_ContextIterateMessages it;
  it.context = context;
  it.cb = callback;
  it.cls = cls;

  return GNUNET_CONTAINER_multihashmap_iterate(
      context->messages, it_context_iterate_messages, &it
  );
}


int
GNUNET_CHAT_context_iterate_files (struct GNUNET_CHAT_Context *context,
				   GNUNET_CHAT_ContextFileCallback callback,
				   void *cls)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!context)
    return GNUNET_SYSERR;

  struct GNUNET_CHAT_ContextIterateFiles it;
  it.context = context;
  it.cb = callback;
  it.cls = cls;

  return GNUNET_CONTAINER_multihashmap_iterate(
      context->files, it_context_iterate_files, &it
  );
}


enum GNUNET_CHAT_MessageKind
GNUNET_CHAT_message_get_kind (const struct GNUNET_CHAT_Message *message)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!message)
    return GNUNET_CHAT_KIND_UNKNOWN;

  switch (message->flag)
  {
    case GNUNET_CHAT_FLAG_WARNING:
      return GNUNET_CHAT_KIND_WARNING;
    case GNUNET_CHAT_FLAG_REFRESH:
      return GNUNET_CHAT_KIND_REFRESH;
    case GNUNET_CHAT_FLAG_LOGIN:
      return GNUNET_CHAT_KIND_LOGIN;
    case GNUNET_CHAT_FLAG_UPDATE:
      return GNUNET_CHAT_KIND_UPDATE;
    default:
      break;
  }

  if (!(message->msg))
    return GNUNET_CHAT_KIND_UNKNOWN;

  switch (message->msg->header.kind)
  {
    case GNUNET_MESSENGER_KIND_JOIN:
      return GNUNET_CHAT_KIND_JOIN;
    case GNUNET_MESSENGER_KIND_LEAVE:
      return GNUNET_CHAT_KIND_LEAVE;
    case GNUNET_MESSENGER_KIND_NAME:
    case GNUNET_MESSENGER_KIND_KEY:
    case GNUNET_MESSENGER_KIND_ID:
      return GNUNET_CHAT_KIND_CONTACT;
    case GNUNET_MESSENGER_KIND_INVITE:
      return GNUNET_CHAT_KIND_INVITATION;
    case GNUNET_MESSENGER_KIND_TEXT:
      return GNUNET_CHAT_KIND_TEXT;
    case GNUNET_MESSENGER_KIND_FILE:
      return GNUNET_CHAT_KIND_FILE;
    case GNUNET_MESSENGER_KIND_PRIVATE:
      return GNUNET_CHAT_KIND_WHISPER;
    case GNUNET_MESSENGER_KIND_DELETE:
      return GNUNET_CHAT_KIND_DELETION;
    default:
      return GNUNET_CHAT_KIND_UNKNOWN;
  }
}


struct GNUNET_TIME_Absolute
GNUNET_CHAT_message_get_timestamp (const struct GNUNET_CHAT_Message *message)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!message) || (!(message->msg)))
    return GNUNET_TIME_absolute_get_zero_();

  return GNUNET_TIME_absolute_ntoh(message->msg->header.timestamp);
}


struct GNUNET_CHAT_Contact*
GNUNET_CHAT_message_get_sender (const struct GNUNET_CHAT_Message *message)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!message) || (GNUNET_CHAT_FLAG_NONE != message->flag) ||
      (!(message->context)) || (!(message->context->room)))
    return NULL;

  const struct GNUNET_MESSENGER_Contact *sender = GNUNET_MESSENGER_get_sender(
      message->context->room, &(message->hash)
  );

  if (!sender)
    return NULL;

  return handle_get_contact_from_messenger(message->context->handle, sender);
}


int
GNUNET_CHAT_message_is_sent (const struct GNUNET_CHAT_Message *message)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!message)
    return GNUNET_SYSERR;

  if (message->flags & GNUNET_MESSENGER_FLAG_SENT)
    return GNUNET_YES;
  else
    return GNUNET_NO;
}


int
GNUNET_CHAT_message_is_private (const struct GNUNET_CHAT_Message *message)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!message)
    return GNUNET_SYSERR;

  if (message->flags & GNUNET_MESSENGER_FLAG_PRIVATE)
    return GNUNET_YES;
  else
    return GNUNET_NO;
}


int
GNUNET_CHAT_message_get_read_receipt (const struct GNUNET_CHAT_Message *message,
				      GNUNET_CHAT_MessageReadReceiptCallback callback,
				      void *cls)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!message) || (!(message->msg)) || (!(message->context)))
    return GNUNET_SYSERR;

  struct GNUNET_CHAT_MessageIterateReadReceipts it;
  it.message = message;
  it.cb = callback;
  it.cls = cls;

  return GNUNET_MESSENGER_iterate_members(
      message->context->room, it_message_iterate_read_receipts, &it
  );
}


const char*
GNUNET_CHAT_message_get_text (const struct GNUNET_CHAT_Message *message)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!message) || (!(message->msg)))
    return NULL;

  if (GNUNET_CHAT_FLAG_WARNING == message->flag)
    return message->warning;
  if (GNUNET_MESSENGER_KIND_FILE == message->msg->header.kind)
    return message->msg->body.file.name;
  if (GNUNET_MESSENGER_KIND_TEXT != message->msg->header.kind)
    return NULL;

  return message->msg->body.text.text;
}


struct GNUNET_CHAT_File*
GNUNET_CHAT_message_get_file (const struct GNUNET_CHAT_Message *message)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!message) || (!(message->msg)) || (!(message->context)))
    return NULL;

  if (GNUNET_MESSENGER_KIND_FILE != message->msg->header.kind)
    return NULL;

  return GNUNET_CONTAINER_multihashmap_get(
      message->context->handle->files,
      &(message->msg->body.file.hash)
  );
}


struct GNUNET_CHAT_Invitation*
GNUNET_CHAT_message_get_invitation (const struct GNUNET_CHAT_Message *message)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!message) || (!(message->msg)) || (!(message->context)))
    return NULL;

  if (GNUNET_MESSENGER_KIND_INVITE != message->msg->header.kind)
    return NULL;

  return GNUNET_CONTAINER_multihashmap_get(
      message->context->invites,
      &(message->hash)
  );
}


const struct GNUNET_CHAT_Message*
GNUNET_CHAT_message_get_target (const struct GNUNET_CHAT_Message *message)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!message) || (!(message->msg)) || (!(message->context)))
    return NULL;

  struct GNUNET_CHAT_Message *target;

  if (GNUNET_MESSENGER_KIND_DELETE == message->msg->header.kind)
    target = GNUNET_CONTAINER_multihashmap_get(
	message->context->messages, &(message->msg->body.deletion.hash)
    );
  else
    target = NULL;

  return target;
}


int
GNUNET_CHAT_message_delete (const struct GNUNET_CHAT_Message *message,
			    struct GNUNET_TIME_Relative delay)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!message) || (!(message->msg)) || (!(message->context)))
    return GNUNET_SYSERR;

  struct GNUNET_MESSENGER_Message msg;
  msg.header.kind = GNUNET_MESSENGER_KIND_DELETE;
  GNUNET_memcpy(&(msg.body.deletion.hash), &(message->hash), sizeof(message->hash));
  msg.body.deletion.delay = GNUNET_TIME_relative_hton(delay);

  GNUNET_MESSENGER_send_message(message->context->room, &msg, NULL);
  return GNUNET_OK;
}


const char*
GNUNET_CHAT_file_get_name (const struct GNUNET_CHAT_File *file)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!file)
    return NULL;

  return file->name;
}


const struct GNUNET_HashCode*
GNUNET_CHAT_file_get_hash (const struct GNUNET_CHAT_File *file)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!file)
    return NULL;

  return &(file->hash);
}


uint64_t
GNUNET_CHAT_file_get_size (const struct GNUNET_CHAT_File *file)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!file) || (!(file->uri)))
    return 0;

  return GNUNET_FS_uri_chk_get_file_size(file->uri);
}


uint64_t
GNUNET_CHAT_file_get_local_size (const struct GNUNET_CHAT_File *file)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!file)
    return 0;

  const char *directory = handle_get_directory(file->handle);

  if (!directory)
    return 0;

  char *filename;
  util_get_filename (
      directory, "files", &(file->hash), &filename
  );

  uint64_t size;
  if (GNUNET_OK != GNUNET_DISK_file_size(filename, &size, GNUNET_NO, GNUNET_YES))
    size = 0;

  GNUNET_free(filename);
  return size;
}


int
GNUNET_CHAT_file_is_uploading (const struct GNUNET_CHAT_File *file)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!file) || (0 == (file->status & GNUNET_CHAT_FILE_STATUS_PUBLISH)))
    return GNUNET_NO;
  else
    return GNUNET_YES;
}


const char*
GNUNET_CHAT_file_open_preview (struct GNUNET_CHAT_File *file)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!file)
    return NULL;

  if (file->preview)
    return file->preview;

  const char *directory = handle_get_directory(file->handle);

  if (!directory)
    return NULL;

  char *filename;
  util_get_filename (
      directory, "files", &(file->hash), &filename
  );

  if (GNUNET_YES != GNUNET_DISK_file_test(filename))
    goto free_filename;

  file->preview = GNUNET_DISK_mktemp(file->name);

  if (!(file->preview))
    goto free_filename;

  remove(file->preview);

  if ((GNUNET_OK != GNUNET_DISK_file_copy(filename, file->preview)) ||
      (GNUNET_OK != util_decrypt_file(file->preview,
				      &(file->hash),
				      &(file->key))))
  {
    GNUNET_free(file->preview);
    file->preview = NULL;
  }

free_filename:
  GNUNET_free(filename);
  return file->preview;
}


void
GNUNET_CHAT_file_close_preview (struct GNUNET_CHAT_File *file)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!file) || (!(file->preview)))
    return;

  remove(file->preview);

  GNUNET_free(file->preview);
  file->preview = NULL;
}


void
GNUNET_CHAT_file_set_user_pointer (struct GNUNET_CHAT_File *file,
				   void *user_pointer)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!file)
    return;

  file->user_pointer = user_pointer;
}


void*
GNUNET_CHAT_file_get_user_pointer (const struct GNUNET_CHAT_File *file)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!file)
    return NULL;

  return file->user_pointer;
}


int
GNUNET_CHAT_file_is_downloading (const struct GNUNET_CHAT_File *file)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!file) || (0 == (file->status & GNUNET_CHAT_FILE_STATUS_DOWNLOAD)))
    return GNUNET_NO;
  else
    return GNUNET_YES;
}


int
GNUNET_CHAT_file_start_download (struct GNUNET_CHAT_File *file,
				 GNUNET_CHAT_FileDownloadCallback callback,
				 void *cls)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!file) || (!(file->uri)))
    return GNUNET_SYSERR;

  if (file->download)
  {
    file_bind_downlaod(file, callback, cls);

    GNUNET_FS_download_resume(file->download);
    return GNUNET_OK;
  }

  const char *directory = handle_get_directory(file->handle);

  if (!directory)
    return GNUNET_SYSERR;

  const uint64_t size = GNUNET_FS_uri_chk_get_file_size(file->uri);

  char *filename;
  util_get_filename (
      directory, "files", &(file->hash), &filename
  );

  uint64_t offset;
  if (GNUNET_OK != GNUNET_DISK_file_size(filename, &offset, GNUNET_NO, GNUNET_YES))
    offset = 0;

  if (offset >= size)
  {
    if (callback)
      callback(cls, file, size, size);

    return GNUNET_OK;
  }

  file_bind_downlaod(file, callback, cls);

  const uint64_t remaining = (size - offset);

  file->download = GNUNET_FS_download_start(
      file->handle->fs,
      file->uri,
      file->meta,
      filename,
      NULL,
      offset,
      remaining,
      1,
      GNUNET_FS_DOWNLOAD_OPTION_NONE,
      file,
      NULL
  );

  if (file->download)
    file->status |= GNUNET_CHAT_FILE_STATUS_DOWNLOAD;

  GNUNET_free(filename);
  return GNUNET_OK;
}


int
GNUNET_CHAT_file_pause_download (struct GNUNET_CHAT_File *file)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!file)
    return GNUNET_SYSERR;

  GNUNET_FS_download_suspend(file->download);
  return GNUNET_OK;
}


int
GNUNET_CHAT_file_resume_download (struct GNUNET_CHAT_File *file)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!file)
    return GNUNET_SYSERR;

  GNUNET_FS_download_resume(file->download);
  return GNUNET_OK;
}


int
GNUNET_CHAT_file_stop_download (struct GNUNET_CHAT_File *file)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!file)
    return GNUNET_SYSERR;

  GNUNET_FS_download_stop(file->download, GNUNET_YES);
  file->download = NULL;
  return GNUNET_OK;
}


int
GNUNET_CHAT_file_is_unindexing (const struct GNUNET_CHAT_File *file)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if ((!file) || (0 == (file->status & GNUNET_CHAT_FILE_STATUS_UNINDEX)))
    return GNUNET_NO;
  else
    return GNUNET_YES;
}


int
GNUNET_CHAT_file_unindex (struct GNUNET_CHAT_File *file,
			  GNUNET_CHAT_FileUnindexCallback callback,
			  void *cls)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!file)
    return GNUNET_SYSERR;

  if (file->publish)
  {
    GNUNET_FS_publish_stop(file->publish);
    file->publish = NULL;
    return GNUNET_OK;
  }

  file_bind_unindex(file, callback, cls);

  if (file->unindex)
    return GNUNET_OK;

  const char *directory = handle_get_directory(file->handle);

  if (!directory)
    return GNUNET_SYSERR;

  char *filename;
  util_get_filename (
      directory, "files", &(file->hash), &filename
  );

  file->unindex = GNUNET_FS_unindex_start(
      file->handle->fs, filename, file
  );

  if (file->unindex)
    file->status |= GNUNET_CHAT_FILE_STATUS_UNINDEX;

  GNUNET_free(filename);
  return GNUNET_OK;
}


void
GNUNET_CHAT_invitation_accept (struct GNUNET_CHAT_Invitation *invitation)
{
  GNUNET_CHAT_VERSION_ASSERT();

  if (!invitation)
    return;

  struct GNUNET_PeerIdentity door;
  GNUNET_PEER_resolve(invitation->door, &door);

  GNUNET_MESSENGER_enter_room(
      invitation->context->handle->messenger,
      &door, &(invitation->key)
  );
}
