/*
 * This file is part of GNUnet
 * Copyright (C) 2015 Christian Grothoff (and other contributing authors)
 *
 * GNUnet is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Affero General Public License as published
 * by the Free Software Foundation, either version 3 of the License,
 * or (at your option) any later version.
 *
 * GNUnet is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.

     SPDX-License-Identifier: AGPL3.0-or-later
 */

/**
 * @file peerstore/plugin_peerstore_flat.c
 * @brief flat file-based peerstore backend
 * @author Martin Schanzenbach
 */

#include "platform.h"
#include "gnunet_peerstore_plugin.h"
#include "gnunet_peerstore_service.h"
#include "peerstore.h"

/**
 * Context for all functions in this plugin.
 */
struct Plugin
{
  /**
   * Configuration handle
   */
  const struct GNUNET_CONFIGURATION_Handle *cfg;

  /**
   * HashMap
   */
  struct GNUNET_CONTAINER_MultiHashMap *hm;

  /**
   * Iterator
   */
  GNUNET_PEERSTORE_Processor iter;

  /**
   * Iterator cls
   */
  void *iter_cls;

  /**
   * iterator key
   */
  const char *iter_key;

  /**
   * Iterator peer
   */
  const struct GNUNET_PeerIdentity *iter_peer;

  /**
   * Iterator subsystem
   */
  const char *iter_sub_system;

  /**
   * Iterator time
   */
  struct GNUNET_TIME_Absolute iter_now;

  /**
   * Deleted entries
   */
  uint64_t deleted_entries;

  /**
   * Expired entries
   */
  uint64_t exp_changes;

  /**
   * Database filename.
   */
  char *fn;

  /**
   * Result found bool
   */
  int iter_result_found;
};


static int
delete_entries (void *cls,
                const struct GNUNET_HashCode *key,
                void *value)
{
  struct Plugin *plugin = cls;
  struct GNUNET_PEERSTORE_Record *entry = value;

  if (0 != strcmp (plugin->iter_key, entry->key))
    return GNUNET_YES;
  if (0 != memcmp (plugin->iter_peer,
                   &entry->peer,
                   sizeof(struct GNUNET_PeerIdentity)))
    return GNUNET_YES;
  if (0 != strcmp (plugin->iter_sub_system, entry->sub_system))
    return GNUNET_YES;

  GNUNET_CONTAINER_multihashmap_remove (plugin->hm, key, value);
  plugin->deleted_entries++;
  return GNUNET_YES;
}


/**
 * Delete records with the given key
 *
 * @param cls closure (internal context for the plugin)
 * @param sub_system name of sub system
 * @param peer Peer identity (can be NULL)
 * @param key entry key string (can be NULL)
 * @return number of deleted records
 */
static int
peerstore_flat_delete_records (void *cls, const char *sub_system,
                               const struct GNUNET_PeerIdentity *peer,
                               const char *key)
{
  struct Plugin *plugin = cls;

  plugin->iter_sub_system = sub_system;
  plugin->iter_peer = peer;
  plugin->iter_key = key;
  plugin->deleted_entries = 0;

  GNUNET_CONTAINER_multihashmap_iterate (plugin->hm,
                                         &delete_entries,
                                         plugin);
  return plugin->deleted_entries;
}


static int
expire_entries (void *cls,
                const struct GNUNET_HashCode *key,
                void *value)
{
  struct Plugin *plugin = cls;
  struct GNUNET_PEERSTORE_Record *entry = value;

  if (entry->expiry.abs_value_us < plugin->iter_now.abs_value_us)
  {
    GNUNET_CONTAINER_multihashmap_remove (plugin->hm, key, value);
    plugin->exp_changes++;
  }
  return GNUNET_YES;
}


/**
 * Delete expired records (expiry < now)
 *
 * @param cls closure (internal context for the plugin)
 * @param now time to use as reference
 * @param cont continuation called with the number of records expired
 * @param cont_cls continuation closure
 * @return #GNUNET_OK on success, #GNUNET_SYSERR on error and cont is not
 * called
 */
static int
peerstore_flat_expire_records (void *cls, struct GNUNET_TIME_Absolute now,
                               GNUNET_PEERSTORE_Continuation cont,
                               void *cont_cls)
{
  struct Plugin *plugin = cls;

  plugin->exp_changes = 0;
  plugin->iter_now = now;

  GNUNET_CONTAINER_multihashmap_iterate (plugin->hm,
                                         &expire_entries,
                                         plugin);
  if (NULL != cont)
  {
    cont (cont_cls, plugin->exp_changes);
  }
  return GNUNET_OK;
}


static int
iterate_entries (void *cls,
                 const struct GNUNET_HashCode *key,
                 void *value)
{
  struct Plugin *plugin = cls;
  struct GNUNET_PEERSTORE_Record *entry = value;

  if ((NULL != plugin->iter_peer) &&
      (0 != memcmp (plugin->iter_peer,
                    &entry->peer,
                    sizeof(struct GNUNET_PeerIdentity))))
  {
    return GNUNET_YES;
  }
  if ((NULL != plugin->iter_key) &&
      (0 != strcmp (plugin->iter_key,
                    entry->key)))
  {
    return GNUNET_YES;
  }
  if (NULL != plugin->iter)
    plugin->iter (plugin->iter_cls, entry, NULL);
  plugin->iter_result_found = GNUNET_YES;
  return GNUNET_YES;
}


/**
 * Iterate over the records given an optional peer id
 * and/or key.
 *
 * @param cls closure (internal context for the plugin)
 * @param sub_system name of sub system
 * @param peer Peer identity (can be NULL)
 * @param key entry key string (can be NULL)
 * @param iter function to call asynchronously with the results, terminated
 * by a NULL result
 * @param iter_cls closure for @a iter
 * @return #GNUNET_OK on success, #GNUNET_SYSERR on error and iter is not
 * called
 */
static int
peerstore_flat_iterate_records (void *cls, const char *sub_system,
                                const struct GNUNET_PeerIdentity *peer,
                                const char *key,
                                GNUNET_PEERSTORE_Processor iter,
                                void *iter_cls)
{
  struct Plugin *plugin = cls;

  plugin->iter = iter;
  plugin->iter_cls = iter_cls;
  plugin->iter_peer = peer;
  plugin->iter_sub_system = sub_system;
  plugin->iter_key = key;

  GNUNET_CONTAINER_multihashmap_iterate (plugin->hm,
                                         &iterate_entries,
                                         plugin);
  if (NULL != iter)
    iter (iter_cls, NULL, NULL);
  return GNUNET_OK;
}


/**
 * Store a record in the peerstore.
 * Key is the combination of sub system and peer identity.
 * One key can store multiple values.
 *
 * @param cls closure (internal context for the plugin)
 * @param sub_system name of the GNUnet sub system responsible
 * @param peer peer identity
 * @param key record key string
 * @param value value to be stored
 * @param size size of value to be stored
 * @param expiry absolute time after which the record is (possibly) deleted
 * @param options options related to the store operation
 * @param cont continuation called when record is stored
 * @param cont_cls continuation closure
 * @return #GNUNET_OK on success, else #GNUNET_SYSERR and cont is not called
 */
static int
peerstore_flat_store_record (void *cls, const char *sub_system,
                             const struct GNUNET_PeerIdentity *peer,
                             const char *key, const void *value, size_t size,
                             struct GNUNET_TIME_Absolute expiry,
                             enum GNUNET_PEERSTORE_StoreOption options,
                             GNUNET_PEERSTORE_Continuation cont,
                             void *cont_cls)
{
  struct Plugin *plugin = cls;
  struct GNUNET_HashCode hkey;
  struct GNUNET_PEERSTORE_Record *entry;
  const char *peer_id;


  entry = GNUNET_new (struct GNUNET_PEERSTORE_Record);
  entry->sub_system = GNUNET_strdup (sub_system);
  entry->key = GNUNET_strdup (key);
  entry->value = GNUNET_malloc (size);
  GNUNET_memcpy (entry->value, value, size);
  entry->value_size = size;
  entry->peer = *peer;
  entry->expiry = expiry;

  peer_id = GNUNET_i2s (peer);
  GNUNET_CRYPTO_hash (peer_id,
                      strlen (peer_id),
                      &hkey);

  if (GNUNET_PEERSTORE_STOREOPTION_REPLACE == options)
  {
    peerstore_flat_delete_records (cls, sub_system, peer, key);
  }

  GNUNET_CONTAINER_multihashmap_put (plugin->hm,
                                     &hkey,
                                     entry,
                                     GNUNET_CONTAINER_MULTIHASHMAPOPTION_MULTIPLE);
  if (NULL != cont)
  {
    cont (cont_cls, GNUNET_OK);
  }
  return GNUNET_OK;
}


/**
 * Initialize the database connections and associated
 * data structures (create tables and indices
 * as needed as well).
 *
 * @param plugin the plugin context (state for this module)
 * @return GNUNET_OK on success
 */
static int
database_setup (struct Plugin *plugin)
{
  char *afsdir;
  char *key;
  char *sub_system;
  const char *peer_id;
  char *peer;
  char *value;
  char *expiry;
  struct GNUNET_DISK_FileHandle *fh;
  struct GNUNET_PEERSTORE_Record *entry;
  struct GNUNET_HashCode hkey;
  uint64_t size;
  char *buffer;
  char *line;

  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_filename (plugin->cfg, "peerstore-flat",
                                               "FILENAME", &afsdir))
  {
    GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, "peerstore-flat",
                               "FILENAME");
    return GNUNET_SYSERR;
  }
  if (GNUNET_OK != GNUNET_DISK_file_test (afsdir))
  {
    if (GNUNET_OK != GNUNET_DISK_directory_create_for_file (afsdir))
    {
      GNUNET_break (0);
      GNUNET_free (afsdir);
      return GNUNET_SYSERR;
    }
  }
  /* afsdir should be UTF-8-encoded. If it isn't, it's a bug */
  plugin->fn = afsdir;

  fh = GNUNET_DISK_file_open (afsdir,
                              GNUNET_DISK_OPEN_CREATE
                              | GNUNET_DISK_OPEN_READWRITE,
                              GNUNET_DISK_PERM_USER_WRITE
                              | GNUNET_DISK_PERM_USER_READ);
  if (NULL == fh)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                _ ("Unable to initialize file: %s.\n"),
                afsdir);
    return GNUNET_SYSERR;
  }

  /* Load data from file into hashmap */
  plugin->hm = GNUNET_CONTAINER_multihashmap_create (10,
                                                     GNUNET_NO);

  if (GNUNET_SYSERR == GNUNET_DISK_file_size (afsdir,
                                              &size,
                                              GNUNET_YES,
                                              GNUNET_YES))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                _ ("Unable to get filesize: %s.\n"),
                afsdir);
    return GNUNET_SYSERR;
  }

  buffer = GNUNET_malloc (size + 1);

  if (GNUNET_SYSERR == GNUNET_DISK_file_read (fh,
                                              buffer,
                                              size))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                _ ("Unable to read file: %s.\n"),
                afsdir);
    GNUNET_DISK_file_close (fh);
    GNUNET_free (buffer);
    return GNUNET_SYSERR;
  }

  buffer[size] = '\0';
  GNUNET_DISK_file_close (fh);
  if (0 < size)
  {
    line = strtok (buffer, "\n");
    while (line != NULL)
    {
      sub_system = strtok (line, ",");
      if (NULL == sub_system)
        break;
      peer = strtok (NULL, ",");
      if (NULL == peer)
        break;
      key = strtok (NULL, ",");
      if (NULL == key)
        break;
      value = strtok (NULL, ",");
      if (NULL == value)
        break;
      expiry = strtok (NULL, ",");
      if (NULL == expiry)
        break;
      entry = GNUNET_new (struct GNUNET_PEERSTORE_Record);
      entry->sub_system = GNUNET_strdup (sub_system);
      entry->key = GNUNET_strdup (key);
      {
        size_t s;
        char *o;

        o = NULL;
        s = GNUNET_STRINGS_base64_decode (peer,
                                          strlen (peer),
                                          (void **) &o);
        if (sizeof(struct GNUNET_PeerIdentity) == s)
          GNUNET_memcpy (&entry->peer,
                         o,
                         s);
        else
          GNUNET_break (0);
        GNUNET_free (o);
      }
      entry->value_size = GNUNET_STRINGS_base64_decode (value,
                                                        strlen (value),
                                                        (void **) &entry->value);
      if (GNUNET_SYSERR ==
          GNUNET_STRINGS_fancy_time_to_absolute (expiry,
                                                 &entry->expiry))
      {
        GNUNET_free (entry->sub_system);
        GNUNET_free (entry->key);
        GNUNET_free (entry);
        break;
      }
      peer_id = GNUNET_i2s (&entry->peer);
      GNUNET_CRYPTO_hash (peer_id,
                          strlen (peer_id),
                          &hkey);

      GNUNET_assert (GNUNET_OK == GNUNET_CONTAINER_multihashmap_put (plugin->hm,
                                                                     &hkey,
                                                                     entry,
                                                                     GNUNET_CONTAINER_MULTIHASHMAPOPTION_MULTIPLE));
    }
  }
  GNUNET_free (buffer);
  return GNUNET_OK;
}


static int
store_and_free_entries (void *cls,
                        const struct GNUNET_HashCode *key,
                        void *value)
{
  struct GNUNET_DISK_FileHandle *fh = cls;
  struct GNUNET_PEERSTORE_Record *entry = value;
  char *line;
  char *peer;
  const char *expiry;
  char *val;

  GNUNET_STRINGS_base64_encode (entry->value,
                                entry->value_size,
                                &val);
  expiry = GNUNET_STRINGS_absolute_time_to_string (entry->expiry);
  GNUNET_STRINGS_base64_encode ((char *) &entry->peer,
                                sizeof(struct GNUNET_PeerIdentity),
                                &peer);
  GNUNET_asprintf (&line,
                   "%s,%s,%s,%s,%s",
                   entry->sub_system,
                   peer,
                   entry->key,
                   val,
                   expiry);
  GNUNET_free (val);
  GNUNET_free (peer);
  GNUNET_DISK_file_write (fh,
                          line,
                          strlen (line));
  GNUNET_free (entry->sub_system);
  GNUNET_free (entry->key);
  GNUNET_free (entry->value);
  GNUNET_free (entry);
  GNUNET_free (line);
  return GNUNET_YES;
}


/**
 * Shutdown database connection and associate data
 * structures.
 * @param plugin the plugin context (state for this module)
 */
static void
database_shutdown (struct Plugin *plugin)
{
  struct GNUNET_DISK_FileHandle *fh;

  fh = GNUNET_DISK_file_open (plugin->fn,
                              GNUNET_DISK_OPEN_CREATE
                              | GNUNET_DISK_OPEN_TRUNCATE
                              | GNUNET_DISK_OPEN_READWRITE,
                              GNUNET_DISK_PERM_USER_WRITE
                              | GNUNET_DISK_PERM_USER_READ);
  if (NULL == fh)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                _ ("Unable to initialize file: %s.\n"),
                plugin->fn);
    return;
  }
  GNUNET_CONTAINER_multihashmap_iterate (plugin->hm,
                                         &store_and_free_entries,
                                         fh);
  GNUNET_CONTAINER_multihashmap_destroy (plugin->hm);
  GNUNET_DISK_file_close (fh);
}


/**
 * Entry point for the plugin.
 *
 * @param cls The struct GNUNET_CONFIGURATION_Handle.
 * @return NULL on error, otherwise the plugin context
 */
void *
libgnunet_plugin_peerstore_flat_init (void *cls)
{
  static struct Plugin plugin;
  const struct GNUNET_CONFIGURATION_Handle *cfg = cls;
  struct GNUNET_PEERSTORE_PluginFunctions *api;

  if (NULL != plugin.cfg)
    return NULL;                /* can only initialize once! */
  memset (&plugin, 0, sizeof(struct Plugin));
  plugin.cfg = cfg;
  if (GNUNET_OK != database_setup (&plugin))
  {
    database_shutdown (&plugin);
    return NULL;
  }
  api = GNUNET_new (struct GNUNET_PEERSTORE_PluginFunctions);
  api->cls = &plugin;
  api->store_record = &peerstore_flat_store_record;
  api->iterate_records = &peerstore_flat_iterate_records;
  api->expire_records = &peerstore_flat_expire_records;
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Flat plugin is running\n");
  return api;
}


/**
 * Exit point from the plugin.
 *
 * @param cls The plugin context (as returned by "init")
 * @return Always NULL
 */
void *
libgnunet_plugin_peerstore_flat_done (void *cls)
{
  struct GNUNET_PEERSTORE_PluginFunctions *api = cls;
  struct Plugin *plugin = api->cls;

  database_shutdown (plugin);
  plugin->cfg = NULL;
  GNUNET_free (api);
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Flat plugin is finished\n");
  return NULL;
}


/* end of plugin_peerstore_sqlite.c */
