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

#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_message.h"
#include "gnunet_chat_util.h"

#define GNUNET_UNUSED __attribute__ ((unused))

void
on_handle_shutdown(void *cls)
{
  struct GNUNET_CHAT_Handle *chat = cls;

  GNUNET_assert((chat) && (chat->shutdown_hook));
  chat->shutdown_hook = NULL;

  handle_destroy(chat);
}

void
on_handle_arm_connection(void *cls,
			 int connected)
{
  struct GNUNET_CHAT_Handle *chat = cls;

  GNUNET_assert((chat) && (chat->arm));

  if (GNUNET_YES == connected) {
    GNUNET_ARM_request_service_start(
    	chat->arm, "identity",
    	GNUNET_OS_INHERIT_STD_NONE,
    	NULL, NULL
    );

    GNUNET_ARM_request_service_start(
	chat->arm, "messenger",
	GNUNET_OS_INHERIT_STD_NONE,
	NULL, NULL
    );

    GNUNET_ARM_request_service_start(
	chat->arm, "fs",
	GNUNET_OS_INHERIT_STD_NONE,
	NULL, NULL
    );

    GNUNET_ARM_request_service_start(
    	chat->arm, "gns",
    	GNUNET_OS_INHERIT_STD_NONE,
    	NULL, NULL
    );

    GNUNET_ARM_request_service_start(
    	chat->arm, "namestore",
    	GNUNET_OS_INHERIT_STD_NONE,
    	NULL, NULL
    );
  } else {
    GNUNET_ARM_request_service_start(
	chat->arm, "arm",
	GNUNET_OS_INHERIT_STD_NONE,
	NULL, NULL
    );
  }
}

void*
notify_handle_fs_progress(void* cls,
			  const struct GNUNET_FS_ProgressInfo* info)
{
  struct GNUNET_CHAT_Handle *chat = cls;

  GNUNET_assert(info);

  if (!chat)
    return NULL;

  switch (info->status) {
    case GNUNET_FS_STATUS_PUBLISH_START: {
      struct GNUNET_CHAT_File *file = info->value.publish.cctx;

      file_update_upload(
	  file,
	  0,
	  info->value.publish.size
      );

      return file;
    } case GNUNET_FS_STATUS_PUBLISH_PROGRESS: {
      struct GNUNET_CHAT_File *file = info->value.publish.cctx;

      file_update_upload(
	  file,
	  info->value.publish.completed,
	  info->value.publish.size
      );

      return file;
    } case GNUNET_FS_STATUS_PUBLISH_COMPLETED: {
      struct GNUNET_CHAT_File *file = info->value.publish.cctx;

      file->uri = GNUNET_FS_uri_dup(
	  info->value.publish.specifics.completed.chk_uri
      );

      file_update_upload(
      	  file,
	  info->value.publish.size,
      	  info->value.publish.size
      );

      file->publish = NULL;
      break;
    } case GNUNET_FS_STATUS_PUBLISH_ERROR: {
      break;
    } case GNUNET_FS_STATUS_DOWNLOAD_START: {
      struct GNUNET_CHAT_File *file = info->value.download.cctx;

      file_update_download(
	  file,
      	  0,
      	  info->value.download.size
      );

      return file;
    } case GNUNET_FS_STATUS_DOWNLOAD_ACTIVE: {
      return info->value.download.cctx;
    } case GNUNET_FS_STATUS_DOWNLOAD_INACTIVE: {
      return info->value.download.cctx;
    } case GNUNET_FS_STATUS_DOWNLOAD_PROGRESS: {
      struct GNUNET_CHAT_File *file = info->value.download.cctx;

      file_update_download(
      	  file,
	  info->value.download.completed,
	  info->value.download.size
      );

      return file;
    } case GNUNET_FS_STATUS_DOWNLOAD_COMPLETED: {
      struct GNUNET_CHAT_File *file = info->value.download.cctx;

      file_update_download(
	  file,
	  info->value.download.size,
	  info->value.download.size
      );

      file->download = NULL;
      break;
    } case GNUNET_FS_STATUS_DOWNLOAD_ERROR: {
      break;
    } case GNUNET_FS_STATUS_UNINDEX_START: {
      struct GNUNET_CHAT_File *file = info->value.unindex.cctx;

      file_update_unindex(
	  file,
      	  0,
      	  info->value.unindex.size
      );

      return file;
    } case GNUNET_FS_STATUS_UNINDEX_PROGRESS: {
      struct GNUNET_CHAT_File *file = info->value.unindex.cctx;

      file_update_unindex(
      	  file,
	  info->value.unindex.completed,
	  info->value.unindex.size
      );

      return file;
    } case GNUNET_FS_STATUS_UNINDEX_COMPLETED: {
      struct GNUNET_CHAT_File *file = info->value.unindex.cctx;

      file_update_unindex(
	  file,
      	  info->value.unindex.size,
      	  info->value.unindex.size
      );

      file->unindex = NULL;
      break;
    } default: {
      break;
    }
  }

  return NULL;
}

void
on_handle_gnunet_identity(void *cls,
			  struct GNUNET_IDENTITY_Ego *ego,
			  void **ctx,
                          const char *name)
{
  struct GNUNET_CHAT_Handle* handle = cls;

  if (!ctx)
    return;

  if (!ego)
    goto send_refresh;

  struct GNUNET_CHAT_InternalAccounts *accounts = handle->accounts_head;

  while (accounts)
  {
    if (!(accounts->account))
      goto skip_account;

    if (ego != accounts->account->ego)
      goto check_matching_name;

    if (name)
    {
      util_set_name_field(name, &(accounts->account->name));

      if (handle->current == accounts->account)
	handle_send_internal_message(
	    handle,
	    NULL,
	    GNUNET_CHAT_FLAG_LOGIN,
	    NULL
	);
    }
    else
    {
      if (handle->current == accounts->account)
	handle_disconnect(handle);

      account_destroy(accounts->account);

      if (accounts->op)
      {
	accounts->account = NULL;
	goto send_refresh;
      }

      GNUNET_CONTAINER_DLL_remove(
	  handle->accounts_head,
	  handle->accounts_tail,
	  accounts
      );

      GNUNET_free(accounts);
    }

    goto send_refresh;

check_matching_name:
    if ((name) && (accounts->account->name) &&
	(0 == strcmp(accounts->account->name, name)))
    {
      accounts->account->ego = ego;
      goto send_refresh;
    }

skip_account:
    accounts = accounts->next;
  }

  if (!name)
    return;

  accounts = GNUNET_new(struct GNUNET_CHAT_InternalAccounts);
  accounts->account = account_create_from_ego(ego, name);

  accounts->handle = handle;
  accounts->op = NULL;

  accounts->wait_for_completion = GNUNET_NO;

  if (handle->directory)
    account_update_directory(accounts->account, handle->directory);

  GNUNET_CONTAINER_DLL_insert_tail(
      handle->accounts_head,
      handle->accounts_tail,
      accounts
  );

send_refresh:
  handle_send_internal_message(handle, NULL, GNUNET_CHAT_FLAG_REFRESH, NULL);
}

void
cb_account_creation (void *cls,
		     const struct GNUNET_IDENTITY_PrivateKey *key,
		     const char *emsg)
{
  GNUNET_assert(cls);

  struct GNUNET_CHAT_InternalAccounts *accounts = (
      (struct GNUNET_CHAT_InternalAccounts*) cls
  );

  struct GNUNET_CHAT_Handle *handle = accounts->handle;

  GNUNET_CONTAINER_DLL_remove(
      handle->accounts_head,
      handle->accounts_tail,
      accounts
  );

  GNUNET_free(accounts);

  if (emsg)
    handle_send_internal_message(handle, NULL, GNUNET_CHAT_FLAG_WARNING, emsg);
  else if (key)
    handle_send_internal_message(handle, NULL, GNUNET_CHAT_FLAG_REFRESH, NULL);
}

void
cb_account_deletion (void *cls,
                     const char *emsg)
{
  GNUNET_assert(cls);

  struct GNUNET_CHAT_InternalAccounts *accounts = (
      (struct GNUNET_CHAT_InternalAccounts*) cls
  );

  struct GNUNET_CHAT_Handle *handle = accounts->handle;

  GNUNET_CONTAINER_DLL_remove(
      handle->accounts_head,
      handle->accounts_tail,
      accounts
  );

  GNUNET_free(accounts);

  if (emsg)
  {
    handle_send_internal_message(handle, NULL, GNUNET_CHAT_FLAG_WARNING, emsg);
    return;
  }
}

int
intern_provide_contact_for_member(struct GNUNET_CHAT_Handle *handle,
				  const struct GNUNET_MESSENGER_Contact *member,
				  struct GNUNET_CHAT_Context *context)
{
  GNUNET_assert((handle) && (handle->contacts));

  if (!member)
    return GNUNET_OK;

  struct GNUNET_ShortHashCode shorthash;
  util_shorthash_from_member(member, &shorthash);

  struct GNUNET_CHAT_Contact *contact = GNUNET_CONTAINER_multishortmap_get(
      handle->contacts, &shorthash
  );

  if (contact)
  {
    if ((context) && (NULL == contact->context))
    {
      contact->context = context;
      context->contact = member;
    }

    return GNUNET_OK;
  }

  contact = contact_create_from_member(
    handle, member
  );

  if (context)
  {
    contact->context = context;
    context->contact = member;
  }

  if (GNUNET_OK == GNUNET_CONTAINER_multishortmap_put(
      handle->contacts, &shorthash, contact,
    GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_FAST))
    return GNUNET_OK;

  if (context)
    context->contact = NULL;

  contact_destroy(contact);
  return GNUNET_SYSERR;
}

struct GNUNET_CHAT_CheckHandleRoomMembers
{
  const struct GNUNET_IDENTITY_PublicKey *ignore_key;
  const struct GNUNET_MESSENGER_Contact *contact;
};

int
check_handle_room_members (void* cls,
			   GNUNET_UNUSED struct GNUNET_MESSENGER_Room *room,
                           const struct GNUNET_MESSENGER_Contact *member)
{
  struct GNUNET_CHAT_CheckHandleRoomMembers *check = cls;

  GNUNET_assert((check) && (member));

  const struct GNUNET_IDENTITY_PublicKey *member_key = (
      GNUNET_MESSENGER_contact_get_key(member)
  );

  if ((member_key) && (check->ignore_key) &&
      (0 == GNUNET_memcmp(member_key, check->ignore_key)))
    return GNUNET_YES;

  if (check->contact)
  {
    check->contact = NULL;
    return GNUNET_NO;
  }

  check->contact = member;
  return GNUNET_YES;
}

int
scan_handle_room_members (void* cls,
			  GNUNET_UNUSED struct GNUNET_MESSENGER_Room *room,
                          const struct GNUNET_MESSENGER_Contact *member)
{
  struct GNUNET_CHAT_Handle *handle = cls;

  if (GNUNET_OK == intern_provide_contact_for_member(handle, member, NULL))
    return GNUNET_YES;
  else
    return GNUNET_NO;
}

void
on_monitor_namestore_record(void *cls,
			    GNUNET_UNUSED const
			    struct GNUNET_IDENTITY_PrivateKey *zone,
			    const char *label,
			    unsigned int count,
			    const struct GNUNET_GNSRECORD_Data *data)
{
  struct GNUNET_CHAT_Handle *chat = cls;

  if (chat->destruction)
  {
    GNUNET_NAMESTORE_zone_monitor_stop(chat->monitor);
    chat->monitor = NULL;
    return;
  }

  handle_process_records(chat, label, count, data);

  if (chat->monitor)
    GNUNET_NAMESTORE_zone_monitor_next(chat->monitor, 1);
}

void
on_handle_identity(void *cls,
		   GNUNET_UNUSED struct GNUNET_MESSENGER_Handle *messenger)
{
  struct GNUNET_CHAT_Handle *handle = cls;

  GNUNET_assert((handle) &&
		(handle->contexts) &&
		(handle->groups) &&
		(handle->contacts));

  handle_update_key(handle);

  if ((0 < GNUNET_CONTAINER_multihashmap_size(handle->contexts)) ||
      (0 < GNUNET_CONTAINER_multihashmap_size(handle->groups)) ||
      (0 < GNUNET_CONTAINER_multishortmap_size(handle->contacts)))
    return;

  GNUNET_assert(handle->messenger);

  handle_send_internal_message(
      handle,
      NULL,
      GNUNET_CHAT_FLAG_LOGIN,
      NULL
  );

  const struct GNUNET_IDENTITY_PrivateKey *zone = handle_get_key(handle);

  if ((!zone) || (handle->monitor))
    return;

  handle->monitor = GNUNET_NAMESTORE_zone_monitor_start(
      handle->cfg,
      zone,
      GNUNET_YES,
      NULL,
      NULL,
      on_monitor_namestore_record,
      handle,
      NULL,
      NULL
  );
}

void
on_handle_message_callback(void *cls)
{
  struct GNUNET_CHAT_Message *message = (struct GNUNET_CHAT_Message*) cls;

  GNUNET_assert((message) &&
		(message->msg) &&
		(message->context) &&
		(message->context->handle));

  message->task = NULL;

  struct GNUNET_CHAT_Context *context = message->context;

  switch (message->msg->header.kind)
  {
    case GNUNET_MESSENGER_KIND_DELETE:
    {
      struct GNUNET_CHAT_Message *target = GNUNET_CONTAINER_multihashmap_get(
	  context->messages, &(message->msg->body.deletion.hash)
      );

      if (target)
	target->msg = NULL;
      break;
    }
    default:
      break;
  }

  struct GNUNET_CHAT_Handle *handle = context->handle;

  if (!(handle->msg_cb))
    return;

  handle->msg_cb(handle->msg_cls, context, message);
}

void
on_handle_message (void *cls,
		   struct GNUNET_MESSENGER_Room *room,
		   const struct GNUNET_MESSENGER_Contact *sender,
		   const struct GNUNET_MESSENGER_Message *msg,
		   const struct GNUNET_HashCode *hash,
		   enum GNUNET_MESSENGER_MessageFlags flags)
{
  struct GNUNET_CHAT_Handle *handle = cls;

  GNUNET_assert((handle) &&
		(room) &&
		(msg) &&
		(hash));

  if ((handle->destruction) ||
      (GNUNET_OK != handle_request_context_by_room(handle, room)) ||
      (GNUNET_OK != intern_provide_contact_for_member(handle, sender, NULL)))
    return;

  GNUNET_MESSENGER_get_message(room, &(msg->header.previous));

  struct GNUNET_CHAT_Context *context = GNUNET_CONTAINER_multihashmap_get(
      handle->contexts, GNUNET_MESSENGER_room_get_key(room)
  );

  const struct GNUNET_TIME_Absolute timestamp = GNUNET_TIME_absolute_ntoh(
      msg->header.timestamp
  );

  struct GNUNET_ShortHashCode shorthash;
  util_shorthash_from_member(sender, &shorthash);

  struct GNUNET_CHAT_Contact *contact = GNUNET_CONTAINER_multishortmap_get(
      handle->contacts, &shorthash
  );

  if (flags & GNUNET_MESSENGER_FLAG_SENT)
    contact->is_owned = GNUNET_YES;

  struct GNUNET_TIME_Absolute *time = GNUNET_CONTAINER_multishortmap_get(
      context->timestamps, &shorthash
  );

  if (!time)
  {
    time = GNUNET_new(struct GNUNET_TIME_Absolute);
    *time = timestamp;

    if (GNUNET_OK != GNUNET_CONTAINER_multishortmap_put(
	context->timestamps, &shorthash, time,
	GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_FAST))
      GNUNET_free(time);
  }
  else
  {
    struct GNUNET_TIME_Relative delta = GNUNET_TIME_absolute_get_difference(
	timestamp, *time
    );

    if (GNUNET_TIME_relative_get_zero_().rel_value_us == delta.rel_value_us)
      *time = timestamp;
  }

  struct GNUNET_CHAT_Message *message = GNUNET_CONTAINER_multihashmap_get(
      context->messages, hash
  );

  if (message)
    return;

  struct GNUNET_SCHEDULER_Task* task = NULL;

  message = message_create_from_msg(context, hash, flags, msg);

  switch (msg->header.kind)
  {
    case GNUNET_MESSENGER_KIND_KEY:
    {
      contact_update_key(contact);
      break;
    }
    case GNUNET_MESSENGER_KIND_INVITE:
    {
      struct GNUNET_CHAT_Invitation *invitation = invitation_create_from_message(
	  context, &(msg->body.invite)
      );

      if (GNUNET_OK != GNUNET_CONTAINER_multihashmap_put(
	  context->invites, hash, invitation,
	  GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_FAST))
	invitation_destroy(invitation);
      break;
    }
    case GNUNET_MESSENGER_KIND_FILE:
    {
      GNUNET_CONTAINER_multihashmap_put(
	    context->files, hash, NULL,
	    GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_FAST
      );

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

      if (file)
	break;

      file = file_create_from_message(
	  context->handle, &(msg->body.file)
      );

      if (GNUNET_OK != GNUNET_CONTAINER_multihashmap_put(
	  context->handle->files, &(file->hash), file,
	  GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_FAST))
	file_destroy(file);
      break;
    }
    case GNUNET_MESSENGER_KIND_DELETE:
    {
      struct GNUNET_TIME_Relative delay = GNUNET_TIME_relative_ntoh(
	  msg->body.deletion.delay
      );

      task = GNUNET_SCHEDULER_add_delayed(
	  GNUNET_TIME_absolute_get_difference(
	      GNUNET_TIME_absolute_get(),
	      GNUNET_TIME_absolute_add(timestamp, delay)
	  ),
	  on_handle_message_callback,
	  message
      );
      break;
    }
    default:
      break;
  }

  if (GNUNET_OK != GNUNET_CONTAINER_multihashmap_put(
      context->messages, hash, message,
      GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_FAST))
  {
    if (task)
      GNUNET_SCHEDULER_cancel(task);

    message_destroy(message);
    return;
  }

  if (!task)
    on_handle_message_callback(message);
  else
    message->task = task;
}

int
it_destroy_handle_groups (GNUNET_UNUSED void *cls,
			  GNUNET_UNUSED const struct GNUNET_HashCode *key,
			  void *value)
{
  GNUNET_assert(value);

  struct GNUNET_CHAT_Group *group = value;
  group_destroy(group);
  return GNUNET_YES;
}

int
it_destroy_handle_contacts (GNUNET_UNUSED void *cls,
			    GNUNET_UNUSED const struct GNUNET_ShortHashCode *key,
			    void *value)
{
  GNUNET_assert(value);

  struct GNUNET_CHAT_Contact *contact = value;
  contact_destroy(contact);
  return GNUNET_YES;
}

int
it_destroy_handle_contexts (GNUNET_UNUSED void *cls,
			    GNUNET_UNUSED const struct GNUNET_HashCode *key,
			    void *value)
{
  GNUNET_assert(value);

  struct GNUNET_CHAT_Context *context = value;
  context_destroy(context);
  return GNUNET_YES;
}

int
it_destroy_handle_files (GNUNET_UNUSED void *cls,
			 GNUNET_UNUSED const struct GNUNET_HashCode *key,
			 void *value)
{
  GNUNET_assert(value);

  struct GNUNET_CHAT_File *file = value;
  file_destroy(file);
  return GNUNET_YES;
}
