/* ========================================================================== */
/*! \file
 * \brief Article header database (cache)
 *
 * Copyright (c) 2012-2024 by the developers. See the LICENSE file for details.
 *
 * If nothing else is specified, function return zero to indicate success
 * and a negative value to indicate an error.
 */


/* ========================================================================== */
/* Include headers */

#include "posix.h"  /* Include this first because of feature test macros */

#include <string.h>

#include "database.h"
#include "encoding.h"
#include "fileutils.h"
#include "main.h"
#include "xdg.h"


/* ========================================================================== */
/*! \defgroup DATABASE DATA: Database for header cache
 *
 * Location of article header database: \c $XDG_CONFIG_HOME/$CFG_NAME/headers
 *
 * This database use no special data structures, instead a subdirectory is
 * created for every group. Inside this directory, for every entry a regular
 * file that contains the article header is created with the article watermark
 * as its name.
 *
 * Every new entry is first written to a temporary file \c .tmp , then pushed to
 * disk and finally merged into the database by atomically renaming the
 * temporary file.
 *
 * All this together makes this database slow and inefficient, but very robust.
 * The data structures should never become damaged - even if the program crash
 * while currently writing to the database it does not become corrupt and no
 * special recovery is necessary to make it usable again.
 */
/*! @{ */


/* ========================================================================== */
/* Constants */

/*! \brief Message prefix for DATABASE module */
#define MAIN_ERR_PREFIX  "DATA: "

/*! \brief Permissions for database content files */
#define DB_PERM  (api_posix_mode_t) (API_POSIX_S_IRUSR | API_POSIX_S_IWUSR)


/* ========================================================================== */
/* Variables */

static api_posix_pthread_mutex_t db_mutex = API_POSIX_PTHREAD_MUTEX_INITIALIZER;
static int  db_mutex_state = 0;
static const char*  db_path = NULL;
static size_t  db_path_len = 0;


/* ========================================================================== */
/* Lock mutex
 *
 * The current state of the mutex is stored in \ref db_mutex_state and nothing
 * is done if \ref db_mutex is already locked.
 *
 * \attention
 * Locking the \ref db_mutex and updating \ref db_mutex_state must be an atomic
 * operation.
 */

static int  db_mutex_lock(void)
{
   int  res = -1;
   int  rv;
   int  cs;

   if(!db_mutex_state)
   {
      rv = api_posix_pthread_setcancelstate(API_POSIX_PTHREAD_CANCEL_DISABLE,
                                            &cs);
      if(rv)  { PRINT_ERROR("Setting thread cancelability state failed"); }
      else
      {
         rv = api_posix_pthread_mutex_lock(&db_mutex);
         if(rv)  { PRINT_ERROR("Locking mutex failed"); }
         else  { db_mutex_state = 1;  res = 0; }
         rv = api_posix_pthread_setcancelstate(cs, &cs);
         if(rv)
         {
            PRINT_ERROR("Restoring thread cancelability state failed");
         }
      }
   }

   return(res);
}


/* ========================================================================== */
/* Unlock mutex
 *
 * The current state of the mutex is stored in \ref db_mutex_state and nothing
 * is done if \ref db_mutex is already unlocked.
 *
 * \attention
 * Locking the \ref db_mutex and updating \ref db_mutex_state must be an atomic
 * operation.
 */

static int  db_mutex_unlock(void)
{
   int  res = -1;
   int  rv;
   int  cs;

   if(db_mutex_state)
   {
      rv = api_posix_pthread_setcancelstate(API_POSIX_PTHREAD_CANCEL_DISABLE,
                                            &cs);
      if(rv)  { PRINT_ERROR("Setting thread cancelability state failed"); }
      else
      {
         rv = api_posix_pthread_mutex_unlock(&db_mutex);
         if(rv)  { PRINT_ERROR("Unlocking mutex failed"); }
         else  { db_mutex_state = 0;  res = 0; }
         rv = api_posix_pthread_setcancelstate(cs, &cs);
         if(rv)
         {
            PRINT_ERROR("Restoring thread cancelability state failed");
         }
      }
   }

   return(res);
}


/* ========================================================================== */
/* Dummy compare function for \c scandir()
 *
 * \return
 * - Always 0 so that the sort order will be undefined.
 */

static int  db_compar_dummy(const api_posix_struct_dirent**  a,
                            const api_posix_struct_dirent**  b)
{
   (void) a;
   (void) b;

   return(0);
}


/* ========================================================================== */
/* Numerical compare function for \c scandir()
 *
 * \note
 * Leading zeros and non-digit characters are not supported and the result is
 * undefined in this case.
 *
 * \return
 * - 0 for equality
 * - 1 if a is greater
 * - -1 if b is greater
 */

static int  db_compar_num(const api_posix_struct_dirent**  a,
                          const api_posix_struct_dirent**  b)
{
   int  res = 0;
   const char*  name_a = (*a)->d_name;
   const char*  name_b = (*b)->d_name;
   size_t  i = 0;

   while(1)
   {
      if(!name_a[i])
      {
         if(name_b[i])  { res = -1; }
         break;
      }
      if(!name_b[i])
      {
         if(name_a[i])  { res = 1; }
         break;
      }
      if(!res)
      {
         if(name_a[i] != name_b[i])
         {
            if(name_a[i] < name_b[i])  { res = -1; }  else  { res = 1; }
         }
      }
      ++i;
   }

   /* printf("compar: a=%s, b=%s => res=%d\n", name_a, name_b, res); */

   return(res);
}


/* ========================================================================== */
/* Get config file pathname
 *
 * The caller is responsible to free the memory for the buffer on success.
 */

static int  db_get_path(const char**  dbpath)
{
   static const char  dbdir[] = "headers/";
   const char*  confdir = xdg_get_confdir(CFG_NAME);
   int  res = -1;
   int  rv;

   /* Init result so that 'free()' can be called in all cases */
   *dbpath = NULL;

   if(NULL != confdir)
   {
      *dbpath = confdir;
      rv = xdg_append_to_path(dbpath, dbdir);
      if(0 == rv)
      {
         /* Create database directory if it doesn't exist */
         res = fu_create_path(*dbpath, (api_posix_mode_t) API_POSIX_S_IRWXU);
      }
   }

   /* Free memory on error */
   if(res)
   {
      PRINT_ERROR("Cannot create database directory");
      api_posix_free((void*) *dbpath);
      *dbpath = NULL;
   }

   return(res);
}


/* ========================================================================== */
/* Init database without locking mutex */

static int  db_init_unlocked(void)
{
   int  res = 0;

   /* Return success if already initialized */
   if(NULL == db_path)
   {
      res = db_get_path(&db_path);
      if(!res) { db_path_len = strlen(db_path); }
      else
      {
         PRINT_ERROR("Initializing database failed");
         db_path = NULL;
         db_path_len = 0;
      }
   }

   return(res);
}


/* ========================================================================== */
/* Shutdown database without locking mutex */

static int  db_exit_unlocked(void)
{
   if(NULL != db_path)
   {
      api_posix_free((void*) db_path);
      db_path = NULL;
      db_path_len = 0;
   }

   return(0);
}


/* ========================================================================== */
/*! \brief Init database
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */

int  db_init(void)
{
   int  res = -1;
   int  rv;

   rv = db_mutex_lock();
   if(!rv)
   {
      res = db_init_unlocked();
      db_mutex_unlock();
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Shutdown database
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */

int  db_exit(void)
{
   int  res = -1;
   int  rv;

   rv = db_mutex_lock();
   if(!rv)
   {
      res = db_exit_unlocked();
      db_mutex_unlock();
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Delete all database content
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */

int  db_clear(void)
{
   int  res = -1;
   int  rv;

   rv = db_mutex_lock();
   if(!rv)
   {
      if(NULL == db_path)  { PRINT_ERROR("Database not initialized"); }
      else  { res = fu_delete_tree(db_path); }
      /* Reinitialize without unlocking mutex */
      db_exit_unlocked();
      db_init_unlocked();
      db_mutex_unlock();
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Delete database content for all groups that are \b not specified
 *
 * \param[in] groupcount  Number of group names in array \e grouplist
 * \param[in] grouplist   Array of group names
 *
 * If \e groupcount is zero, the database content for all groups is deleted.
 * The parameter \e grouplist is ignored in this case and may be \c NULL .
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */

int  db_update_groups(size_t  groupcount, const char**  grouplist)
{
   int  res = -1;
   int  rv;
   int  num;
   api_posix_struct_dirent**  content;
   const char*  entry;
   size_t  i;
   size_t  ii;
   int  found;
   char*  path;

   rv = db_mutex_lock();
   if(!rv)
   {
      if(NULL == db_path)  { PRINT_ERROR("Database not initialized"); }
      else
      {
         /* Get groups currently present in database */
         num = api_posix_scandir(db_path, &content, NULL, db_compar_dummy);
         if(0 <= num)
         {
            for(i = 0; i < (size_t) num; ++i)
            {
               entry = content[i]->d_name;
               /* Ignore "." and ".." entries */
               if(!strcmp(".", entry))  { continue; }
               if(!strcmp("..", entry))  { continue; }
               /* Check whether group must be preserved */
               found = 0;
               for(ii = 0; ii < groupcount; ++ii)
               {
                  if(!strcmp(grouplist[ii], entry))
                  {
                     found = 1;
                     break;
                  }
               }
               if(!found)
               {
                  /* Remove group from database */
                  path = (char*) api_posix_malloc(strlen(db_path)
                                                  + strlen(entry)
                                                  + (size_t) 1);
                  if(NULL == path)
                  {
                     PRINT_ERROR("Cannot allocate memory for path");
                     break;
                  }
                  else
                  {
                     strcpy(path, db_path);
                     strcat(path, entry);
                     res = fu_delete_tree(path);
                     api_posix_free((void*) path);
                     if(res)  { break; }
                  }
               }
            }
            /* Free memory allocated by scandir() */
            while(num--)  { api_posix_free((void*) content[num]); }
            api_posix_free((void*) content);
         }
      }
      db_mutex_unlock();
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Add entry
 *
 * \param[in] group   Newsgroup of article
 * \param[in] anum    Article ID
 * \param[in] header  Pointer to article header
 * \param[in] len     Length of article header
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */

int  db_add(const char*  group, core_anum_t  anum,
            const char*  header, size_t  len)
{
   static char  tmpfile[] = ".tmp";
   static size_t  tmpfile_len = sizeof(tmpfile) - (size_t) 1;
   char  file[17];
   size_t  file_len;
   int  res = -1;
   char*  tmppathname = NULL;
   char*  pathname = NULL;
   int  rv;
   int  fd;

   if(NULL == db_path)
   {
      PRINT_ERROR("Database not initialized");
      return(res);
   }

   /* Verify parameters */
   if(NULL == group || !anum || NULL == header)
   {
      PRINT_ERROR("db_add() called with invalid parameters");
      return(res);
   }

   rv = db_mutex_lock();
   if(!rv)
   {
      /* Calculate memory requirements for pathnames */
      rv = enc_convert_anum_to_ascii(file, &file_len, anum);
      if(!rv)
      {
         /* The additional bytes are for '/' and the terminating NUL */
         tmppathname = (char*) api_posix_malloc(db_path_len + strlen(group)
                                                + tmpfile_len + (size_t) 2);
         pathname = (char*) api_posix_malloc(db_path_len + strlen(group)
                                             + file_len + (size_t) 2);
         if (NULL == tmppathname || NULL == pathname)
         {
            PRINT_ERROR("Cannot allocate memory for database pathname");
         }
         else
         {
            /* Create group directory if it doesn't exist */
            strcpy(tmppathname, db_path);
            strcat(tmppathname, group);
            strcat(tmppathname, "/");
            rv = api_posix_mkdir(tmppathname,
                                 (api_posix_mode_t) API_POSIX_S_IRWXU);
            if (!rv || (-1 == rv && API_POSIX_EEXIST == api_posix_errno))
            {
               strcat(tmppathname, tmpfile);
               /* Open and lock temporary file */
               rv = fu_open_file(tmppathname, &fd,
                                 API_POSIX_O_WRONLY | API_POSIX_O_CREAT
                                 | API_POSIX_O_TRUNC, DB_PERM);
               if(!rv)
               {
                  rv = fu_lock_file(fd);
                  if(!rv)
                  {
                     /* Write header into temporary file */
                     rv = fu_write_to_filedesc(fd, header, len);
                     if(!rv)
                     {
                        rv = fu_sync(fd, NULL);
                        if(!rv)
                        {
                           /* Rename temporary file to new entry file */
                           strcpy(pathname, db_path);
                           strcat(pathname, group);
                           strcat(pathname, "/");
                           strcat(pathname, file);
                           rv = api_posix_rename(tmppathname, pathname);
                        }
                     }
                     if(rv)
                     {
                        /* Unlink temporary file on error */
                        PRINT_ERROR("Failed to store data");
                        if(NULL != tmppathname)
                        {
                           (void) fu_unlink_file(tmppathname);
                        }
                     }
                     else  { res = 0; }
                  }
                  fu_close_file(&fd, NULL);
               }
            }
            else  { PRINT_ERROR("Cannot create group directory"); }
         }
      }
      api_posix_free((void*) pathname);
      api_posix_free((void*) tmppathname);
      db_mutex_unlock();
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Read entry
 *
 * \param[in]  group   Newsgroup of article
 * \param[in]  anum    Article ID
 * \param[out] header  Pointer to article header buffer
 * \param[out] len     Pointer to length of article header buffer (not content!)
 *
 * On success, the caller is responsible to free the memory allocated for the
 * article header buffer.
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */

int  db_read(const char*  group, core_anum_t  anum, char**  header,
             size_t*  len)
{
   int  res = -1;
   int  rv;
   char  file[17];
   size_t  file_len;
   char*  pathname = NULL;
   int  fd;

   if(NULL == db_path)
   {
      PRINT_ERROR("Database not initialized");
      return(res);
   }

   /* Verify parameters */
   if(NULL == group || !anum || NULL == header)
   {
      PRINT_ERROR("db_read() called with invalid parameters");
      return(res);
   }

   rv = db_mutex_lock();
   if(!rv)
   {
      /* Calculate memory requirements for pathname */
      rv = enc_convert_anum_to_ascii(file, &file_len, anum);
      if(!rv)
      {
         /* The additional bytes are for '/' and the terminating NUL */
         pathname = (char*) api_posix_malloc(db_path_len + strlen(group)
                                             + file_len + (size_t) 2);
         if (NULL == pathname)
         {
            PRINT_ERROR("Cannot allocate memory for database pathname");
         }
         else
         {
            strcpy(pathname, db_path);
            strcat(pathname, group);
            strcat(pathname, "/");
            strcat(pathname, file);
            rv = fu_open_file(pathname, &fd, API_POSIX_O_RDWR, 0);
            if(-1 != rv)
            {
               rv = fu_lock_file(fd);
               if(!rv)
               {
                  rv = fu_read_whole_file(fd, header, len);
                  if(!rv)  { res = 0; }
               }
               fu_close_file(&fd, NULL);
            }
         }
      }
      api_posix_free((void*) pathname);
      db_mutex_unlock();
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Delete entries
 *
 * \param[in] group   Newsgroup of article
 * \param[in] start   Start ID of article range
 * \param[in] end     End ID of article range
 *
 * To delete all entries of \e group , specify both \e start and \e end as 0.
 *
 * To delete anything from the beginning up to \e end, specify 1 for \e start
 * and this function determines the first entry automatically without trying
 * to delete (in worst case) billions of nonexistent entries one by one.
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */

int  db_delete(const char*  group, core_anum_t  start, core_anum_t  end)
{
   int  res = -1;
   int  rv;
   char  file[17];
   size_t  file_len;
   char*  pathname = NULL;
   size_t  path_len;
   core_anum_t  i = start;
   api_posix_struct_dirent**  content;
   int  num;
   const char*  entry;
   size_t  ii;
   core_anum_t  e;

   if(NULL == db_path)
   {
      PRINT_ERROR("Database not initialized");
      return(res);
   }

   /* Verify parameters */
   if(NULL == group || end < start)
   {
      PRINT_ERROR("db_delete() called with invalid parameters");
      return(res);
   }

   rv = db_mutex_lock();
   if(!rv)
   {
      /* Calculate maximum memory requirements for pathname */
      /* The additional 2 bytes are for '/' and the terminating NUL */
      pathname = (char*) api_posix_malloc(db_path_len + strlen(group)
                                          + (size_t) 17 + (size_t) 2);
      if (NULL == pathname)
      {
         PRINT_ERROR("Cannot allocate memory for database pathname");
      }
      else
      {
         strcpy(pathname, db_path);
         strcat(pathname, group);
         strcat(pathname, "/");
         path_len = strlen(pathname);
         /* Check whether whole group should be cleared */
         if(!start && !end)
         {
            /* Yes => Delete subdirectory of group */
            /* printf("Delete database subtree: %s\n", pathname); */
            res = fu_delete_tree(pathname);
         }
         else if(!start || !end)
         {
            PRINT_ERROR("Invalid range specified for deletion");
         }
         else
         {
            /* Determine first entry of database */
            num = api_posix_scandir(pathname, &content, NULL, db_compar_num);
            if(0 <= num)
            {
               /* The entries were numerically sorted by 'scandir()' */
               for(ii = 0; ii < (size_t) num; ++ii)
               {
                  entry = content[ii]->d_name;
                  /* Ignore "." and ".." entries */
                  if(!strcmp(".", entry))  { continue; }
                  if(!strcmp("..", entry))  { continue; }
                  rv = enc_convert_ascii_to_anum(&e, entry,
                                                 (int) strlen(entry));
                  if(!rv)
                  {
                     /* Clamp range start to beginning of database content */
                     if(e > start)  { i = e; }
                  }
                  break;
               }
               while(i <= end)
               {
                  rv = enc_convert_anum_to_ascii(file, &file_len, i);
                  if(rv)  { break; }
                  else
                  {
                     pathname[path_len] = 0;
                     strncpy(&pathname[path_len], file, 17);
                     /* Unlink file of entry */
                     (void) fu_unlink_file(pathname);
                     /* Continue if entry was not found */
                  }
                  if(i == end)  { res = 0; }
                  ++i;
               }
               /* Free memory allocated by scandir() */
               while(num--)  { api_posix_free((void*) content[num]); }
               api_posix_free((void*) content);
            }
         }
      }
      api_posix_free((void*) pathname);
      db_mutex_unlock();
   }

   return(res);
}


/*! @} */

/* EOF */
